uql-orm 0.36.1 → 0.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/browser/http/http.d.ts +1 -1
  2. package/dist/browser/querier/httpQuerier.d.ts +12 -7
  3. package/dist/browser/querier/httpQuerier.js +6 -1
  4. package/dist/browser/type/clientQuerier.d.ts +10 -23
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +7 -7
  7. package/dist/cockroachdb/cockroachDialect.d.ts +8 -1
  8. package/dist/cockroachdb/cockroachDialect.js +12 -0
  9. package/dist/dialect/abstractSqlDialect.d.ts +18 -2
  10. package/dist/dialect/abstractSqlDialect.js +45 -12
  11. package/dist/dialect/mysqlLikeSqlDialect.d.ts +6 -0
  12. package/dist/dialect/mysqlLikeSqlDialect.js +19 -1
  13. package/dist/dialect/queryJoins.d.ts +6 -0
  14. package/dist/dialect/queryJoins.js +13 -0
  15. package/dist/http/contract.d.ts +0 -7
  16. package/dist/http/handler.d.ts +2 -2
  17. package/dist/http/handler.js +1 -1
  18. package/dist/http/query.js +3 -2
  19. package/dist/mongo/mongoDialect.d.ts +21 -1
  20. package/dist/mongo/mongoDialect.js +70 -11
  21. package/dist/mongo/mongodbQuerier.d.ts +13 -2
  22. package/dist/mongo/mongodbQuerier.js +44 -3
  23. package/dist/postgres/postgresDialect.d.ts +7 -0
  24. package/dist/postgres/postgresDialect.js +13 -0
  25. package/dist/querier/abstractQuerier.d.ts +34 -18
  26. package/dist/querier/abstractQuerier.js +39 -25
  27. package/dist/querier/abstractQuerierPool.d.ts +9 -7
  28. package/dist/querier/abstractQuerierPool.js +6 -0
  29. package/dist/querier/abstractSqlQuerier.d.ts +20 -2
  30. package/dist/querier/abstractSqlQuerier.js +44 -8
  31. package/dist/querier/relationCount.d.ts +16 -0
  32. package/dist/querier/relationCount.js +111 -0
  33. package/dist/sqlite/sqliteDialect.d.ts +6 -1
  34. package/dist/sqlite/sqliteDialect.js +10 -0
  35. package/dist/type/dialect.d.ts +4 -3
  36. package/dist/type/index.d.ts +1 -0
  37. package/dist/type/index.js +1 -0
  38. package/dist/type/querier.d.ts +21 -14
  39. package/dist/type/query.d.ts +77 -16
  40. package/dist/type/query.js +17 -0
  41. package/dist/type/universalQuerier.d.ts +85 -48
  42. package/dist/type/wire.d.ts +37 -0
  43. package/dist/type/wire.js +1 -0
  44. package/dist/util/dialect.util.d.ts +16 -0
  45. package/dist/util/dialect.util.js +25 -0
  46. package/dist/util/relationQuery.util.d.ts +9 -0
  47. package/dist/util/relationQuery.util.js +13 -0
  48. package/package.json +4 -4
@@ -4,7 +4,7 @@ import type { SqlDialectName } from './dialect.js';
4
4
  import type { FieldKey, HookEvent, RelationKey } from './entity.js';
5
5
  import type { LoggingOptions } from './logger.js';
6
6
  import type { NamingStrategy } from './namingStrategy.js';
7
- import type { QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryProjected, QuerySearch, QueryUpdateResult } from './query.js';
7
+ import type { QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult } from './query.js';
8
8
  import type { UniversalQuerier } from './universalQuerier.js';
9
9
  import type { Type } from './utility.js';
10
10
  /**
@@ -35,39 +35,46 @@ export interface Querier extends UniversalQuerier {
35
35
  /**
36
36
  * Find one record. Supports both entity-as-argument and entity-as-field patterns.
37
37
  */
38
- findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryOneProjected<E, S, V, X, P> & {
38
+ findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(q: QueryOneProjected<E, S, V, X, P, C> & {
39
39
  $entity: Type<E>;
40
- }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
41
- findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
40
+ }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
41
+ findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
42
42
  /**
43
43
  * Find many records. Supports both entity-as-argument and entity-as-field patterns.
44
44
  */
45
- findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P> & {
45
+ findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P, C> & {
46
46
  $entity: Type<E>;
47
- }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
48
- findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
47
+ }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
48
+ findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
49
49
  /**
50
50
  * Stream records as an async iterable. Supports both patterns.
51
51
  * Does not fill relations or fire lifecycle hooks.
52
52
  */
53
- findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P> & {
53
+ findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryStreamProjected<E, S, V, X, P> & {
54
54
  $entity: Type<E>;
55
55
  }, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
56
- findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
56
+ findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
57
57
  /**
58
58
  * Find many records and count. Supports both patterns.
59
59
  */
60
- findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P> & {
60
+ findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P, C> & {
61
61
  $entity: Type<E>;
62
- }, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
63
- findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
62
+ }, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
63
+ findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
64
64
  /**
65
65
  * Count records. Supports both patterns.
66
66
  */
67
- count<E extends object>(q: QueryFilter<E> & {
67
+ count<E extends object>(q: QueryPage<E> & {
68
68
  $entity: Type<E>;
69
69
  }, opts?: QueryOptions): Promise<number>;
70
- count<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
70
+ count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
71
+ /**
72
+ * Whether anything matches. Supports both patterns.
73
+ */
74
+ exists<E extends object>(q: QueryFilter<E> & {
75
+ $entity: Type<E>;
76
+ }, opts?: QueryOptions): Promise<boolean>;
77
+ exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
71
78
  /**
72
79
  * Delete many records (soft-deletes when the entity has a soft-delete field, else removes them).
73
80
  * Supports both entity-as-argument and entity-as-field patterns.
@@ -59,6 +59,20 @@ export type QueryExclude<E> = QuerySelect<E>;
59
59
  export type QueryPopulate<E> = {
60
60
  [K in RelationKey<E>]?: BooleanLike | QueryPopulateRelationOptions<E[K]>;
61
61
  };
62
+ /**
63
+ * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow
64
+ * which ones count. One statement per relation named here, batched over every parent at once, so it
65
+ * stays flat however many rows the read returned. Comes back under `_count`, which keeps it clear of
66
+ * a relation of the same name that `$populate` filled with rows.
67
+ */
68
+ /**
69
+ * The key a read carries its relation tallies under. One spelling for the type and the runtime that
70
+ * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.
71
+ */
72
+ export declare const COUNT_RESULT_KEY = "_count";
73
+ export type QueryCount<E> = {
74
+ [K in ToManyRelationKey<E>]?: BooleanLike | QueryFilter<RelationTarget<E[K]>>;
75
+ };
62
76
  /**
63
77
  * query conflict paths - subset of field keys used to detect upsert conflicts.
64
78
  */
@@ -139,8 +153,15 @@ type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
139
153
  * against an intersection is repeated per constituent, which made this the single most expensive
140
154
  * type in the package to check.
141
155
  */
156
+ /**
157
+ * Ordering parents by how many rows a to-many relation holds - "the ten users with the most posts".
158
+ * The tally is computed per parent as a correlated count, never by loading the rows.
159
+ */
160
+ export type QuerySortByCount = {
161
+ $count: QuerySortDirection;
162
+ };
142
163
  export type QuerySortMap<E, Vector extends boolean = true> = {
143
- [K in FieldKey<E> | JsonFieldPaths<E> | ToOneRelationKey<E>]?: K extends RelationKey<E> ? QuerySortMap<RelationTarget<E[K]>, false> : K extends FieldKey<E> ? Vector extends true ? NonNullable<E[K]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection : QuerySortDirection;
164
+ [K in FieldKey<E> | JsonFieldPaths<E> | RelationKey<E>]?: K extends RelationKey<E> ? IsMany<E[K]> extends true ? QuerySortByCount : QuerySortMap<RelationTarget<E[K]>, false> : K extends FieldKey<E> ? Vector extends true ? NonNullable<E[K]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection : QuerySortDirection;
144
165
  };
145
166
  /**
146
167
  * pager options.
@@ -156,8 +177,7 @@ export type QueryPager = {
156
177
  $limit?: number;
157
178
  };
158
179
  /**
159
- * Which rows a statement addresses. `count` takes exactly this: how many rows match is all a count
160
- * can answer, so an ordering or a page on it is a clause it could only drop or choke on.
180
+ * Which rows a statement addresses.
161
181
  */
162
182
  export type QueryFilter<E> = {
163
183
  /**
@@ -166,18 +186,22 @@ export type QueryFilter<E> = {
166
186
  $where?: QueryWhere<E>;
167
187
  };
168
188
  /**
169
- * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the rows they
170
- * picked with a SELECT before writing, so the page is portable rather than MySQL-only - and so a
189
+ * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never
190
+ * how many, so a count that accepted one would promise an influence it cannot have.
191
+ */
192
+ export type QueryPage<E> = QueryFilter<E> & QueryPager;
193
+ /**
194
+ * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the
195
+ * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a
171
196
  * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the
172
- * projection list to hold the distance. `$lock` is the clause that stays off these, declared on
173
- * {@link Query} instead.
197
+ * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.
174
198
  */
175
- export type QuerySearch<E> = QueryFilter<E> & {
199
+ export type QuerySearch<E> = QueryPage<E> & {
176
200
  /**
177
201
  * sorting options.
178
202
  */
179
203
  $sort?: QuerySortMap<E>;
180
- } & QueryPager;
204
+ };
181
205
  /**
182
206
  * query options.
183
207
  */
@@ -192,6 +216,10 @@ export type Query<E> = {
192
216
  * relation population options.
193
217
  */
194
218
  $populate?: QueryPopulate<E>;
219
+ /**
220
+ * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.
221
+ */
222
+ $count?: QueryCount<E>;
195
223
  /**
196
224
  * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.
197
225
  * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept
@@ -240,6 +268,12 @@ export type Query<E> = {
240
268
  * query accepts, so leaving it out is what excludes it from both.
241
269
  */
242
270
  export declare const QUERY_OBJECT_CLAUSES: readonly ["$select", "$populate", "$exclude", "$where", "$sort"];
271
+ /**
272
+ * Object clauses only the statement itself takes, never a relation's own query - the mirror of
273
+ * `$lock`, which neither takes. Counting a relation is batched over the rows a read returned, and a
274
+ * populated relation's rows are assembled after that, so there is nothing for a nested one to count.
275
+ */
276
+ export declare const QUERY_ROOT_OBJECT_CLAUSES: readonly ["$count"];
243
277
  export declare const QUERY_NUMBER_CLAUSES: readonly ["$skip", "$limit"];
244
278
  export declare const QUERY_BOOLEAN_CLAUSES: readonly ["$distinct"];
245
279
  /**
@@ -263,7 +297,18 @@ export type QueryUnique<E> = Pick<QueryOne<E>, '$select' | '$exclude' | '$popula
263
297
  * populated relation's own query - stays the concrete {@link Query} it is today.
264
298
  * @internal
265
299
  */
266
- type QueryProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = {
300
+ type QueryProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = QueryStreamProjection<E, S, V, X, P> & {
301
+ $count?: {
302
+ [K in C & keyof QueryCount<E>]?: QueryCount<E>[K];
303
+ };
304
+ };
305
+ /**
306
+ * {@link QueryProjection} without `$count`, which a stream cannot honor. Split out rather than
307
+ * subtracted afterwards: an optional key is a *known* key even when its value maps over `never`, so
308
+ * a statement that must not take the clause has to be built without it in the first place.
309
+ * @internal
310
+ */
311
+ type QueryStreamProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = {
267
312
  $select?: {
268
313
  [K in S]?: V;
269
314
  } | readonly QueryRaw[];
@@ -274,14 +319,19 @@ type QueryProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P exten
274
319
  [K in P]?: QueryPopulate<E>[K];
275
320
  };
276
321
  };
322
+ /**
323
+ * A {@link QueryProjected} a stream can honor: no `$count`, which is batched over a result set a
324
+ * stream never holds all of.
325
+ */
326
+ export type QueryStreamProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = Except<Query<E>, '$count'> & QueryStreamProjection<E, S, V, X, P>;
277
327
  /**
278
328
  * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.
279
329
  */
280
- export type QueryProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = Query<E> & QueryProjection<E, S, V, X, P>;
330
+ export type QueryProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never> = Query<E> & QueryProjection<E, S, V, X, P, C>;
281
331
  /**
282
332
  * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.
283
333
  */
284
- export type QueryOneProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>> = QueryOne<E> & QueryProjection<E, S, V, X, P>;
334
+ export type QueryOneProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never> = QueryOne<E> & QueryProjection<E, S, V, X, P, C>;
285
335
  /**
286
336
  * The keys a query comes back with, mirroring what the runtime projects: the fields a positive
287
337
  * `$select` names, or every field minus what a falsy `$select` entry or a truthy `$exclude` entry
@@ -289,7 +339,7 @@ export type QueryOneProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>
289
339
  * why `$exclude` is only read on the branch where there is none.
290
340
  * @internal
291
341
  */
292
- type ProjectedKeys<E, S, V, X, P> = ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S) | P | ([P] extends [never] ? never : NamedIdKey<E>);
342
+ type ProjectedKeys<E, S, V, X, P, C> = ([V] extends [false | 0] ? Exclude<FieldKey<E>, S> : [S] extends [never] ? Exclude<FieldKey<E>, X> : S) | P | ([P | C] extends [never] ? never : NamedIdKey<E>);
293
343
  /**
294
344
  * The id key when it can be named, and nothing when it cannot: {@link IdKey} widens to *every* field
295
345
  * for an entity whose id is neither branded nor called `id`/`_id`/`uuid`, and adding that back would
@@ -315,11 +365,22 @@ type IsUniform<V> = [V] extends [true | 1] ? true : [V] extends [false | 0] ? tr
315
365
  * keep their declared type: narrowing them means capturing their queries as maps, which costs those
316
366
  * queries their own checks.
317
367
  */
318
- export type QueryFindResult<E, S extends FieldKey<E> = never, V = true, X extends FieldKey<E> = never, P extends RelationKey<E> = never> = [S | X] extends [never] ? E : IsUniform<V> extends true ? [PopulatedToMany<E, P>] extends [never] ? {
319
- [K in keyof E as K extends ProjectedKeys<E, S, V, X, P> ? K : never]: E[K];
368
+ export type QueryFindResult<E, S extends FieldKey<E> = never, V = true, X extends FieldKey<E> = never, P extends RelationKey<E> = never, C extends RelationKey<E> = never> = QueryProjectedRow<E, S, V, X, P, C> & CountedRelations<C>;
369
+ /**
370
+ * The `_count` a query asked for, or an inert intersection member when it asked for none - so a read
371
+ * without `$count` keeps exactly the row type it had.
372
+ */
373
+ type CountedRelations<C extends PropertyKey> = [C] extends [never] ? unknown : {
374
+ [K in typeof COUNT_RESULT_KEY]: {
375
+ [R in C]: number;
376
+ };
377
+ };
378
+ /** @internal */
379
+ type QueryProjectedRow<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = [S | X] extends [never] ? E : IsUniform<V> extends true ? [PopulatedToMany<E, P>] extends [never] ? {
380
+ [K in keyof E as K extends ProjectedKeys<E, S, V, X, P, C> ? K : never]: E[K];
320
381
  } : // A populated to-many is always a list, empty where the parent has no children, so it maps
321
382
  {
322
- [K in keyof E as K extends Exclude<ProjectedKeys<E, S, V, X, P>, PopulatedToMany<E, P>> ? K : never]: E[K];
383
+ [K in keyof E as K extends Exclude<ProjectedKeys<E, S, V, X, P, C>, PopulatedToMany<E, P>> ? K : never]: E[K];
323
384
  } & {
324
385
  [K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;
325
386
  } : E;
@@ -1,3 +1,14 @@
1
+ /**
2
+ * How many rows each named relation holds per parent, `true` for all of them or a filter to narrow
3
+ * which ones count. One statement per relation named here, batched over every parent at once, so it
4
+ * stays flat however many rows the read returned. Comes back under `_count`, which keeps it clear of
5
+ * a relation of the same name that `$populate` filled with rows.
6
+ */
7
+ /**
8
+ * The key a read carries its relation tallies under. One spelling for the type and the runtime that
9
+ * fills it: they sit in different modules, so a drift would type-check and answer `undefined`.
10
+ */
11
+ export const COUNT_RESULT_KEY = '_count';
1
12
  /**
2
13
  * `Query`'s clauses grouped by the shape of their value - what a parser reading one off the wire and
3
14
  * a validator checking a relation's own query both need, and what each used to enumerate for itself.
@@ -14,5 +25,11 @@ export const QUERY_OBJECT_CLAUSES = [
14
25
  '$where',
15
26
  '$sort',
16
27
  ];
28
+ /**
29
+ * Object clauses only the statement itself takes, never a relation's own query - the mirror of
30
+ * `$lock`, which neither takes. Counting a relation is batched over the rows a read returned, and a
31
+ * populated relation's rows are assembled after that, so there is nothing for a nested one to count.
32
+ */
33
+ export const QUERY_ROOT_OBJECT_CLAUSES = ['$count'];
17
34
  export const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'];
18
35
  export const QUERY_BOOLEAN_CLAUSES = ['$distinct'];
@@ -1,11 +1,20 @@
1
1
  import type { EntityData, FieldKey, IdValue, RelationKey, UpdatePayload } from './entity.js';
2
- import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryProjected, QuerySearch, QueryUpdateResult } from './query.js';
2
+ import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateResult, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { Type } from './utility.js';
5
+ import type { QuerierCountedResult, QuerierResult, QuerierTransport } from './wire.js';
5
6
  /**
6
- * A `querier` allows to interact with the datasource to perform persistence operations on any entity.
7
+ * The operations {@link UniversalQuerier} and `ClientQuerier` declare identically, written once and
8
+ * instantiated per transport: `SharedQuerier<'server', QueryOptions>` against
9
+ * `SharedQuerier<'client', RequestOptions, QueryOptions & RequestOptions>`. See {@link QuerierResult}
10
+ * for how the return type follows `W`.
11
+ *
12
+ * @typeParam W - which side of the wire, picking each method's return type.
13
+ * @typeParam O - the per-call options.
14
+ * @typeParam DO - the delete methods' options, which the client also lets carry {@link QueryOptions}
15
+ * (`hardDelete`, `filters`) since it has no other way to reach them.
7
16
  */
8
- export interface UniversalQuerier {
17
+ export interface SharedQuerier<W extends QuerierTransport, O, DO = O> {
9
18
  /**
10
19
  * obtains the record with the given primary key.
11
20
  * @param entity the target entity
@@ -13,30 +22,21 @@ export interface UniversalQuerier {
13
22
  * @param q the additional criteria options
14
23
  * @return the record
15
24
  */
16
- findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
25
+ findOneById<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: O): QuerierResult<W, QueryFindResult<E, S, V, X, P, C> | undefined>;
17
26
  /**
18
27
  * obtains the first record matching the given search parameters.
19
28
  * @param entity the target entity
20
29
  * @param q the criteria options
21
30
  * @return the record
22
31
  */
23
- findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
32
+ findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryOneProjected<E, S, V, X, P, C>, opts?: O): QuerierResult<W, QueryFindResult<E, S, V, X, P, C> | undefined>;
24
33
  /**
25
34
  * obtains the records matching the given search parameters.
26
35
  * @param entity the target entity
27
36
  * @param q the criteria options
28
37
  * @return the records
29
38
  */
30
- findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
31
- /**
32
- * streams the records matching the given search parameters as an async iterable.
33
- * Does not fill relations or fire lifecycle hooks - designed for high-performance
34
- * bulk reads (ETL, exports, migrations).
35
- * @param entity the target entity
36
- * @param q the criteria options
37
- * @return an async iterable of records
38
- */
39
- findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
39
+ findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: O): QuerierResult<W, QueryFindResult<E, S, V, X, P, C>[]>;
40
40
  /**
41
41
  * obtains the records matching the given search parameters,
42
42
  * also counts the number of matches ignoring pagination.
@@ -44,14 +44,67 @@ export interface UniversalQuerier {
44
44
  * @param q the criteria options
45
45
  * @return the records and the count
46
46
  */
47
- findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
47
+ findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: O): QuerierCountedResult<W, QueryFindResult<E, S, V, X, P, C>>;
48
48
  /**
49
- * counts the number of records matching the given filter.
49
+ * counts the number of records matching the given filter, optionally paged - a `$skip`/`$limit`
50
+ * settles the matching rows first and counts them, rather than scanning every match.
50
51
  * @param entity the target entity
51
52
  * @param q the filter
52
53
  * @return the count
53
54
  */
54
- count<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
55
+ count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: O): QuerierResult<W, number>;
56
+ /**
57
+ * whether any record matches the given filter - a count capped at one row, so the engine stops at
58
+ * the first match instead of scanning every other one.
59
+ * @param entity the target entity
60
+ * @param q the filter
61
+ * @return whether anything matched
62
+ */
63
+ exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: O): QuerierResult<W, boolean>;
64
+ /**
65
+ * updates a record partially.
66
+ * @param entity the entity to persist on
67
+ * @param id the primary key of the record to be updated
68
+ * @param payload the data to be persisted
69
+ * @return the number of affected records
70
+ */
71
+ updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: O): QuerierResult<W, number>;
72
+ /**
73
+ * updates many records partially.
74
+ * @param entity the entity to persist on
75
+ * @param q the criteria to look for the records
76
+ * @param payload the data to be persisted
77
+ * @return the number of affected records
78
+ */
79
+ updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: O): QuerierResult<W, number>;
80
+ /**
81
+ * delete or SoftDelete a record.
82
+ * @param entity the entity to persist on
83
+ * @param id the primary key of the record
84
+ * @return the number of affected records
85
+ */
86
+ deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts?: DO): QuerierResult<W, number>;
87
+ /**
88
+ * delete or SoftDelete records.
89
+ * @param entity the entity to persist on
90
+ * @param q the criteria to look for the records
91
+ * @return the number of affected records
92
+ */
93
+ deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: DO): QuerierResult<W, number>;
94
+ }
95
+ /**
96
+ * A `querier` allows to interact with the datasource to perform persistence operations on any entity.
97
+ */
98
+ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions> {
99
+ /**
100
+ * streams the records matching the given search parameters as an async iterable.
101
+ * Does not fill relations or fire lifecycle hooks - designed for high-performance
102
+ * bulk reads (ETL, exports, migrations).
103
+ * @param entity the target entity
104
+ * @param q the criteria options
105
+ * @return an async iterable of records
106
+ */
107
+ findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
55
108
  /**
56
109
  * Insert a single record and return its ID (provided, `onInsert`-generated, or
57
110
  * database-generated - see {@link UniversalQuerier.insertMany} for the exact semantics).
@@ -78,22 +131,6 @@ export interface UniversalQuerier {
78
131
  * @return the IDs
79
132
  */
80
133
  insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
81
- /**
82
- * updates a record partially.
83
- * @param entity the entity to persist on
84
- * @param id the primary key of the record to be updated
85
- * @param payload the data to be persisted
86
- * @return the number of affected records
87
- */
88
- updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
89
- /**
90
- * updates many records partially.
91
- * @param entity the entity to persist on
92
- * @param q the criteria to look for the records
93
- * @param payload the data to be persisted
94
- * @return the number of affected records
95
- */
96
- updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
97
134
  /**
98
135
  * Insert or update a record based on the conflict paths.
99
136
  * @param entity the entity to persist on
@@ -124,20 +161,6 @@ export interface UniversalQuerier {
124
161
  * @return the IDs
125
162
  */
126
163
  saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
127
- /**
128
- * delete or SoftDelete a record.
129
- * @param entity the entity to persist on
130
- * @param id the primary key of the record
131
- * @return the number of affected records
132
- */
133
- deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts?: QueryOptions): Promise<number>;
134
- /**
135
- * delete or SoftDelete records.
136
- * @param entity the entity to persist on
137
- * @param q the criteria to look for the records
138
- * @return the number of affected records
139
- */
140
- deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
141
164
  /**
142
165
  * Restore soft-deleted records (sets the soft-delete field back to `null`). Throws if the
143
166
  * entity has no soft-delete field.
@@ -151,4 +174,18 @@ export interface UniversalQuerier {
151
174
  * @return the aggregate results
152
175
  */
153
176
  aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
177
+ /**
178
+ * How many rows the engine's own statistics say the table holds, without reading one: Postgres'
179
+ * `pg_class.reltuples`, CockroachDB's table statistics, MySQL/MariaDB's `information_schema`,
180
+ * MongoDB's `estimatedDocumentCount`. For a table too big to {@link count} cheaply.
181
+ *
182
+ * The whole table, and only ever approximately. It takes no filter because none of those sources
183
+ * can answer one, which also puts soft-deleted rows and every entity `filters` inside the number.
184
+ * It is as stale as the last `ANALYZE`/autovacuum (Postgres reports nothing at all until the
185
+ * first one, which reads here as `0`), and a table SQLite can answer for does not exist - it
186
+ * throws there rather than quietly running the scan this exists to avoid.
187
+ * @param entity the target entity
188
+ * @return the estimated number of rows
189
+ */
190
+ estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
154
191
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The envelope a wire response wraps its result in.
3
+ */
4
+ export type RequestSuccessResponse<E> = {
5
+ data: E;
6
+ count?: number;
7
+ };
8
+ /**
9
+ * A {@link RequestSuccessResponse} whose `count` is always present - what `findManyAndCount` sends.
10
+ */
11
+ export type RequestCountedSuccessResponse<E> = RequestSuccessResponse<E> & {
12
+ count: number;
13
+ };
14
+ /**
15
+ * Which side of the wire a querier sits on. A server querier hands the result back directly, a
16
+ * client one hands back the envelope its transport wrapped it in.
17
+ */
18
+ export type QuerierTransport = 'server' | 'client';
19
+ /**
20
+ * A querier method's result on the given transport, so one signature serves both:
21
+ * `QuerierResult<'server', User[]>` is `Promise<User[]>` and `QuerierResult<'client', User[]>` is
22
+ * `Promise<RequestSuccessResponse<User[]>>`. Indexing a map by the transport is what stands in for
23
+ * the higher-kinded wrapper TypeScript cannot express, and it resolves away: errors and hovers show
24
+ * the `Promise<User[]>` it picked, never this indirection.
25
+ */
26
+ export type QuerierResult<W extends QuerierTransport, T> = {
27
+ server: Promise<T>;
28
+ client: Promise<RequestSuccessResponse<T>>;
29
+ }[W];
30
+ /**
31
+ * `findManyAndCount`'s result: the one shape the transports disagree on past the envelope, a tuple
32
+ * on the server against a counted envelope on the client.
33
+ */
34
+ export type QuerierCountedResult<W extends QuerierTransport, T> = {
35
+ server: Promise<[T[], number]>;
36
+ client: Promise<RequestCountedSuccessResponse<T[]>>;
37
+ }[W];
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,14 @@
1
1
  import { type CascadeType, type EntityData, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
2
+ /**
3
+ * The alias an internally-built count answers under - a batched relation tally, a MongoDB relation
4
+ * lookup, a total taken past a `$required` unwind. Every one of those picks the alias and reads it
5
+ * back a few lines later, so they share the spelling rather than each repeating it twice.
6
+ *
7
+ * Prefixed like every other internal alias here: a batched tally selects it alongside the column it
8
+ * groups by, so a bare name would collide with a real column of that name and answer the grouped
9
+ * value twice over, with no statement failing to say so.
10
+ */
11
+ export declare const COUNT_AGG_ALIAS = "_uql_count";
2
12
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
3
13
  export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
4
14
  /**
@@ -108,6 +118,12 @@ export type ParsedGroupEntry = {
108
118
  * through to field filtering and emit a condition on a `$size` *column*.
109
119
  */
110
120
  export declare function parseRelationSize(val: unknown): number | QuerySizeComparisonOps | undefined;
121
+ /**
122
+ * The direction a `$sort` orders a to-many relation by its size, or `undefined` when the value is a
123
+ * map of the relation's own fields (which only a to-one can be ordered by). Mirrors
124
+ * {@link parseRelationSize}, the same clause spelled for a filter rather than an ordering.
125
+ */
126
+ export declare function parseSortByCount(val: unknown): unknown;
111
127
  /**
112
128
  * Parse the `$group` (grouped columns) and `$agg` (computed aggregates) maps into structured
113
129
  * entries consumable by any dialect. Grouped columns come first, then computed columns.
@@ -1,6 +1,16 @@
1
1
  import { getContext, UqlSecurityError } from '../context/context.js';
2
2
  import { QueryRaw, resolveAggregateOp, } from '../type/index.js';
3
3
  import { getFieldKeys, getKeys, hasKeys, someKey } from './object.util.js';
4
+ /**
5
+ * The alias an internally-built count answers under - a batched relation tally, a MongoDB relation
6
+ * lookup, a total taken past a `$required` unwind. Every one of those picks the alias and reads it
7
+ * back a few lines later, so they share the spelling rather than each repeating it twice.
8
+ *
9
+ * Prefixed like every other internal alias here: a batched tally selects it alongside the column it
10
+ * groups by, so a bare name would collide with a real column of that name and answer the grouped
11
+ * value twice over, with no statement failing to say so.
12
+ */
13
+ export const COUNT_AGG_ALIAS = '_uql_count';
4
14
  export function filterFieldKeys(meta, payload, callbackKey) {
5
15
  return getKeys(payload).filter((key) => {
6
16
  const fieldOpts = meta.fields[key];
@@ -290,6 +300,21 @@ export function parseRelationSize(val) {
290
300
  }
291
301
  return val.$size;
292
302
  }
303
+ /**
304
+ * The direction a `$sort` orders a to-many relation by its size, or `undefined` when the value is a
305
+ * map of the relation's own fields (which only a to-one can be ordered by). Mirrors
306
+ * {@link parseRelationSize}, the same clause spelled for a filter rather than an ordering.
307
+ */
308
+ export function parseSortByCount(val) {
309
+ if (!val || typeof val !== 'object' || !('$count' in val)) {
310
+ return undefined;
311
+ }
312
+ const siblings = getKeys(val).filter((key) => key !== '$count');
313
+ if (siblings.length) {
314
+ throw new TypeError(`$count in a $sort cannot be combined with other keys: ${siblings.join(', ')}`);
315
+ }
316
+ return val.$count;
317
+ }
293
318
  /**
294
319
  * Parse the `$group` (grouped columns) and `$agg` (computed aggregates) maps into structured
295
320
  * entries consumable by any dialect. Grouped columns come first, then computed columns.
@@ -9,6 +9,15 @@ export type RelationRequestSummary<E> = {
9
9
  * the one field it reads, so it answers for a relation being declared as well as for a resolved one.
10
10
  */
11
11
  export declare function isToManyRelation(relation: Pick<RelationMeta, 'cardinality'>): boolean;
12
+ /**
13
+ * The column holding the parent's id: a junction's own for a relation that goes through one, the
14
+ * child's foreign key otherwise. The two are spelled from opposite ends - `local` names a column of
15
+ * the table the relation is declared to write, `foreign` a column of the other one - and getting
16
+ * that backwards reads a real column of the wrong table, so it is answered once here.
17
+ */
18
+ export declare function parentKeyColumn(relOpts: Pick<RelationMeta, 'references' | 'through'>): string;
19
+ /** The column on a junction table holding the target's id, the other half of {@link parentKeyColumn}. */
20
+ export declare function targetKeyColumn(relOpts: Pick<RelationMeta, 'references'>): string;
12
21
  /**
13
22
  * What a joined relation cannot carry, and why. A to-many is loaded by a query of its own, which is
14
23
  * what gives these four a meaning there; a to-one is one row of the parent's, so every backend used
@@ -7,6 +7,19 @@ import { getKeys, someKey } from './object.util.js';
7
7
  export function isToManyRelation(relation) {
8
8
  return relation.cardinality === '1m' || relation.cardinality === 'mm';
9
9
  }
10
+ /**
11
+ * The column holding the parent's id: a junction's own for a relation that goes through one, the
12
+ * child's foreign key otherwise. The two are spelled from opposite ends - `local` names a column of
13
+ * the table the relation is declared to write, `foreign` a column of the other one - and getting
14
+ * that backwards reads a real column of the wrong table, so it is answered once here.
15
+ */
16
+ export function parentKeyColumn(relOpts) {
17
+ return relOpts.through ? relOpts.references[0].local : relOpts.references[0].foreign;
18
+ }
19
+ /** The column on a junction table holding the target's id, the other half of {@link parentKeyColumn}. */
20
+ export function targetKeyColumn(relOpts) {
21
+ return relOpts.references[1].local;
22
+ }
10
23
  /**
11
24
  * What a joined relation cannot carry, and why. A to-many is loaded by a query of its own, which is
12
25
  * what gives these four a meaning there; a to-one is one row of the parent's, so every backend used