• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rogerpadilla / uql / 30055935090

24 Jul 2026 12:20AM UTC coverage: 95.028% (-0.01%) from 95.038%
30055935090

push

github

rogerpadilla
chore: update devDependencies for biome and lint-staged; upgrade better-sqlite3 in uql-orm

3328 of 3676 branches covered (90.53%)

Branch coverage included in aggregate %.

5732 of 5858 relevant lines covered (97.85%)

599.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.94
/packages/uql-orm/src/util/dialect.util.ts
1
import { getContext, UqlSecurityError } from '../context/context.js';
2
import {
3
  type CascadeType,
4
  type EntityMeta,
5
  type FieldKey,
6
  type FieldOptions,
7
  type FilterOnMissing,
8
  type IdValue,
9
  type MongoId,
10
  type OnFieldCallback,
11
  type QueryAggMap,
12
  type QueryAggregateArg,
13
  type QueryAggregateDistinctOp,
14
  type QueryAggregateOp,
15
  type QueryExclude,
16
  type QueryGroupMap,
17
  type QueryOptions,
18
  QueryRaw,
19
  type QuerySelect,
20
  type QuerySortMap,
21
  type QueryVectorSearch,
22
  type QueryWhere,
23
  type QueryWhereMap,
24
  type RelationKey,
25
  resolveAggregateOp,
26
  type UqlContext,
27
} from '../type/index.js';
28
import { getFieldKeys, getKeys, hasKeys, someKey } from './object.util.js';
29

30
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
31

32
export function filterFieldKeys<E>(meta: EntityMeta<E>, payload: E, callbackKey: CallbackKey): FieldKey<E>[] {
33
  return getKeys(payload as object).filter((key) => {
359✔
34
    const fieldOpts = meta.fields[key];
898✔
35
    return fieldOpts && !fieldOpts.virtual && (callbackKey !== 'onUpdate' || fieldOpts.updatable !== false);
898✔
36
  }) as FieldKey<E>[];
37
}
38

39
/** Whether `key` is a real, non-virtual field that `record` provides a defined value for. */
40
function isInsertableField<E>(meta: EntityMeta<E>, record: E, key: FieldKey<E>): boolean {
41
  const field = meta.fields[key];
3,277✔
42
  return !!field && !field.virtual && record[key] !== undefined;
3,277✔
43
}
44

45
/** Appends `record`'s not-yet-`seen` insertable keys (real, non-virtual, defined value) to `keys`. */
46
function addInsertFieldKeys<E>(meta: EntityMeta<E>, record: E, seen: Set<FieldKey<E>>, keys: FieldKey<E>[]): void {
47
  for (const key of getKeys(record as object) as FieldKey<E>[]) {
1,317✔
48
    if (!seen.has(key) && isInsertableField(meta, record, key)) {
3,286✔
49
      seen.add(key);
3,118✔
50
      keys.push(key);
3,118✔
51
    }
52
  }
53
}
54

55
/**
56
 * Resolves the columns of an INSERT statement: the union of the persistable fields provided by
57
 * any record (in first-seen order), plus every `onInsert` field. Records missing one of these
58
 * columns insert its database default.
59
 *
60
 * The column list is seeded from the first record, then extended only by records that introduce a
61
 * new column. A homogeneous batch (every record the same shape, the common case) is detected with
62
 * {@link someKey}, which walks a record without allocating a key array, so only the rare record that
63
 * actually adds a column pays for a full rescan.
64
 *
65
 * `onInsert` fields are always included so the column set is stable whether or not the caller
66
 * has run {@link fillOnFields} first (it stamps them on every record, but the querier's
67
 * chunk-size estimate inspects the raw payload).
68
 */
69
export function getInsertFieldKeys<E>(meta: EntityMeta<E>, payloads: E[]): FieldKey<E>[] {
70
  const seen = new Set<FieldKey<E>>();
1,292✔
71
  const keys: FieldKey<E>[] = [];
1,292✔
72
  addInsertFieldKeys(meta, payloads[0], seen, keys);
1,292✔
73
  for (let i = 1; i < payloads.length; i++) {
1,292✔
74
    const record = payloads[i]!;
594✔
75
    if (
594✔
76
      someKey(
77
        record as object,
78
        (key) => !seen.has(key as FieldKey<E>) && isInsertableField(meta, record, key as FieldKey<E>),
1,398✔
79
      )
80
    ) {
81
      addInsertFieldKeys(meta, record, seen, keys);
25✔
82
    }
83
  }
84
  for (const key of getKeys(meta.fields) as FieldKey<E>[]) {
594✔
85
    if (meta.fields[key]!.onInsert !== undefined && !seen.has(key)) {
10,023✔
86
      keys.push(key);
494✔
87
    }
88
  }
89
  return keys;
1,292✔
90
}
91

92
export function getFieldCallbackValue(val: OnFieldCallback) {
93
  return typeof val === 'function' ? val() : val;
1,230✔
94
}
95

96
/**
97
 * Resolves the value stamped on the soft-delete field when deleting a row.
98
 * `true` stamps the current timestamp (`new Date()`); any other marker is an {@link OnFieldCallback}.
99
 */
100
export function getSoftDeleteValue(field: FieldOptions) {
101
  return field.softDelete === true ? new Date() : getFieldCallbackValue(field.softDelete as OnFieldCallback);
204✔
102
}
103

104
export function fillOnFields<E>(meta: EntityMeta<E>, payload: E | E[], callbackKey: CallbackKey): E[] {
105
  const payloads = Array.isArray(payload) ? payload : [payload];
1,018✔
106
  const keys = getKeys(meta.fields).filter((key) => meta.fields[key]![callbackKey]!) as FieldKey<E>[];
7,884✔
107
  if (keys.length === 0) {
1,018✔
108
    return payloads;
77✔
109
  }
110
  for (const it of payloads) {
941✔
111
    for (const key of keys) {
1,239✔
112
      if (it[key] === undefined) {
1,400✔
113
        it[key] = getFieldCallbackValue(meta.fields[key]![callbackKey]!) as E[typeof key];
1,025✔
114
      }
115
    }
116
  }
117
  return payloads;
941✔
118
}
119

120
export function filterPersistableRelationKeys<E>(
121
  meta: EntityMeta<E>,
122
  payload: E,
123
  action: CascadeType,
124
): RelationKey<E>[] {
125
  const keys = getKeys(payload as object);
1,685✔
126
  return keys.filter((key) => {
1,685✔
127
    const relOpts = meta.relations[key];
5,756✔
128
    return relOpts && isCascadable(action, relOpts.cascade);
5,756✔
129
  }) as RelationKey<E>[];
130
}
131

132
export function isCascadable(action: CascadeType, configuration?: boolean | CascadeType): boolean {
133
  if (typeof configuration === 'boolean') {
1,918✔
134
    return configuration;
324✔
135
  }
136
  return configuration === action;
1,594✔
137
}
138

139
export function normalizeScalarFieldSelection<E>(
140
  meta: EntityMeta<E>,
141
  select?: QuerySelect<E>,
142
  exclude?: QueryExclude<E>,
143
): FieldKey<E>[] {
144
  // A positive `$select` (the common case) wins outright and returns
145
  // before `$exclude` is ever scanned.
146
  const positiveFields: FieldKey<E>[] = [];
7,928✔
147
  let excludedFields: Set<FieldKey<E>> | undefined;
148
  if (select) {
7,928✔
149
    for (const key of getKeys(select)) {
7,615✔
150
      if (!(key in meta.fields)) continue;
7,878✔
151
      if (select[key]) {
7,865✔
152
        positiveFields.push(key as FieldKey<E>);
7,843✔
153
      } else {
154
        excludedFields ??= new Set<FieldKey<E>>();
22✔
155
        excludedFields.add(key);
22✔
156
      }
157
    }
158
    if (positiveFields.length) {
7,615✔
159
      return positiveFields;
7,604✔
160
    }
161
  }
162

163
  // No positive selection: every field minus the ones excluded by a falsy `$select` entry or a
164
  // truthy `$exclude` entry.
165
  if (exclude) {
324✔
166
    for (const key of getKeys(exclude)) {
13✔
167
      if (exclude[key] && key in meta.fields) {
12✔
168
        excludedFields ??= new Set<FieldKey<E>>();
10✔
169
        excludedFields.add(key);
10✔
170
      }
171
    }
172
  }
173

174
  const allFields = getFieldKeys(meta.fields);
324✔
175
  if (!excludedFields) {
324✔
176
    return allFields;
304✔
177
  }
178
  const excluded = excludedFields;
20✔
179
  return allFields.filter((it) => !excluded.has(it));
137✔
180
}
181

182
export function buildSortMap<E>(sort: QuerySortMap<E> | undefined): QuerySortMap<E> {
183
  return (sort ?? {}) as QuerySortMap<E>;
464✔
184
}
185

186
/** Type guard: checks whether a sort value is a vector similarity search. */
187
export function isVectorSearch(value: unknown): value is QueryVectorSearch {
188
  return value !== null && typeof value === 'object' && '$vector' in (value as Record<string, unknown>);
671✔
189
}
190

191
export function augmentWhere<E>(
192
  meta: EntityMeta<E>,
193
  target: QueryWhere<E> = {},
205✔
194
  source: QueryWhere<E> = {},
205✔
195
): QueryWhere<E> {
196
  const targetComparison = buildQueryWhereAsMap(meta, target);
205✔
197
  const sourceComparison = buildQueryWhereAsMap(meta, source);
205✔
198
  return {
205✔
199
    ...targetComparison,
200
    ...sourceComparison,
201
  };
202
}
203

204
/**
205
 * Normalizes any `$where` shape (id, id[], raw, or map) to a `QueryWhereMap`. Read-only: for a map
206
 * input it returns that same object by reference (no copy), so callers must not mutate the result -
207
 * {@link applyFilters} and {@link augmentWhere} return new objects instead.
208
 */
209
export function buildQueryWhereAsMap<E>(meta: EntityMeta<E>, filter: QueryWhere<E> = {}): QueryWhereMap<E> {
24,086✔
210
  if (filter instanceof QueryRaw) {
24,086✔
211
    return { $and: [filter] } as QueryWhereMap<E>;
5✔
212
  }
213
  if (isIdValue(filter)) {
24,081✔
214
    return {
879✔
215
      [meta.id]: filter,
216
    } as QueryWhereMap<E>;
217
  }
218
  return filter as QueryWhereMap<E>;
23,202✔
219
}
220

221
/** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
222
export function withoutSoftDeleteFilter(filters: QueryOptions['filters']): QueryOptions['filters'] {
223
  return filters === false ? false : { ...filters, softDelete: false };
894!
224
}
225

226
/**
227
 * Returns a new `$where` map with every active entity filter's condition merged in, resolving
228
 * parameterized conditions against the explicit or ambient {@link UqlContext}. Never mutates the input.
229
 *
230
 * Convenience filters are active by default (unless `opts.filters === false` or bypassed by name), and
231
 * their keys are applied only when absent from the map, so an explicit `$where` on that key opts out.
232
 *
233
 * `security` filters are always active (bypass is ignored) and AND-merged, so a client `$where` on the
234
 * same field can't override them. A security condition that returns `undefined` fails the query closed
235
 * (throws {@link UqlSecurityError}) unless its `onMissing` is `skip`; one that returns an empty object
236
 * (`{}`) resolved to "no restriction" and adds nothing - the escape hatch for trusted cross-tenant
237
 * work (e.g. a maintenance job running under a `system` context).
238
 */
239
export function applyFilters<E>(
240
  meta: EntityMeta<E>,
241
  whereMap: QueryWhereMap<E>,
242
  opts?: QueryOptions,
243
): QueryWhereMap<E> {
244
  if (!meta.filters) {
11,192✔
245
    return whereMap;
9,925✔
246
  }
247
  const context = getContext();
1,267✔
248
  const result: Record<string, unknown> = { ...whereMap };
1,267✔
249
  const securityConditions: unknown[] = [];
1,267✔
250

251
  for (const name of getKeys(meta.filters)) {
1,267✔
252
    const filter = meta.filters[name];
1,286✔
253

254
    let active: boolean;
255
    if (filter.security) {
1,286✔
256
      active = true;
8✔
257
    } else if (opts?.filters === false) {
1,278✔
258
      active = false;
7✔
259
    } else {
260
      active = opts?.filters?.[name] ?? filter.default !== false;
1,271✔
261
    }
262
    if (!active) {
1,286✔
263
      continue;
70✔
264
    }
265

266
    const raw = filter.condition;
1,216✔
267
    const condition =
268
      typeof raw === 'function' ? (raw as (c: UqlContext | undefined) => QueryWhere<E> | undefined)(context) : raw;
1,216✔
269
    if (condition === undefined) {
1,286✔
270
      const onMissing: FilterOnMissing = filter.onMissing ?? (filter.security ? 'throw' : 'skip');
2!
271
      if (onMissing === 'throw') {
2!
272
        throw new UqlSecurityError(`filter '${name}' on '${meta.name ?? ''}' could not resolve (missing context)`);
2!
273
      }
274
      continue;
×
275
    }
276

277
    const conditionMap = buildQueryWhereAsMap(meta, condition) as Record<string, unknown>;
1,214✔
278
    if (!hasKeys(conditionMap)) {
1,214✔
279
      continue; // resolved to "no restriction" (e.g. a trusted system context) - nothing to merge
1✔
280
    }
281
    if (filter.security) {
1,213✔
282
      securityConditions.push(conditionMap);
5✔
283
    } else {
284
      for (const key of getKeys(conditionMap)) {
1,208✔
285
        if (result[key] === undefined) {
1,208✔
286
          result[key] = conditionMap[key];
1,191✔
287
        }
288
      }
289
    }
290
  }
291

292
  if (securityConditions.length) {
1,265✔
293
    const existing = result['$and'] as unknown[] | undefined;
5✔
294
    result['$and'] = existing ? [...existing, ...securityConditions] : securityConditions;
5✔
295
  }
296

297
  return result as QueryWhereMap<E>;
1,265✔
298
}
299

300
function isIdValue<E>(filter: QueryWhere<E>): filter is IdValue<E> | IdValue<E>[] {
301
  const type = typeof filter;
24,081✔
302
  return (
24,081✔
303
    type === 'string' ||
119,330✔
304
    type === 'number' ||
305
    type === 'bigint' ||
306
    typeof (filter as MongoId).toHexString === 'function' ||
307
    Array.isArray(filter)
308
  );
309
}
310

311
/**
312
 * Parsed entry from a `$group` map - either a raw group key or an aggregate function call.
313
 */
314
export type ParsedGroupEntry =
315
  | { readonly kind: 'key'; readonly alias: string }
316
  | {
317
      readonly kind: 'fn';
318
      readonly alias: string;
319
      readonly op: QueryAggregateOp;
320
      readonly fieldRef: string;
321
      /** `true` for a flat distinct op (`$countDistinct`, ...) → `COUNT(DISTINCT field)`. */
322
      readonly distinct: boolean;
323
    };
324

325
/**
326
 * Parse the `$group` (grouped columns) and `$agg` (computed aggregates) maps into structured
327
 * entries consumable by any dialect. Grouped columns come first, then computed columns.
328
 */
329
export function parseGroupMap<E>(group?: QueryGroupMap<E>, agg?: QueryAggMap<E>): ParsedGroupEntry[] {
330
  const entries: ParsedGroupEntry[] = [];
101✔
331
  const groupMap = group ?? {};
101✔
332
  for (const alias of getKeys(groupMap)) {
101✔
333
    if (groupMap[alias]) {
74✔
334
      entries.push({ kind: 'key', alias });
71✔
335
    }
336
  }
337
  if (!agg) {
101✔
338
    return entries;
5✔
339
  }
340
  for (const alias of getKeys(agg)) {
96✔
341
    const fnEntry: Partial<Record<QueryAggregateOp | QueryAggregateDistinctOp, QueryAggregateArg<E>>> = agg[alias];
122✔
342
    const key = getKeys(fnEntry)[0];
122✔
343
    // Flat DISTINCT ops (`$countDistinct`, ...) normalize to their base op + a `distinct` flag.
344
    const { op, distinct } = resolveAggregateOp(key);
122✔
345
    const fieldRef = fnEntry[key];
122✔
346
    if (fieldRef === undefined) {
122!
347
      throw TypeError(`empty aggregate function for: ${alias}`);
×
348
    }
349
    entries.push({ kind: 'fn', alias, op, fieldRef, distinct });
119✔
350
  }
351
  return entries;
93✔
352
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc