uql-orm 0.31.1 → 0.31.3

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.
package/README.md CHANGED
@@ -70,7 +70,7 @@ Release notes live in [CHANGELOG.md](https://github.com/rogerpadilla/uql/blob/ma
70
70
 
71
71
  ## Made with UQL
72
72
 
73
- **[Variability.ai](https://variability.ai)** - AI meeting recorder and video summarizer for Zoom, Meet, and Teams. Instant summaries with action items in 35+ languages.
73
+ **[Variability.ai](https://variability.ai)** - AI meeting ntetaker and video summarizer for Zoom, Meet, Slack, and Teams. Instant summaries with action items in 35+ languages. Built by UQL's author.
74
74
 
75
75
  Built something? [Open a PR](https://github.com/rogerpadilla/uql/blob/main/CONTRIBUTING.md) and add it here.
76
76
 
@@ -30,6 +30,11 @@ export type UqlModuleAsyncOptions<Req = unknown> = UqlModuleCommon<Req> & {
30
30
  /** Providers to inject into `useFactory`. */
31
31
  readonly inject?: FactoryProvider['inject'];
32
32
  };
33
+ /**
34
+ * NestJS integration: provides the pool via DI, sets it as UQL's default pool (so `getQuerier()`,
35
+ * `querierMiddleware` (express platform) and `createFetchHandler` work unchanged), optionally scopes
36
+ * every request to a {@link UqlContext} (multi-tenancy), and ends the pool on application shutdown.
37
+ */
33
38
  export declare class UqlModule {
34
39
  /** Configure with an already-built pool. */
35
40
  static forRoot<Req = unknown>({ pool, global, getContext }: UqlModuleOptions<Req>): DynamicModule;
@@ -43,11 +43,6 @@ import { UqlContextInterceptor } from './uqlContextInterceptor.js';
43
43
  * does not redirect UQL internals.
44
44
  */
45
45
  export const UQL_QUERIER_POOL = Symbol('UQL_QUERIER_POOL');
46
- /**
47
- * NestJS integration: provides the pool via DI, sets it as UQL's default pool (so `getQuerier()`,
48
- * `querierMiddleware` (express platform) and `createFetchHandler` work unchanged), optionally scopes
49
- * every request to a {@link UqlContext} (multi-tenancy), and ends the pool on application shutdown.
50
- */
51
46
  /**
52
47
  * Ends the pool when Nest shuts down.
53
48
  *
@@ -66,6 +61,11 @@ class UqlPoolLifecycle {
66
61
  return this.pool.end();
67
62
  }
68
63
  }
64
+ /**
65
+ * NestJS integration: provides the pool via DI, sets it as UQL's default pool (so `getQuerier()`,
66
+ * `querierMiddleware` (express platform) and `createFetchHandler` work unchanged), optionally scopes
67
+ * every request to a {@link UqlContext} (multi-tenancy), and ends the pool on application shutdown.
68
+ */
69
69
  let UqlModule = (() => {
70
70
  let _classDecorators = [Module({})];
71
71
  let _classDescriptor;
@@ -23,11 +23,13 @@ export declare abstract class AbstractQuerier implements Querier {
23
23
  private validateProjectionQueryRecursive;
24
24
  /**
25
25
  * Resolves `[entity, query, opts]` for the dual call pattern: `(entity, q, opts)` (entity argument)
26
- * vs `(query, opts)` (entity via the query's `$entity` field).
26
+ * vs `(query, opts)` (entity via the query's `$entity` field). Generic in the query `Q` because it
27
+ * only ever reads `$entity`: pinning it to one statement's shape made every caller launder its own
28
+ * through a cast, which is how a read query's `$sort` used to reach a write's.
27
29
  */
28
- protected resolveEntityQuery<E extends object>(entityOrQuery: Type<E> | (QuerySearch<E> & {
30
+ protected resolveEntityQuery<E extends object, Q extends object>(entityOrQuery: Type<E> | (Q & {
29
31
  $entity: Type<E>;
30
- }), maybeQueryOrOpts?: QuerySearch<E> | QueryOptions, maybeOpts?: QueryOptions): [Type<E>, QuerySearch<E>, QueryOptions | undefined];
32
+ }), maybeQueryOrOpts?: Q | QueryOptions, maybeOpts?: QueryOptions): [Type<E>, Q, QueryOptions | undefined];
31
33
  findOneById<E extends object>(entity: Type<E>, id: IdValue<E>, q?: QueryOne<E>, opts?: QueryOptions): Promise<E | undefined>;
32
34
  /**
33
35
  * Find a single record matching the query.
@@ -65,7 +65,9 @@ export class AbstractQuerier {
65
65
  }
66
66
  /**
67
67
  * Resolves `[entity, query, opts]` for the dual call pattern: `(entity, q, opts)` (entity argument)
68
- * vs `(query, opts)` (entity via the query's `$entity` field).
68
+ * vs `(query, opts)` (entity via the query's `$entity` field). Generic in the query `Q` because it
69
+ * only ever reads `$entity`: pinning it to one statement's shape made every caller launder its own
70
+ * through a cast, which is how a read query's `$sort` used to reach a write's.
69
71
  */
70
72
  resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
71
73
  if (typeof entityOrQuery === 'function' && entityOrQuery.prototype) {
@@ -13,17 +13,21 @@ export declare const idKey: unique symbol;
13
13
  export type Key<E> = keyof E & string;
14
14
  /**
15
15
  * Infers the field names of an entity.
16
- * Includes scalar fields, JSON fields, and scalar arrays (e.g. vector `number[]`).
16
+ * Includes scalar fields, JSON fields, scalar arrays (e.g. vector `number[]`) and arrays of JSON.
17
17
  * The `-?` modifier strips optionality so the indexed access yields clean key unions
18
18
  * (without it, optional properties leak `undefined` into the union).
19
19
  *
20
+ * `readonly Json[]` is its own arm because the brand sits on the element, so the `Json` arm cannot
21
+ * see it. What keeps a to-many relation out of that arm is the weak-type check: `Json<unknown>` is
22
+ * all-optional, which a class with named properties is not assignable to.
23
+ *
20
24
  * The check is bracketed so `any` resolves once rather than matching both this and
21
25
  * {@link RelationKey}: an unbracketed `any extends X` satisfies either branch. It reads
22
26
  * `readonly Scalar[]`, which every mutable one satisfies too, so declaring a vector or a scalar
23
27
  * array `readonly` does not push the field over into {@link RelationKey}.
24
28
  */
25
29
  export type FieldKey<E> = {
26
- readonly [K in keyof E]-?: [NonNullable<E[K]>] extends [Scalar | readonly Scalar[] | Json] ? K : never;
30
+ readonly [K in keyof E]-?: [NonNullable<E[K]>] extends [Scalar | readonly Scalar[] | Json | readonly Json[]] ? K : never;
27
31
  }[Key<E>];
28
32
  /**
29
33
  * Infers the relation names of an entity: whatever is left once its fields and its methods are
@@ -41,10 +45,21 @@ type IsJson<T> = '__json' extends keyof T ? true : false;
41
45
  /** The payload `P` of a branded `Json<P>`, or `never` for any non-JSON type. */
42
46
  type UnwrapJson<T> = IsJson<T> extends true ? (T extends Json<infer P> ? P : never) : never;
43
47
  /**
44
- * The `Json` payload of a field value `V`: both `Json<T>` and `Json<T>[]` yield `T` (via
45
- * `Unpacked`, a no-op for the non-array case); `never` when `V` is not a JSON field.
48
+ * The one branded value a field value `V` holds: `Json<T>` for both `Json<T>` and `Json<T>[]`, via
49
+ * `Unpacked`, a no-op for the non-array case.
50
+ */
51
+ type JsonElement<V> = NonNullable<Unpacked<NonNullable<V>>>;
52
+ /** The `Json` payload of a field value `V`; `never` when `V` is not a JSON field. */
53
+ type JsonPayload<V> = UnwrapJson<JsonElement<V>>;
54
+ /**
55
+ * The fields carrying the `Json` brand, `never` on an entity with none - which is most of them, and
56
+ * what makes {@link JsonFieldPaths} collapse to `never` without deriving a path for anything. Tests
57
+ * the brand rather than the payload, whose extra `Json<infer P>` inference is only worth doing once
58
+ * a field is known to be JSON.
46
59
  */
47
- type JsonPayload<V> = UnwrapJson<NonNullable<Unpacked<NonNullable<V>>>>;
60
+ type JsonFieldKey<E> = {
61
+ readonly [K in keyof E]-?: IsJson<JsonElement<E[K]>> extends true ? K : never;
62
+ }[Key<E>];
48
63
  /**
49
64
  * Recursively derives dot-notation key paths from a JSON payload type. Handles every shape at
50
65
  * entry: an untyped (`unknown`) payload accepts any suffix via a `string` pattern, scalars are
@@ -57,15 +72,15 @@ type DeepJsonKeys<T, D extends unknown[] = []> = unknown extends T ? string : No
57
72
  }[keyof NonNullable<T> & string];
58
73
  /**
59
74
  * Extracts dot-notation paths from `Json<T>` values, handling both scalar JSON
60
- * and arrays of JSON (e.g., `Json<{foo: string}>[]` in MongoDB).
75
+ * and arrays of JSON (`Json<{foo: string}>[]`, a column holding a list of documents).
61
76
  * For `kind?: Json<{ public: number; theme: { color: string } }>`,
62
77
  * produces `'kind.public' | 'kind.theme' | 'kind.theme.color'`.
63
78
  * For `items?: Json<{id: string}>[]`, produces `'items.id'`.
64
79
  * An untyped `Json<unknown>` field yields the scoped pattern `` `${K}.${string}` ``.
65
80
  */
66
81
  export type JsonFieldPaths<E> = {
67
- readonly [K in FieldKey<E>]: [JsonPayload<E[K]>] extends [never] ? never : `${K & string}.${Exclude<DeepJsonKeys<JsonPayload<E[K]>>, '__json'>}`;
68
- }[FieldKey<E>];
82
+ readonly [K in JsonFieldKey<E>]: `${K & string}.${DeepJsonKeys<JsonPayload<E[K]>>}`;
83
+ }[JsonFieldKey<E>];
69
84
  /**
70
85
  * The value type inside `T` at dot-path `P`; `unknown` when unresolvable (e.g. through a
71
86
  * `Record<string, unknown>` leaf). Arrays are stepped into via their element type.
@@ -73,9 +88,10 @@ export type JsonFieldPaths<E> = {
73
88
  type PathValue<T, P extends string> = unknown extends T ? unknown : NonNullable<T> extends readonly (infer U)[] ? PathValue<NonNullable<U>, P> : P extends `${infer K}.${infer Rest}` ? K extends keyof NonNullable<T> ? PathValue<NonNullable<T>[K], Rest> : unknown : P extends keyof NonNullable<T> ? NonNullable<T>[P] : unknown;
74
89
  /**
75
90
  * The value type at a JSON dot-path `P` of entity `E`; `unknown` when unresolvable, which keeps
76
- * untyped paths fully permissive in `$where`.
91
+ * untyped paths fully permissive in `$where`. Gated on {@link JsonFieldKey}, the same predicate
92
+ * {@link JsonFieldPaths} derives its keys from, so a path that is offered always resolves a value.
77
93
  */
78
- export type JsonFieldPathValue<E, P extends string> = P extends `${infer F}.${infer Rest}` ? F extends FieldKey<E> ? [JsonPayload<E[F]>] extends [never] ? unknown : PathValue<JsonPayload<E[F]>, Rest> : unknown : unknown;
94
+ export type JsonFieldPathValue<E, P extends string> = P extends `${infer F}.${infer Rest}` ? F extends JsonFieldKey<E> ? PathValue<JsonPayload<E[F]>, Rest> : unknown : unknown;
79
95
  /**
80
96
  * Extracts only the array-typed keys from `T`, mapping each to its element type via `Unpacked`.
81
97
  * Used by `$push` and `$pull` to provide type-safe element targets.
@@ -36,7 +36,9 @@ export type QuerySelectOptions = {
36
36
  autoPrefixAlias?: boolean;
37
37
  };
38
38
  /**
39
- * Query field selection - `{ name: true }` whitelists specific fields.
39
+ * Query field selection - `{ name: true }` whitelists specific fields. Fields only: a relation is a
40
+ * sub-query rather than a projection flag, and a whitelist naming one could not say whether the
41
+ * scalars come with it. Relations go in `$populate`.
40
42
  */
41
43
  export type QuerySelect<E> = {
42
44
  [K in FieldKey<E>]?: BooleanLike;
@@ -116,17 +118,27 @@ export type QuerySortDirection = -1 | 1 | 'asc' | 'desc';
116
118
  * Accepted value for a field in `$sort` - either a direction or a vector similarity search.
117
119
  */
118
120
  export type QuerySortValue = QuerySortDirection | QueryVectorSearch;
121
+ /**
122
+ * To-one relations only: a parent holds many rows of a to-many, so there is no single value to order
123
+ * it by, and joining one in would duplicate the parent instead. Order those inside `$populate`.
124
+ */
125
+ type ToOneRelationKey<E> = {
126
+ [K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K;
127
+ }[RelationKey<E>];
119
128
  /**
120
129
  * sort by map - supports field keys, JSON dot-notation paths (restricted to real JSON fields,
121
130
  * like `QueryWhereMap`), relation sort via nested objects, and vector similarity search on
122
- * `number[]` fields.
131
+ * `number[]` fields. `Vector` is what confines a vector search to the level the statement ranks:
132
+ * the queried entity. A relation of it is joined in one row at a time, so there is nothing to rank
133
+ * there - the SQL dialects throw, and MongoDB would quietly drop it, so this is its only guard.
134
+ *
135
+ * One mapped type over the three key sets rather than three intersected. The sets are disjoint - a
136
+ * JSON path is dotted, and a field key cannot also be a relation key - and an assignability check
137
+ * against an intersection is repeated per constituent, which made this the single most expensive
138
+ * type in the package to check.
123
139
  */
124
140
  export type QuerySortMap<E, Vector extends boolean = true> = {
125
- [K in FieldKey<E>]?: Vector extends true ? NonNullable<E[K]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection;
126
- } & {
127
- [P in JsonFieldPaths<E>]?: QuerySortDirection;
128
- } & {
129
- [K in RelationKey<E> as IsMany<E[K]> extends true ? never : K]?: QuerySortMap<RelationTarget<E[K]>, false>;
141
+ [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;
130
142
  };
131
143
  /**
132
144
  * pager options.
@@ -153,17 +165,16 @@ export type QueryFilter<E> = {
153
165
  };
154
166
  /**
155
167
  * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the rows they
156
- * picked before writing, so the page is portable rather than MySQL-only.
157
- *
158
- * `$sort` excludes vector search here for the reason `$lock` is declared on {@link Query} instead:
159
- * a vector search ranks rows into a projected distance column, and only a SELECT has a projection
160
- * list to hold one. Passing `QuerySortMap` its `Vector = false` is what keeps it off these.
168
+ * picked with a SELECT before writing, so the page is portable rather than MySQL-only - and so a
169
+ * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the
170
+ * projection list to hold the distance. `$lock` is the clause that stays off these, declared on
171
+ * {@link Query} instead.
161
172
  */
162
173
  export type QuerySearch<E> = QueryFilter<E> & {
163
174
  /**
164
175
  * sorting options.
165
176
  */
166
- $sort?: QuerySortMap<E, false>;
177
+ $sort?: QuerySortMap<E>;
167
178
  } & QueryPager;
168
179
  /**
169
180
  * query options.
@@ -243,3 +254,4 @@ export type QueryUpdateResult = {
243
254
  */
244
255
  created?: boolean;
245
256
  };
257
+ export {};
@@ -25,7 +25,8 @@ export type PrimaryKey = string | number | bigint;
25
25
  /**
26
26
  * Marker type for JSON/JSONB fields.
27
27
  * Wrapping a field's TypeScript type with `Json<T>` ensures it is classified as a `FieldKey`
28
- * (not a `RelationKey`), enabling type-safe usage in `$where`, `$select`, and `$sort`.
28
+ * (not a `RelationKey`), enabling type-safe usage in `$where`, `$select`, and `$sort`. A column
29
+ * holding a list of documents is `Json<T>[]`, also a field, whose dot-paths address the element.
29
30
  *
30
31
  * @example
31
32
  * ```ts
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "JSON-native TypeScript ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.31.1",
6
+ "version": "0.31.3",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -129,13 +129,13 @@
129
129
  }
130
130
  },
131
131
  "devDependencies": {
132
- "@electric-sql/pglite": "0.5.7",
133
- "@electric-sql/pglite-pgvector": "0.0.8",
132
+ "@electric-sql/pglite": "0.5.8",
133
+ "@electric-sql/pglite-pgvector": "0.0.9",
134
134
  "@libsql/client": "^0.17.4",
135
135
  "@neondatabase/serverless": "^1.1.0",
136
- "@nestjs/common": "^11.2.1",
137
- "@nestjs/core": "^11.2.1",
138
- "@nestjs/testing": "^11.2.1",
136
+ "@nestjs/common": "^12.0.1",
137
+ "@nestjs/core": "^12.0.1",
138
+ "@nestjs/testing": "^12.0.1",
139
139
  "@tursodatabase/database": "^0.7.2",
140
140
  "@tursodatabase/serverless": "^1.4.0",
141
141
  "@types/better-sqlite3": "^9.6.0",
@@ -145,8 +145,8 @@
145
145
  "better-sqlite3": "^13.0.3",
146
146
  "express": "^5.2.1",
147
147
  "mariadb": "^3.5.3",
148
- "mongodb": "^7.5.0",
149
- "mysql2": "^3.23.4",
148
+ "mongodb": "^7.6.0",
149
+ "mysql2": "^3.24.2",
150
150
  "pg": "^8.23.0",
151
151
  "pg-query-stream": "^4.17.0",
152
152
  "rxjs": "^7.8.2",