uql-orm 0.28.2 → 0.30.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/querier/httpQuerier.d.ts +5 -5
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +3 -3
  4. package/dist/dialect/abstractSqlDialect.d.ts +6 -4
  5. package/dist/dialect/abstractSqlDialect.js +29 -29
  6. package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -1
  7. package/dist/dialect/mysqlLikeSqlDialect.js +9 -0
  8. package/dist/dialect/vectorSqlDialect.js +8 -1
  9. package/dist/mongo/mongoDialect.d.ts +3 -3
  10. package/dist/mongo/mongoDialect.js +17 -9
  11. package/dist/mongo/mongodbQuerier.d.ts +9 -4
  12. package/dist/mongo/mongodbQuerier.js +15 -7
  13. package/dist/mysql/mysql2Querier.d.ts +1 -1
  14. package/dist/mysql/mysql2Querier.js +7 -1
  15. package/dist/nestjs/uqlContextInterceptor.d.ts +2 -2
  16. package/dist/nestjs/uqlContextInterceptor.js +2 -2
  17. package/dist/nestjs/uqlModule.d.ts +3 -3
  18. package/dist/nestjs/uqlModule.js +5 -6
  19. package/dist/postgres/abstractPgQuerier.d.ts +3 -2
  20. package/dist/postgres/abstractPgQuerier.js +2 -2
  21. package/dist/querier/abstractPoolQuerier.d.ts +2 -2
  22. package/dist/querier/abstractPoolQuerier.js +7 -7
  23. package/dist/querier/abstractQuerier.d.ts +45 -16
  24. package/dist/querier/abstractQuerier.js +99 -40
  25. package/dist/querier/abstractQuerierPool.d.ts +7 -7
  26. package/dist/querier/abstractSqlQuerier.d.ts +17 -6
  27. package/dist/querier/abstractSqlQuerier.js +61 -34
  28. package/dist/querier/index.d.ts +0 -2
  29. package/dist/querier/index.js +0 -2
  30. package/dist/sqlite/abstractSqliteQuerier.d.ts +1 -2
  31. package/dist/sqlite/abstractSqliteQuerier.js +2 -8
  32. package/dist/sqlite/hranaQuerier.d.ts +5 -0
  33. package/dist/sqlite/hranaQuerier.js +10 -6
  34. package/dist/type/entity.d.ts +34 -11
  35. package/dist/type/querier.d.ts +26 -10
  36. package/dist/type/query.d.ts +22 -9
  37. package/dist/type/queryAggregate.d.ts +67 -24
  38. package/dist/type/queryWhere.d.ts +4 -1
  39. package/dist/type/universalQuerier.d.ts +11 -11
  40. package/dist/util/dialect.util.d.ts +26 -4
  41. package/dist/util/dialect.util.js +42 -0
  42. package/package.json +4 -5
  43. package/dist/querier/querierContext.browser.d.ts +0 -12
  44. package/dist/querier/querierContext.browser.js +0 -18
  45. package/dist/querier/querierContext.d.ts +0 -22
  46. package/dist/querier/querierContext.js +0 -42
  47. package/dist/querier/transactional.d.ts +0 -26
  48. package/dist/querier/transactional.js +0 -43
@@ -257,8 +257,11 @@ export type QueryWhereFieldOperators<T> = unknown extends T ? QueryWhereFieldOpe
257
257
  * Value for a field comparison. A bare array is an implicit `$in` for scalar fields only:
258
258
  * on array-typed fields (e.g. a vector `number[]`) an array of arrays is ambiguous, so
259
259
  * membership there requires an explicit operator.
260
+ *
261
+ * `null` is accepted on a nullable field (an optional property is a nullable column), matching what
262
+ * `$eq: null` already took.
260
263
  */
261
- export type QueryWhereFieldValue<T> = T | ([NonNullable<T>] extends [readonly unknown[]] ? never : T[]) | QueryWhereFieldOperators<T> | QueryRaw;
264
+ export type QueryWhereFieldValue<T> = T | (undefined extends T ? null : never) | ([NonNullable<T>] extends [readonly unknown[]] ? never : T[]) | QueryWhereFieldOperators<T> | QueryRaw;
262
265
  /**
263
266
  * query filter array - used for `$and`, `$or`, `$not`, `$nor` operators.
264
267
  */
@@ -1,5 +1,5 @@
1
- import type { IdValue, UpdatePayload } from './entity.js';
2
- import type { Query, QueryConflictPaths, QueryOne, QueryOptions, QuerySearch, QueryUpdateResult } from './query.js';
1
+ import type { EntityData, IdValue, UpdatePayload } from './entity.js';
2
+ import type { Query, QueryConflictPaths, QueryFilter, QueryOne, QueryOptions, QuerySearch, QueryUpdateResult } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateResult, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { Type } from './utility.js';
5
5
  /**
@@ -46,12 +46,12 @@ export interface UniversalQuerier {
46
46
  */
47
47
  findManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
48
48
  /**
49
- * counts the number of records matching the given search parameters.
49
+ * counts the number of records matching the given filter.
50
50
  * @param entity the target entity
51
- * @param q the search options
51
+ * @param q the filter
52
52
  * @return the count
53
53
  */
54
- count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
54
+ count<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
55
55
  /**
56
56
  * Insert a single record and return its ID (provided, `onInsert`-generated, or
57
57
  * database-generated - see {@link UniversalQuerier.insertMany} for the exact semantics).
@@ -61,7 +61,7 @@ export interface UniversalQuerier {
61
61
  * @param payload the data to be persisted
62
62
  * @return the ID
63
63
  */
64
- insertOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E> | undefined>;
64
+ insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
65
65
  /**
66
66
  * Insert multiple records in a single statement (auto-chunked when the batch exceeds the
67
67
  * dialect's bind-parameter limit) and return their IDs in payload order.
@@ -75,7 +75,7 @@ export interface UniversalQuerier {
75
75
  * @param payload the data to be persisted
76
76
  * @return the IDs
77
77
  */
78
- insertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
78
+ insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
79
79
  /**
80
80
  * updates a record partially.
81
81
  * @param entity the entity to persist on
@@ -99,7 +99,7 @@ export interface UniversalQuerier {
99
99
  * @param payload the data to be persisted
100
100
  * @return operation metadata; see {@link QueryUpdateResult}
101
101
  */
102
- upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E): Promise<QueryUpdateResult>;
102
+ upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
103
103
  /**
104
104
  * Insert or update many records based on the conflict paths.
105
105
  * @param entity the entity to persist on
@@ -107,21 +107,21 @@ export interface UniversalQuerier {
107
107
  * @param payload the data to be persisted
108
108
  * @return operation metadata; see {@link QueryUpdateResult}
109
109
  */
110
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E[]): Promise<QueryUpdateResult>;
110
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
111
111
  /**
112
112
  * insert or update a record.
113
113
  * @param entity the entity to persist on
114
114
  * @param payload the data to be persisted
115
115
  * @return the ID
116
116
  */
117
- saveOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E>>;
117
+ saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E>>;
118
118
  /**
119
119
  * Insert or update records.
120
120
  * @param entity the entity to persist on
121
121
  * @param payload the data to be persisted
122
122
  * @return the IDs
123
123
  */
124
- saveMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
124
+ saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
125
125
  /**
126
126
  * delete or SoftDelete a record.
127
127
  * @param entity the entity to persist on
@@ -1,6 +1,6 @@
1
- import { type CascadeType, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
1
+ import { type CascadeType, type EntityData, type EntityMeta, type FieldKey, type FieldOptions, type JsonUpdateOp, type OnFieldCallback, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
2
2
  export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
3
- export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: E, callbackKey: CallbackKey): FieldKey<E>[];
3
+ export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
4
4
  /**
5
5
  * Resolves the columns of an INSERT statement: the union of the persistable fields provided by
6
6
  * any record (in first-seen order), plus every `onInsert` field. Records missing one of these
@@ -13,7 +13,7 @@ export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: E, call
13
13
  * has run {@link fillOnFields} first (it stamps them on every record, but the querier's
14
14
  * chunk-size estimate inspects the raw payload).
15
15
  */
16
- export declare function getInsertFieldKeys<E>(meta: EntityMeta<E>, payloads: E[]): FieldKey<E>[];
16
+ export declare function getInsertFieldKeys<E>(meta: EntityMeta<E>, payloads: EntityData<E>[]): FieldKey<E>[];
17
17
  export declare function getFieldCallbackValue(val: OnFieldCallback): string | number | bigint | boolean | Date | QueryRaw | readonly {
18
18
  readonly __json?: never;
19
19
  }[] | readonly number[] | Uint8Array<ArrayBufferLike> | {
@@ -28,7 +28,7 @@ export declare function getSoftDeleteValue(field: FieldOptions): string | number
28
28
  }[] | readonly number[] | Uint8Array<ArrayBufferLike> | {
29
29
  readonly __json?: never;
30
30
  };
31
- export declare function fillOnFields<E>(meta: EntityMeta<E>, payload: E | E[], callbackKey: CallbackKey): E[];
31
+ export declare function fillOnFields<E>(meta: EntityMeta<E>, payload: EntityData<E> | EntityData<E>[], callbackKey: CallbackKey): EntityData<E>[];
32
32
  /**
33
33
  * The relation keys present in `payload` whose cascade configuration allows `action`. Only
34
34
  * `payload`'s keys are read, so any keys-bearing object works (an entity, an update payload,
@@ -100,3 +100,25 @@ export declare function parseRelationSize(val: unknown): number | QuerySizeCompa
100
100
  * entries consumable by any dialect. Grouped columns come first, then computed columns.
101
101
  */
102
102
  export declare function parseGroupMap<E>(group?: QueryGroupMap<E>, agg?: QueryAggMap<E>): ParsedGroupEntry[];
103
+ /**
104
+ * Whether `value` is a map of comparison operators rather than a value to compare against. Only a
105
+ * plain object qualifies: `Date`, `QueryRaw`, `Uint8Array` and arrays are all `typeof 'object'`, and
106
+ * reading their keys as operators drops the condition (a `Date` has none) or throws on an array's
107
+ * indices. Shared by the SQL and MongoDB builders, whose `$where` and `$having` all face this.
108
+ */
109
+ export declare function isOperatorMap(value: unknown): value is Record<string, unknown>;
110
+ /**
111
+ * A page operand, checked before it reaches an engine. These arrive from page arithmetic and from
112
+ * REST query strings (`parseQueryParams` yields `NaN` for a non-numeric one), and each engine's own
113
+ * complaint names neither the clause nor the value - or, for `$limit: 0`, quietly means something
114
+ * else. Shared so every backend rejects the same input.
115
+ */
116
+ export declare function assertNonNegativeInteger(value: number, clause: string): number;
117
+ /**
118
+ * Rejects a `$having`/`$sort` key naming something an aggregate does not emit. Its rows are its
119
+ * `$group` columns and its `$agg` aliases; anything else is a value that is not there. Shared so
120
+ * SQL and MongoDB refuse the same query with the same words.
121
+ */
122
+ export declare function throwUnknownAggregateColumn(key: string, clause: string): never;
123
+ /** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
124
+ export declare function assertAggregateColumns(clauseMap: object | undefined, emitted: ReadonlySet<string>, clause: string): void;
@@ -301,3 +301,45 @@ export function parseGroupMap(group, agg) {
301
301
  }
302
302
  return entries;
303
303
  }
304
+ /**
305
+ * Whether `value` is a map of comparison operators rather than a value to compare against. Only a
306
+ * plain object qualifies: `Date`, `QueryRaw`, `Uint8Array` and arrays are all `typeof 'object'`, and
307
+ * reading their keys as operators drops the condition (a `Date` has none) or throws on an array's
308
+ * indices. Shared by the SQL and MongoDB builders, whose `$where` and `$having` all face this.
309
+ */
310
+ export function isOperatorMap(value) {
311
+ return (typeof value === 'object' &&
312
+ value !== null &&
313
+ !Array.isArray(value) &&
314
+ !(value instanceof Date) &&
315
+ !(value instanceof Uint8Array) &&
316
+ !(value instanceof QueryRaw));
317
+ }
318
+ /**
319
+ * A page operand, checked before it reaches an engine. These arrive from page arithmetic and from
320
+ * REST query strings (`parseQueryParams` yields `NaN` for a non-numeric one), and each engine's own
321
+ * complaint names neither the clause nor the value - or, for `$limit: 0`, quietly means something
322
+ * else. Shared so every backend rejects the same input.
323
+ */
324
+ export function assertNonNegativeInteger(value, clause) {
325
+ if (!Number.isInteger(value) || value < 0) {
326
+ throw new TypeError(`${clause} must be a non-negative integer, got ${value}`);
327
+ }
328
+ return value;
329
+ }
330
+ /**
331
+ * Rejects a `$having`/`$sort` key naming something an aggregate does not emit. Its rows are its
332
+ * `$group` columns and its `$agg` aliases; anything else is a value that is not there. Shared so
333
+ * SQL and MongoDB refuse the same query with the same words.
334
+ */
335
+ export function throwUnknownAggregateColumn(key, clause) {
336
+ throw new TypeError(`cannot ${clause} by '${key}': it is neither a $group column nor an $agg alias`);
337
+ }
338
+ /** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
339
+ export function assertAggregateColumns(clauseMap, emitted, clause) {
340
+ for (const key of getKeys(clauseMap ?? {})) {
341
+ if (!emitted.has(key)) {
342
+ throwUnknownAggregateColumn(key, clause);
343
+ }
344
+ }
345
+ }
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, 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.28.2",
6
+ "version": "0.30.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"
@@ -12,8 +12,7 @@
12
12
  "main": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts",
14
14
  "browser": {
15
- "./dist/context/context.js": "./dist/context/context.browser.js",
16
- "./dist/querier/querierContext.js": "./dist/querier/querierContext.browser.js"
15
+ "./dist/context/context.js": "./dist/context/context.browser.js"
17
16
  },
18
17
  "bin": {
19
18
  "uql-migrate": "./dist/migrate/bin.js"
@@ -134,13 +133,13 @@
134
133
  "@tursodatabase/serverless": "^1.4.0",
135
134
  "@types/better-sqlite3": "^9.6.0",
136
135
  "@types/express": "^5.0.6",
137
- "@types/pg": "^8.21.0",
136
+ "@types/pg": "^8.23.1",
138
137
  "@types/ws": "^8.18.1",
139
138
  "better-sqlite3": "^13.0.3",
140
139
  "express": "^5.2.1",
141
140
  "mariadb": "^3.5.3",
142
141
  "mongodb": "^7.5.0",
143
- "mysql2": "^3.23.3",
142
+ "mysql2": "^3.23.4",
144
143
  "pg": "^8.23.0",
145
144
  "pg-query-stream": "^4.17.0",
146
145
  "rxjs": "^7.8.2",
@@ -1,12 +0,0 @@
1
- import type { Querier } from '../type/index.js';
2
- /**
3
- * Browser build of the transactional querier context: bundlers targeting the browser resolve
4
- * `querierContext.ts` to this file (see the `browser` map in package.json), keeping the root entrypoint
5
- * free of `node:async_hooks`.
6
- *
7
- * Nothing in a browser bundle opens a transaction. The browser querier serializes queries over HTTP and
8
- * the server owns the transaction, so there is no ambient querier to hand out and no flow to track.
9
- */
10
- export declare function withQuerierContext<T>(_querier: Querier, callback: () => T): T;
11
- export declare function currentQuerier(): Querier;
12
- export declare function currentQuerierIfAny(): Querier | undefined;
@@ -1,18 +0,0 @@
1
- /**
2
- * Browser build of the transactional querier context: bundlers targeting the browser resolve
3
- * `querierContext.ts` to this file (see the `browser` map in package.json), keeping the root entrypoint
4
- * free of `node:async_hooks`.
5
- *
6
- * Nothing in a browser bundle opens a transaction. The browser querier serializes queries over HTTP and
7
- * the server owns the transaction, so there is no ambient querier to hand out and no flow to track.
8
- */
9
- export function withQuerierContext(_querier, callback) {
10
- return callback();
11
- }
12
- export function currentQuerier() {
13
- throw new TypeError('currentQuerier() is server-only: transactions run on the server, and the browser querier sends each ' +
14
- 'request over HTTP. Call it from server code, or use the querier you already have.');
15
- }
16
- export function currentQuerierIfAny() {
17
- return undefined;
18
- }
@@ -1,22 +0,0 @@
1
- import type { Querier } from '../type/index.js';
2
- /** Runs `callback` with `querier` as the ambient one, for the whole async flow beneath it. */
3
- export declare function withQuerierContext<T>(querier: Querier, callback: () => T): T;
4
- /**
5
- * The querier of the enclosing `@Transactional()` method.
6
- *
7
- * This is what replaced `@InjectQuerier()`: the standard decorator spec has no parameter decorators, so
8
- * the querier can no longer be injected into an argument and is read from the ambient flow instead.
9
- *
10
- * @example
11
- * ```ts
12
- * class UserService {
13
- * @Transactional()
14
- * async register(data: Partial<User>) {
15
- * await currentQuerier().insertOne(User, data);
16
- * }
17
- * }
18
- * ```
19
- */
20
- export declare function currentQuerier(): Querier;
21
- /** The ambient querier, or `undefined` outside a transaction. For callers that can work without one. */
22
- export declare function currentQuerierIfAny(): Querier | undefined;
@@ -1,42 +0,0 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
2
- /**
3
- * The querier `@Transactional()` opened for the current async flow.
4
- *
5
- * Separate from the `UqlContext` storage in `context/context.ts` on purpose: that one is remapped to a
6
- * synchronous browser shim which cannot propagate across `await`, and a querier handle must. Nothing in
7
- * a browser bundle opens a transaction, so this stays server-only and `verify-dist` keeps it out of the
8
- * browser-facing graph.
9
- */
10
- const querierStorage = new AsyncLocalStorage();
11
- /** Runs `callback` with `querier` as the ambient one, for the whole async flow beneath it. */
12
- export function withQuerierContext(querier, callback) {
13
- return querierStorage.run(querier, callback);
14
- }
15
- /**
16
- * The querier of the enclosing `@Transactional()` method.
17
- *
18
- * This is what replaced `@InjectQuerier()`: the standard decorator spec has no parameter decorators, so
19
- * the querier can no longer be injected into an argument and is read from the ambient flow instead.
20
- *
21
- * @example
22
- * ```ts
23
- * class UserService {
24
- * @Transactional()
25
- * async register(data: Partial<User>) {
26
- * await currentQuerier().insertOne(User, data);
27
- * }
28
- * }
29
- * ```
30
- */
31
- export function currentQuerier() {
32
- const querier = querierStorage.getStore();
33
- if (!querier) {
34
- throw new TypeError('currentQuerier() found no active querier. Call it inside a @Transactional() method, or take a querier ' +
35
- 'from the pool yourself with `await using querier = await pool.getQuerier()`.');
36
- }
37
- return querier;
38
- }
39
- /** The ambient querier, or `undefined` outside a transaction. For callers that can work without one. */
40
- export function currentQuerierIfAny() {
41
- return querierStorage.getStore();
42
- }
@@ -1,26 +0,0 @@
1
- import type { IsolationLevel, QuerierPool } from '../type/index.js';
2
- export type TransactionalOptions = {
3
- /** `required` opens a transaction when none is active; `supported` joins one but never starts one. */
4
- readonly propagation?: 'supported' | 'required';
5
- readonly pool?: QuerierPool;
6
- readonly isolationLevel?: IsolationLevel;
7
- };
8
- /**
9
- * Wraps the method in a transaction and publishes its querier for {@link currentQuerier} to pick up.
10
- *
11
- * @remarks Replaces the `@InjectQuerier()` parameter that used to receive the querier. The standard
12
- * decorator spec has no parameter decorators, and the separate TC39 proposal for them is still Stage 1,
13
- * so the querier travels through async-local storage instead. A nested call joins the transaction
14
- * already in flight rather than opening a second one.
15
- *
16
- * @example
17
- * ```ts
18
- * class UserService {
19
- * @Transactional()
20
- * async register(data: Partial<User>) {
21
- * await currentQuerier().insertOne(User, data);
22
- * }
23
- * }
24
- * ```
25
- */
26
- export declare function Transactional({ propagation, pool, isolationLevel }?: TransactionalOptions): <This, Args extends unknown[], R>(original: (this: This, ...args: Args) => Promise<R>, context: ClassMethodDecoratorContext<This>) => (this: This, ...args: Args) => Promise<R>;
@@ -1,43 +0,0 @@
1
- import { getQuerierPool } from '../options.js';
2
- import { currentQuerierIfAny, withQuerierContext } from './querierContext.js';
3
- /**
4
- * Wraps the method in a transaction and publishes its querier for {@link currentQuerier} to pick up.
5
- *
6
- * @remarks Replaces the `@InjectQuerier()` parameter that used to receive the querier. The standard
7
- * decorator spec has no parameter decorators, and the separate TC39 proposal for them is still Stage 1,
8
- * so the querier travels through async-local storage instead. A nested call joins the transaction
9
- * already in flight rather than opening a second one.
10
- *
11
- * @example
12
- * ```ts
13
- * class UserService {
14
- * @Transactional()
15
- * async register(data: Partial<User>) {
16
- * await currentQuerier().insertOne(User, data);
17
- * }
18
- * }
19
- * ```
20
- */
21
- export function Transactional({ propagation = 'required', pool, isolationLevel } = {}) {
22
- return (original, context) => {
23
- // Checked at decoration time rather than on the first call: a synchronous method cannot be wrapped in
24
- // a transaction, and finding that out at startup beats finding out mid-request.
25
- if (original.constructor.name !== 'AsyncFunction') {
26
- throw new TypeError(`@Transactional() needs an async method, but '${String(context.name)}' is not one.`);
27
- }
28
- return async function (...args) {
29
- // Already inside a transactional flow: join it and let the outermost call own commit and release.
30
- if (currentQuerierIfAny()) {
31
- return original.apply(this, args);
32
- }
33
- // `withQuerier` releases; `transaction` commits or rolls back. `supported` joins a transaction but
34
- // never starts one, so it takes only the first half.
35
- return (pool ?? getQuerierPool()).withQuerier((querier) => {
36
- const run = () => original.apply(this, args);
37
- return withQuerierContext(querier, () => propagation === 'supported'
38
- ? run()
39
- : querier.transaction(run, isolationLevel ? { isolationLevel } : undefined));
40
- });
41
- };
42
- };
43
- }