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

rogerpadilla / uql / 29210089652

12 Jul 2026 09:40PM UTC coverage: 95.051% (+0.08%) from 94.975%
29210089652

push

github

rogerpadilla
fix(package): update gitHead to latest commit hash

3297 of 3643 branches covered (90.5%)

Branch coverage included in aggregate %.

5691 of 5813 relevant lines covered (97.9%)

455.5 hits per line

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

97.78
/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
  isQueryAggregateOp,
10
  type MongoId,
11
  type OnFieldCallback,
12
  type QueryAggregateOp,
13
  type QueryExclude,
14
  type QueryGroupMap,
15
  type QueryOptions,
16
  QueryRaw,
17
  type QuerySelect,
18
  type QuerySortMap,
19
  type QueryVectorSearch,
20
  type QueryWhere,
21
  type QueryWhereMap,
22
  type RelationKey,
23
  type UqlContext,
24
} from '../type/index.js';
25
import { getFieldKeys, getKeys, hasKeys, someKey } from './object.util.js';
26

27
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
28

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

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

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

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

89
export function getFieldCallbackValue(val: OnFieldCallback) {
90
  return typeof val === 'function' ? val() : val;
968✔
91
}
92

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

101
export function fillOnFields<E>(meta: EntityMeta<E>, payload: E | E[], callbackKey: CallbackKey): E[] {
102
  const payloads = Array.isArray(payload) ? payload : [payload];
790✔
103
  const keys = getKeys(meta.fields).filter((key) => meta.fields[key]![callbackKey]!) as FieldKey<E>[];
6,197✔
104
  if (keys.length === 0) {
790✔
105
    return payloads;
46✔
106
  }
107
  for (const it of payloads) {
744✔
108
    for (const key of keys) {
978✔
109
      if (it[key] === undefined) {
1,103✔
110
        it[key] = getFieldCallbackValue(meta.fields[key]![callbackKey]!) as E[typeof key];
779✔
111
      }
112
    }
113
  }
114
  return payloads;
744✔
115
}
116

117
export function filterPersistableRelationKeys<E>(
118
  meta: EntityMeta<E>,
119
  payload: E,
120
  action: CascadeType,
121
): RelationKey<E>[] {
122
  const keys = getKeys(payload as object);
1,268✔
123
  return keys.filter((key) => {
1,268✔
124
    const relOpts = meta.relations[key];
4,347✔
125
    return relOpts && isCascadable(action, relOpts.cascade);
4,347✔
126
  }) as RelationKey<E>[];
127
}
128

129
export function isCascadable(action: CascadeType, configuration?: boolean | CascadeType): boolean {
130
  if (typeof configuration === 'boolean') {
1,444✔
131
    return configuration;
247✔
132
  }
133
  return configuration === action;
1,197✔
134
}
135

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

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

171
  const allFields = getFieldKeys(meta.fields);
245✔
172
  if (!excludedFields) {
245✔
173
    return allFields;
228✔
174
  }
175
  const excluded = excludedFields;
17✔
176
  return allFields.filter((it) => !excluded.has(it));
116✔
177
}
178

179
export function buildSortMap<E>(sort: QuerySortMap<E> | undefined): QuerySortMap<E> {
180
  return (sort ?? {}) as QuerySortMap<E>;
429✔
181
}
182

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

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

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

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

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

248
  for (const name of getKeys(meta.filters)) {
981✔
249
    const filter = meta.filters[name];
1,000✔
250

251
    let active: boolean;
252
    if (filter.security) {
1,000✔
253
      active = true;
8✔
254
    } else if (opts?.filters === false) {
992✔
255
      active = false;
7✔
256
    } else {
257
      active = opts?.filters?.[name] ?? filter.default !== false;
985✔
258
    }
259
    if (!active) {
1,000✔
260
      continue;
60✔
261
    }
262

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

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

289
  if (securityConditions.length) {
979✔
290
    const existing = result['$and'] as unknown[] | undefined;
5✔
291
    result['$and'] = existing ? [...existing, ...securityConditions] : securityConditions;
5✔
292
  }
293

294
  return result as QueryWhereMap<E>;
979✔
295
}
296

297
function isIdValue<E>(filter: QueryWhere<E>): filter is IdValue<E> | IdValue<E>[] {
298
  const type = typeof filter;
17,957✔
299
  return (
17,957✔
300
    type === 'string' ||
88,968✔
301
    type === 'number' ||
302
    type === 'bigint' ||
303
    typeof (filter as MongoId).toHexString === 'function' ||
304
    Array.isArray(filter)
305
  );
306
}
307

308
/**
309
 * Parsed entry from a `$group` map - either a raw group key or an aggregate function call.
310
 */
311
export type ParsedGroupEntry =
312
  | { readonly kind: 'key'; readonly alias: string }
313
  | { readonly kind: 'fn'; readonly alias: string; readonly op: QueryAggregateOp; readonly fieldRef: string };
314

315
/**
316
 * Parse a `QueryGroupMap` into structured entries consumable by any dialect.
317
 * Eliminates the duplicated `value === true` / `typeof value === 'object'` pattern.
318
 */
319
export function parseGroupMap<E>(group: QueryGroupMap<E>): ParsedGroupEntry[] {
320
  const entries: ParsedGroupEntry[] = [];
77✔
321
  for (const alias of getKeys(group)) {
77✔
322
    const value = group[alias];
159✔
323
    if (value === true) {
159✔
324
      entries.push({ kind: 'key', alias });
55✔
325
    } else if (value && typeof value === 'object') {
104✔
326
      const fnEntry = value as Record<string, string>;
101✔
327
      const key = getKeys(fnEntry)[0];
101✔
328
      if (!isQueryAggregateOp(key)) {
101✔
329
        throw TypeError(`unsupported aggregate operator: ${key}`);
3✔
330
      }
331
      entries.push({ kind: 'fn', alias, op: key, fieldRef: fnEntry[key] });
98✔
332
    }
333
  }
334
  return entries;
74✔
335
}
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