uql-orm 0.28.1 → 0.29.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.
- package/README.md +4 -6
- package/dist/mongo/mongodbQuerier.d.ts +5 -0
- package/dist/mongo/mongodbQuerier.js +15 -7
- package/dist/mysql/mysql2Querier.d.ts +1 -1
- package/dist/mysql/mysql2Querier.js +7 -1
- package/dist/nestjs/uqlContextInterceptor.d.ts +2 -2
- package/dist/nestjs/uqlContextInterceptor.js +2 -2
- package/dist/nestjs/uqlModule.d.ts +3 -3
- package/dist/nestjs/uqlModule.js +5 -6
- package/dist/postgres/abstractPgQuerier.d.ts +3 -2
- package/dist/postgres/abstractPgQuerier.js +2 -2
- package/dist/querier/abstractPoolQuerier.d.ts +2 -2
- package/dist/querier/abstractPoolQuerier.js +7 -7
- package/dist/querier/abstractQuerier.d.ts +26 -8
- package/dist/querier/abstractQuerier.js +38 -15
- package/dist/querier/abstractSqlQuerier.d.ts +11 -2
- package/dist/querier/abstractSqlQuerier.js +30 -25
- package/dist/querier/index.d.ts +0 -2
- package/dist/querier/index.js +0 -2
- package/dist/sqlite/abstractSqliteQuerier.d.ts +1 -2
- package/dist/sqlite/abstractSqliteQuerier.js +2 -8
- package/dist/sqlite/hranaQuerier.d.ts +5 -0
- package/dist/sqlite/hranaQuerier.js +10 -6
- package/dist/type/querier.d.ts +12 -2
- package/package.json +23 -19
- package/dist/querier/querierContext.browser.d.ts +0 -12
- package/dist/querier/querierContext.browser.js +0 -18
- package/dist/querier/querierContext.d.ts +0 -22
- package/dist/querier/querierContext.js +0 -42
- package/dist/querier/transactional.d.ts +0 -26
- package/dist/querier/transactional.js +0 -43
package/README.md
CHANGED
|
@@ -32,9 +32,7 @@
|
|
|
32
32
|
npm install uql-orm pg # or mysql2, mariadb, better-sqlite3, mongodb, @tursodatabase/serverless, @libsql/client
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
That is the whole install.
|
|
36
|
-
and no compiler flag to turn on ([setup](https://uql-orm.dev/getting-started)), and the
|
|
37
|
-
[imperative API](https://uql-orm.dev/entities/imperative) skips decorators altogether.
|
|
35
|
+
That is the whole install ([setup](https://uql-orm.dev/getting-started)), and the [imperative API](https://uql-orm.dev/entities/imperative) skips decorators altogether.
|
|
38
36
|
|
|
39
37
|
```ts
|
|
40
38
|
await pool.findMany(User, {
|
|
@@ -51,14 +49,14 @@ from the browser to the server. The same object runs on every supported database
|
|
|
51
49
|
## Why UQL?
|
|
52
50
|
|
|
53
51
|
- **Serializable queries (JSON), not method chains.** Plain JSON in, typed rows out. No DSL to learn.
|
|
54
|
-
- **Type-safe to the leaf, nothing to generate.** Every key is checked against your entity, down into populated relations and [JSON/JSONB](https://uql-orm.dev/querying/json) dot-paths, so `$like` on a numeric column is a compile error. Entities are plain classes: no `.prisma` file, no generated client, no
|
|
52
|
+
- **Type-safe to the leaf, nothing to generate.** Every key is checked against your entity, down into populated relations and [JSON/JSONB](https://uql-orm.dev/querying/json) dot-paths, so `$like` on a numeric column is a compile error. Entities are plain classes on the standard TC39 decorators: no `.prisma` file, no generated client, no `reflect-metadata`, no `experimentalDecorators`.
|
|
55
53
|
- **One API, everywhere it runs.** PostgreSQL, CockroachDB, MySQL, MariaDB, SQLite, Turso, libSQL, Neon, Cloudflare D1, Bun's native SQL, and even MongoDB. The same code on Node 24+, Bun, Deno, [Cloudflare Workers](https://uql-orm.dev/cloudflare-d1), [AWS Lambda and Vercel](https://uql-orm.dev/serverless), and [the browser](https://uql-orm.dev/browser), with no native binaries on the `fetch`-based drivers.
|
|
56
54
|
- **Relations without N+1.** [`$populate`](https://uql-orm.dev/querying/relations) loads a to-many with one query for all parents, not one per parent. Nothing is lazy, so nothing fires behind your back in a serializer.
|
|
57
55
|
- **Migrations you read before they run.** Edit an entity, run `uql-migrate generate:entities`, review the SQL in the PR like any other file. [`drift:check`](https://uql-orm.dev/migrations) catches a database that no longer matches.
|
|
58
56
|
- **Raw SQL when you want it.** [`raw()`](https://uql-orm.dev/querying/raw-sql) fits anywhere in a query, [virtual fields](https://uql-orm.dev/entities/virtual-fields) are sub-queries you can filter on, and a migration can be plain SQL.
|
|
59
|
-
- **Light.** Zero runtime dependencies,
|
|
57
|
+
- **Light.** Zero runtime dependencies, under 280 kB on the wire, every dialect included. See [what we deleted to get there](https://uql-orm.dev/blog/zero-dependencies).
|
|
60
58
|
- **The hard things are built in.** [Semantic and vector search](https://uql-orm.dev/ai-semantic-search), [multi-tenant filters you cannot bypass by accident](https://uql-orm.dev/multi-tenancy), [soft-delete with restore](https://uql-orm.dev/entities/soft-delete), [streaming](https://uql-orm.dev/querying/streaming), and [a REST API from your entities](https://uql-orm.dev/http).
|
|
61
|
-
- **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark)
|
|
59
|
+
- **The fastest ORM.** On a full PostgreSQL round trip it adds the least over hand-written driver code of any ORM in our open-source [benchmark](https://github.com/rogerpadilla/ts-orm-benchmark), by roughly 3x over the next closest and an order of magnitude over the slowest.
|
|
62
60
|
|
|
63
61
|
## Get started
|
|
64
62
|
|
|
@@ -47,9 +47,14 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
47
47
|
}>;
|
|
48
48
|
protected internalDeleteMany<E extends Document>(entity: Type<E>, qm: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
49
49
|
get hasOpenTransaction(): boolean;
|
|
50
|
+
/** Every read and write goes through here, which makes it where a released querier is caught. */
|
|
50
51
|
collection<E extends Document>(entity: Type<E>): import("mongodb").Collection<E>;
|
|
51
52
|
get db(): import("mongodb").Db;
|
|
52
53
|
beginTransaction(_opts?: TransactionOptions): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* The driver owns the transaction state here and settles it in its own `finally`, so a failed commit
|
|
56
|
+
* or abort still leaves `inTransaction()` false and the querier releasable.
|
|
57
|
+
*/
|
|
53
58
|
commitTransaction(): Promise<void>;
|
|
54
59
|
rollbackTransaction(): Promise<void>;
|
|
55
60
|
internalRelease(): Promise<void>;
|
|
@@ -278,7 +278,11 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
278
278
|
get hasOpenTransaction() {
|
|
279
279
|
return !!this.session?.inTransaction();
|
|
280
280
|
}
|
|
281
|
+
/** Every read and write goes through here, which makes it where a released querier is caught. */
|
|
281
282
|
collection(entity) {
|
|
283
|
+
if (this.released) {
|
|
284
|
+
throw new TypeError('querier already released');
|
|
285
|
+
}
|
|
282
286
|
const { name } = getMeta(entity);
|
|
283
287
|
return this.db.collection(name);
|
|
284
288
|
}
|
|
@@ -296,29 +300,33 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
296
300
|
this.session.startTransaction();
|
|
297
301
|
});
|
|
298
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* The driver owns the transaction state here and settles it in its own `finally`, so a failed commit
|
|
305
|
+
* or abort still leaves `inTransaction()` false and the querier releasable.
|
|
306
|
+
*/
|
|
299
307
|
async commitTransaction() {
|
|
300
308
|
return this.serialize(async () => {
|
|
301
309
|
if (!this.hasOpenTransaction) {
|
|
302
310
|
throwNoPendingTransaction();
|
|
303
311
|
}
|
|
304
312
|
this.logger.logInfo('commitTransaction');
|
|
305
|
-
await this.session
|
|
313
|
+
await this.session?.commitTransaction();
|
|
306
314
|
});
|
|
307
315
|
}
|
|
308
316
|
async rollbackTransaction() {
|
|
309
317
|
return this.serialize(async () => {
|
|
310
318
|
if (!this.hasOpenTransaction) {
|
|
311
|
-
|
|
319
|
+
return;
|
|
312
320
|
}
|
|
313
321
|
this.logger.logInfo('rollbackTransaction');
|
|
314
|
-
await this.session
|
|
322
|
+
await this.session?.abortTransaction();
|
|
315
323
|
});
|
|
316
324
|
}
|
|
317
325
|
async internalRelease() {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await this.session?.endSession();
|
|
326
|
+
const session = this.session;
|
|
327
|
+
// Cleared first, so a failing `endSession` cannot leave the querier holding a session it already
|
|
328
|
+
// tried to end.
|
|
322
329
|
this.session = undefined;
|
|
330
|
+
await session?.endSession();
|
|
323
331
|
}
|
|
324
332
|
}
|
|
@@ -7,5 +7,5 @@ export declare class MySql2Querier extends AbstractPoolQuerier<PoolConnection> {
|
|
|
7
7
|
internalAll<T>(query: string, values?: unknown[]): Promise<T[]>;
|
|
8
8
|
internalRun(query: string, values?: unknown[]): Promise<import("../type/query.js").QueryUpdateResult>;
|
|
9
9
|
internalStream<T>(query: string, values?: unknown[]): AsyncGenerator<Awaited<T>, void, unknown>;
|
|
10
|
-
protected releaseConn(conn: PoolConnection): Promise<void>;
|
|
10
|
+
protected releaseConn(conn: PoolConnection, discard: boolean): Promise<void>;
|
|
11
11
|
}
|
|
@@ -27,7 +27,13 @@ export class MySql2Querier extends AbstractPoolQuerier {
|
|
|
27
27
|
stream.destroy();
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
|
-
async releaseConn(conn) {
|
|
30
|
+
async releaseConn(conn, discard) {
|
|
31
|
+
// mysql2 resets nothing on release, so a connection the pool takes back after a failed rollback
|
|
32
|
+
// hands the next caller someone else's open transaction. `destroy` drops it from the pool instead.
|
|
33
|
+
if (discard) {
|
|
34
|
+
conn.destroy();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
31
37
|
await conn.release();
|
|
32
38
|
}
|
|
33
39
|
}
|
|
@@ -3,8 +3,8 @@ import { Observable } from 'rxjs';
|
|
|
3
3
|
import type { UqlContext } from '../type/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Runs each request inside `withContext`, so parameterized/`security` filters (multi-tenancy, RLS)
|
|
6
|
-
* are scoped automatically for every query in the request - including relations, cascades, and
|
|
7
|
-
*
|
|
6
|
+
* are scoped automatically for every query in the request - including relations, cascades, and the
|
|
7
|
+
* queries a transaction runs. Wired for you by {@link UqlModule.forRoot} when you pass `getContext`.
|
|
8
8
|
*/
|
|
9
9
|
export declare class UqlContextInterceptor<Req = unknown> implements NestInterceptor {
|
|
10
10
|
private readonly getContext;
|
|
@@ -37,8 +37,8 @@ import { Observable } from 'rxjs';
|
|
|
37
37
|
import { withContext } from '../context/context.js';
|
|
38
38
|
/**
|
|
39
39
|
* Runs each request inside `withContext`, so parameterized/`security` filters (multi-tenancy, RLS)
|
|
40
|
-
* are scoped automatically for every query in the request - including relations, cascades, and
|
|
41
|
-
*
|
|
40
|
+
* are scoped automatically for every query in the request - including relations, cascades, and the
|
|
41
|
+
* queries a transaction runs. Wired for you by {@link UqlModule.forRoot} when you pass `getContext`.
|
|
42
42
|
*/
|
|
43
43
|
let UqlContextInterceptor = (() => {
|
|
44
44
|
let _classDecorators = [Injectable()];
|
|
@@ -2,9 +2,9 @@ import { type DynamicModule, type FactoryProvider } from '@nestjs/common';
|
|
|
2
2
|
import type { QuerierPool, UqlContext } from '../type/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Injection token for the configured {@link QuerierPool} - for injecting into your own
|
|
5
|
-
* providers. UQL's own machinery (`getQuerier`, `querierMiddleware`, `createFetchHandler
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* providers. UQL's own machinery (`getQuerier`, `querierMiddleware`, `createFetchHandler`) reads
|
|
6
|
+
* the default pool set by {@link UqlModule.forRoot}, not this token, so overriding the provider
|
|
7
|
+
* does not redirect UQL internals.
|
|
8
8
|
*/
|
|
9
9
|
export declare const UQL_QUERIER_POOL: unique symbol;
|
|
10
10
|
/** Shared by {@link UqlModuleOptions} and {@link UqlModuleAsyncOptions}. */
|
package/dist/nestjs/uqlModule.js
CHANGED
|
@@ -38,16 +38,15 @@ import { setQuerierPool } from '../options.js';
|
|
|
38
38
|
import { UqlContextInterceptor } from './uqlContextInterceptor.js';
|
|
39
39
|
/**
|
|
40
40
|
* Injection token for the configured {@link QuerierPool} - for injecting into your own
|
|
41
|
-
* providers. UQL's own machinery (`getQuerier`, `querierMiddleware`, `createFetchHandler
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* providers. UQL's own machinery (`getQuerier`, `querierMiddleware`, `createFetchHandler`) reads
|
|
42
|
+
* the default pool set by {@link UqlModule.forRoot}, not this token, so overriding the provider
|
|
43
|
+
* does not redirect UQL internals.
|
|
44
44
|
*/
|
|
45
45
|
export const UQL_QUERIER_POOL = Symbol('UQL_QUERIER_POOL');
|
|
46
46
|
/**
|
|
47
47
|
* NestJS integration: provides the pool via DI, sets it as UQL's default pool (so `getQuerier()`,
|
|
48
|
-
* `querierMiddleware` (express platform)
|
|
49
|
-
*
|
|
50
|
-
* application shutdown.
|
|
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.
|
|
51
50
|
*/
|
|
52
51
|
/**
|
|
53
52
|
* Ends the pool when Nest shuts down.
|
|
@@ -9,7 +9,8 @@ export interface PgAnyClient {
|
|
|
9
9
|
query(stream: object): AsyncIterable<RawRow> & {
|
|
10
10
|
destroy(): void;
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
/** Any truthy argument makes `pg-pool` evict the client instead of returning it to the idle list. */
|
|
13
|
+
release(discard?: boolean): void | Promise<void>;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Shared base class for Postgres-compatible queriers (standard pg, CockroachDB, Neon).
|
|
@@ -19,5 +20,5 @@ export declare abstract class AbstractPgQuerier<C extends PgAnyClient, D extends
|
|
|
19
20
|
internalAll<T>(query: string, values?: unknown[]): Promise<T[]>;
|
|
20
21
|
internalRun(query: string, values?: unknown[]): Promise<import("../type/query.js").QueryUpdateResult>;
|
|
21
22
|
internalStream<T>(query: string, values?: unknown[]): AsyncGenerator<Awaited<T>, void, unknown>;
|
|
22
|
-
protected releaseConn(conn: C): Promise<void>;
|
|
23
|
+
protected releaseConn(conn: C, discard: boolean): Promise<void>;
|
|
23
24
|
}
|
|
@@ -8,6 +8,6 @@ export declare abstract class AbstractPoolQuerier<C> extends AbstractSqlQuerier
|
|
|
8
8
|
protected getConn(): C;
|
|
9
9
|
constructor(dialect: AbstractSqlDialect, connect: () => Promise<C>, extra?: ExtraOptions | undefined);
|
|
10
10
|
protected lazyConnect(): Promise<void>;
|
|
11
|
-
internalRelease(): Promise<void>;
|
|
12
|
-
protected abstract releaseConn(conn: C): Promise<void>;
|
|
11
|
+
internalRelease(discard: boolean): Promise<void>;
|
|
12
|
+
protected abstract releaseConn(conn: C, discard: boolean): Promise<void>;
|
|
13
13
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { throwPendingTransaction } from '../util/index.js';
|
|
2
1
|
import { AbstractSqlQuerier } from './abstractSqlQuerier.js';
|
|
3
2
|
export class AbstractPoolQuerier extends AbstractSqlQuerier {
|
|
4
3
|
connect;
|
|
@@ -15,16 +14,17 @@ export class AbstractPoolQuerier extends AbstractSqlQuerier {
|
|
|
15
14
|
this.extra = extra;
|
|
16
15
|
}
|
|
17
16
|
async lazyConnect() {
|
|
17
|
+
await super.lazyConnect();
|
|
18
18
|
this.conn ??= await this.connect();
|
|
19
19
|
}
|
|
20
|
-
async internalRelease() {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
if (!this.conn) {
|
|
20
|
+
async internalRelease(discard) {
|
|
21
|
+
const conn = this.conn;
|
|
22
|
+
if (!conn) {
|
|
25
23
|
return;
|
|
26
24
|
}
|
|
27
|
-
|
|
25
|
+
// Cleared even when the hand-back fails: keeping a connection the pool has already been given
|
|
26
|
+
// back means the next `release()` returns it twice, which pg reports as an already-released client.
|
|
28
27
|
this.conn = undefined;
|
|
28
|
+
await this.releaseConn(conn, discard);
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -12,6 +12,11 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
12
12
|
* and ensuring that the database connection is used safely across concurrent calls.
|
|
13
13
|
*/
|
|
14
14
|
private taskQueue;
|
|
15
|
+
/**
|
|
16
|
+
* A querier is one unit of work, so releasing ends it. Checked where each backend reaches for its
|
|
17
|
+
* connection, on every driver and not just the pooled ones.
|
|
18
|
+
*/
|
|
19
|
+
protected released: boolean;
|
|
15
20
|
protected readonly logger: LoggerWrapper;
|
|
16
21
|
constructor(extra?: ExtraOptions | undefined);
|
|
17
22
|
protected validateProjectionQuery<E extends object>(entity: Type<E>, q: Query<E>): void;
|
|
@@ -121,8 +126,8 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
121
126
|
* below were got wrong by code that hand-rolled it:
|
|
122
127
|
*
|
|
123
128
|
* - `beginTransaction` connects before it begins, so a refused connection lands in the catch with no
|
|
124
|
-
* transaction open.
|
|
125
|
-
*
|
|
129
|
+
* transaction open. `rollbackTransaction` is a no-op there rather than an error, which is why a
|
|
130
|
+
* wrong password no longer surfaces as a transaction-state error.
|
|
126
131
|
* - A rollback that fails too is a consequence of the original failure, not news, so it must not
|
|
127
132
|
* replace it either.
|
|
128
133
|
*
|
|
@@ -137,13 +142,12 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
137
142
|
* Fires global listeners first, then entity-level hooks.
|
|
138
143
|
*/
|
|
139
144
|
private emitHook;
|
|
140
|
-
releaseIfFree(): Promise<void>;
|
|
141
145
|
/**
|
|
142
|
-
*
|
|
143
|
-
* This is used by the @Serialized decorator to protect database-level operations.
|
|
146
|
+
* Runs `task` after everything already queued on this querier, one at a time.
|
|
144
147
|
*
|
|
145
|
-
* @
|
|
146
|
-
*
|
|
148
|
+
* @remarks Not re-entrant: only one task runs at a time, so a serialized method awaited from inside
|
|
149
|
+
* another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
|
|
150
|
+
* sequential rather than nested.
|
|
147
151
|
*/
|
|
148
152
|
protected serialize<T>(task: () => Promise<T>): Promise<T>;
|
|
149
153
|
/**
|
|
@@ -157,9 +161,23 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
157
161
|
*/
|
|
158
162
|
protected timed<T>(query: string, values: unknown[] | undefined, task: () => Promise<T>): Promise<T>;
|
|
159
163
|
abstract beginTransaction(opts?: TransactionOptions): Promise<void>;
|
|
164
|
+
/** Strict: this is the check that catches a forgotten `beginTransaction`. */
|
|
160
165
|
abstract commitTransaction(): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Rolls the open transaction back, or does nothing when there is none: it is called from `catch` and
|
|
168
|
+
* `finally`, where the caller cannot know whether `beginTransaction` got far enough to open one.
|
|
169
|
+
*/
|
|
161
170
|
abstract rollbackTransaction(): Promise<void>;
|
|
162
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Rolls back an unfinished transaction, then hands the connection back.
|
|
173
|
+
*
|
|
174
|
+
* @remarks Refusing to release was the opposite of safe: the throw came *before* the connection went
|
|
175
|
+
* back, so it destroyed the error that got here and cost the pool a connection with a live `BEGIN` on
|
|
176
|
+
* it. It is also the only option `await using` can reach, which calls `Symbol.asyncDispose` with no
|
|
177
|
+
* arguments and discards what it returns.
|
|
178
|
+
*/
|
|
163
179
|
release(): Promise<void>;
|
|
164
180
|
[Symbol.asyncDispose](): Promise<void>;
|
|
181
|
+
/** `discard` means the connection must not be reused; backends with a pool evict it instead. */
|
|
182
|
+
protected abstract internalRelease(discard: boolean): Promise<void>;
|
|
165
183
|
}
|
|
@@ -13,6 +13,11 @@ export class AbstractQuerier {
|
|
|
13
13
|
* and ensuring that the database connection is used safely across concurrent calls.
|
|
14
14
|
*/
|
|
15
15
|
taskQueue = Promise.resolve();
|
|
16
|
+
/**
|
|
17
|
+
* A querier is one unit of work, so releasing ends it. Checked where each backend reaches for its
|
|
18
|
+
* connection, on every driver and not just the pooled ones.
|
|
19
|
+
*/
|
|
20
|
+
released = false;
|
|
16
21
|
logger;
|
|
17
22
|
constructor(extra) {
|
|
18
23
|
this.extra = extra;
|
|
@@ -365,8 +370,8 @@ export class AbstractQuerier {
|
|
|
365
370
|
* below were got wrong by code that hand-rolled it:
|
|
366
371
|
*
|
|
367
372
|
* - `beginTransaction` connects before it begins, so a refused connection lands in the catch with no
|
|
368
|
-
* transaction open.
|
|
369
|
-
*
|
|
373
|
+
* transaction open. `rollbackTransaction` is a no-op there rather than an error, which is why a
|
|
374
|
+
* wrong password no longer surfaces as a transaction-state error.
|
|
370
375
|
* - A rollback that fails too is a consequence of the original failure, not news, so it must not
|
|
371
376
|
* replace it either.
|
|
372
377
|
*
|
|
@@ -386,9 +391,12 @@ export class AbstractQuerier {
|
|
|
386
391
|
return res;
|
|
387
392
|
}
|
|
388
393
|
catch (err) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
394
|
+
// Reported rather than thrown: the error being unwound is the useful one. Inline rather than
|
|
395
|
+
// shared with `release` below, because this method is grafted onto plain objects in tests and
|
|
396
|
+
// every `this.x` it reaches for has to exist there too.
|
|
397
|
+
await this.rollbackTransaction().catch((rollbackErr) => {
|
|
398
|
+
this.logger.logError('rollback failed', rollbackErr);
|
|
399
|
+
});
|
|
392
400
|
throw err;
|
|
393
401
|
}
|
|
394
402
|
}
|
|
@@ -419,17 +427,12 @@ export class AbstractQuerier {
|
|
|
419
427
|
await runHooks(entity, event, payloads, { querier: this });
|
|
420
428
|
}
|
|
421
429
|
}
|
|
422
|
-
async releaseIfFree() {
|
|
423
|
-
if (!this.hasOpenTransaction) {
|
|
424
|
-
await this.internalRelease();
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
430
|
/**
|
|
428
|
-
*
|
|
429
|
-
* This is used by the @Serialized decorator to protect database-level operations.
|
|
431
|
+
* Runs `task` after everything already queued on this querier, one at a time.
|
|
430
432
|
*
|
|
431
|
-
* @
|
|
432
|
-
*
|
|
433
|
+
* @remarks Not re-entrant: only one task runs at a time, so a serialized method awaited from inside
|
|
434
|
+
* another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
|
|
435
|
+
* sequential rather than nested.
|
|
433
436
|
*/
|
|
434
437
|
async serialize(task) {
|
|
435
438
|
const res = this.taskQueue.then(task);
|
|
@@ -457,8 +460,28 @@ export class AbstractQuerier {
|
|
|
457
460
|
this.logger.logQuery(query, values, Math.round(performance.now() - startTime));
|
|
458
461
|
}
|
|
459
462
|
}
|
|
463
|
+
/**
|
|
464
|
+
* Rolls back an unfinished transaction, then hands the connection back.
|
|
465
|
+
*
|
|
466
|
+
* @remarks Refusing to release was the opposite of safe: the throw came *before* the connection went
|
|
467
|
+
* back, so it destroyed the error that got here and cost the pool a connection with a live `BEGIN` on
|
|
468
|
+
* it. It is also the only option `await using` can reach, which calls `Symbol.asyncDispose` with no
|
|
469
|
+
* arguments and discards what it returns.
|
|
470
|
+
*/
|
|
460
471
|
async release() {
|
|
461
|
-
|
|
472
|
+
let discard = false;
|
|
473
|
+
if (this.hasOpenTransaction) {
|
|
474
|
+
this.logger.logWarn('rolling back a transaction left open at release');
|
|
475
|
+
// The rollback doubles as a health check. One that succeeds proves the connection round-trips and
|
|
476
|
+
// left no transaction behind, so it is safe to reuse. One that fails leaves a session state
|
|
477
|
+
// nothing here can name, and the next borrower would inherit it.
|
|
478
|
+
await this.rollbackTransaction().catch((err) => {
|
|
479
|
+
this.logger.logError('rollback failed; discarding the connection', err);
|
|
480
|
+
discard = true;
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
this.released = true;
|
|
484
|
+
return this.serialize(() => this.internalRelease(discard));
|
|
462
485
|
}
|
|
463
486
|
async [Symbol.asyncDispose]() {
|
|
464
487
|
return this.release();
|
|
@@ -27,8 +27,9 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
27
27
|
*/
|
|
28
28
|
protected loadInsertIdIncrement(): Promise<number>;
|
|
29
29
|
/**
|
|
30
|
-
* Hook for subclasses (e.g. pool queriers) to establish a connection.
|
|
31
|
-
*
|
|
30
|
+
* Hook for subclasses (e.g. pool queriers) to establish a connection. Called before every query and
|
|
31
|
+
* before `BEGIN`, outside the timing window, which makes it the one place a released querier is
|
|
32
|
+
* caught for every SQL backend.
|
|
32
33
|
*/
|
|
33
34
|
protected lazyConnect(): Promise<void>;
|
|
34
35
|
all<T>(query: string, values?: unknown[]): Promise<T[]>;
|
|
@@ -72,4 +73,12 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
72
73
|
beginTransaction(opts?: TransactionOptions): Promise<void>;
|
|
73
74
|
commitTransaction(): Promise<void>;
|
|
74
75
|
rollbackTransaction(): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Only a statement that succeeded ends the transaction. A `COMMIT` that fails can leave it open
|
|
78
|
+
* (SQLite answers `SQLITE_BUSY` and keeps it), so the flag has to stay set for the `catch` in
|
|
79
|
+
* {@link AbstractQuerier.transaction} or {@link AbstractQuerier.release} to roll it back.
|
|
80
|
+
*/
|
|
81
|
+
private endTransactionWith;
|
|
82
|
+
/** Transaction statements skip `timed()`, so they attach their own query context to a failure. */
|
|
83
|
+
private runTransactionCommand;
|
|
75
84
|
}
|
|
@@ -35,10 +35,15 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
35
35
|
return Number.isInteger(value) && value > 0 ? value : 1;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
38
|
-
* Hook for subclasses (e.g. pool queriers) to establish a connection.
|
|
39
|
-
*
|
|
38
|
+
* Hook for subclasses (e.g. pool queriers) to establish a connection. Called before every query and
|
|
39
|
+
* before `BEGIN`, outside the timing window, which makes it the one place a released querier is
|
|
40
|
+
* caught for every SQL backend.
|
|
40
41
|
*/
|
|
41
|
-
async lazyConnect() {
|
|
42
|
+
async lazyConnect() {
|
|
43
|
+
if (this.released) {
|
|
44
|
+
throw new TypeError('querier already released');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
42
47
|
async all(query, values) {
|
|
43
48
|
return this.serialize(async () => {
|
|
44
49
|
await this.lazyConnect();
|
|
@@ -281,12 +286,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
281
286
|
}
|
|
282
287
|
await this.lazyConnect();
|
|
283
288
|
for (const sql of this.dialect.getBeginTransactionStatements(opts?.isolationLevel)) {
|
|
284
|
-
|
|
285
|
-
await this.internalRun(sql);
|
|
286
|
-
}
|
|
287
|
-
catch (err) {
|
|
288
|
-
throw enrichError(err, this.logger, sql);
|
|
289
|
-
}
|
|
289
|
+
await this.runTransactionCommand(sql);
|
|
290
290
|
}
|
|
291
291
|
this.hasPendingTransaction = true;
|
|
292
292
|
});
|
|
@@ -296,27 +296,32 @@ export class AbstractSqlQuerier extends AbstractQuerier {
|
|
|
296
296
|
if (!this.hasPendingTransaction) {
|
|
297
297
|
throwNoPendingTransaction();
|
|
298
298
|
}
|
|
299
|
-
|
|
300
|
-
await this.internalRun(this.dialect.commitTransactionCommand);
|
|
301
|
-
}
|
|
302
|
-
catch (err) {
|
|
303
|
-
throw enrichError(err, this.logger, this.dialect.commitTransactionCommand);
|
|
304
|
-
}
|
|
305
|
-
this.hasPendingTransaction = false;
|
|
299
|
+
await this.endTransactionWith(this.dialect.commitTransactionCommand);
|
|
306
300
|
});
|
|
307
301
|
}
|
|
308
302
|
async rollbackTransaction() {
|
|
309
303
|
return this.serialize(async () => {
|
|
310
|
-
if (
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
314
|
-
await this.internalRun(this.dialect.rollbackTransactionCommand);
|
|
315
|
-
}
|
|
316
|
-
catch (err) {
|
|
317
|
-
throw enrichError(err, this.logger, this.dialect.rollbackTransactionCommand);
|
|
304
|
+
if (this.hasPendingTransaction) {
|
|
305
|
+
await this.endTransactionWith(this.dialect.rollbackTransactionCommand);
|
|
318
306
|
}
|
|
319
|
-
this.hasPendingTransaction = false;
|
|
320
307
|
});
|
|
321
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Only a statement that succeeded ends the transaction. A `COMMIT` that fails can leave it open
|
|
311
|
+
* (SQLite answers `SQLITE_BUSY` and keeps it), so the flag has to stay set for the `catch` in
|
|
312
|
+
* {@link AbstractQuerier.transaction} or {@link AbstractQuerier.release} to roll it back.
|
|
313
|
+
*/
|
|
314
|
+
async endTransactionWith(command) {
|
|
315
|
+
await this.runTransactionCommand(command);
|
|
316
|
+
this.hasPendingTransaction = false;
|
|
317
|
+
}
|
|
318
|
+
/** Transaction statements skip `timed()`, so they attach their own query context to a failure. */
|
|
319
|
+
async runTransactionCommand(sql) {
|
|
320
|
+
try {
|
|
321
|
+
await this.internalRun(sql);
|
|
322
|
+
}
|
|
323
|
+
catch (err) {
|
|
324
|
+
throw enrichError(err, this.logger, sql);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
322
327
|
}
|
package/dist/querier/index.d.ts
CHANGED
|
@@ -2,6 +2,4 @@ export * from './abstractQuerier.js';
|
|
|
2
2
|
export * from './abstractQuerierPool.js';
|
|
3
3
|
export * from './abstractSqlQuerier.js';
|
|
4
4
|
export * from './abstractSqlQuerierPool.js';
|
|
5
|
-
export * from './querierContext.js';
|
|
6
5
|
export * from './queryError.js';
|
|
7
|
-
export * from './transactional.js';
|
package/dist/querier/index.js
CHANGED
|
@@ -2,6 +2,4 @@ export * from './abstractQuerier.js';
|
|
|
2
2
|
export * from './abstractQuerierPool.js';
|
|
3
3
|
export * from './abstractSqlQuerier.js';
|
|
4
4
|
export * from './abstractSqlQuerierPool.js';
|
|
5
|
-
export * from './querierContext.js';
|
|
6
5
|
export * from './queryError.js';
|
|
7
|
-
export * from './transactional.js';
|
|
@@ -30,8 +30,7 @@ export type SqlitePreparedStatement = {
|
|
|
30
30
|
export declare abstract class AbstractSqliteQuerier extends AbstractSqlQuerier {
|
|
31
31
|
/**
|
|
32
32
|
* SQLite drivers hold a single shared handle rather than a connection from a pool, so releasing
|
|
33
|
-
* a querier returns nothing
|
|
34
|
-
* closable per-querier connection override this.
|
|
33
|
+
* a querier returns nothing at all. Drivers owning a closable per-querier connection override this.
|
|
35
34
|
*/
|
|
36
35
|
internalRelease(): Promise<void>;
|
|
37
36
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { AbstractSqlQuerier } from '../querier/index.js';
|
|
2
|
-
import { throwPendingTransaction } from '../util/index.js';
|
|
3
2
|
/** Bound parameters reach a driver as `unknown[]` from the compiler; every driver types them narrowly. */
|
|
4
3
|
export function toSqliteBindValues(values) {
|
|
5
4
|
return (values || []);
|
|
@@ -7,14 +6,9 @@ export function toSqliteBindValues(values) {
|
|
|
7
6
|
export class AbstractSqliteQuerier extends AbstractSqlQuerier {
|
|
8
7
|
/**
|
|
9
8
|
* SQLite drivers hold a single shared handle rather than a connection from a pool, so releasing
|
|
10
|
-
* a querier returns nothing
|
|
11
|
-
* closable per-querier connection override this.
|
|
9
|
+
* a querier returns nothing at all. Drivers owning a closable per-querier connection override this.
|
|
12
10
|
*/
|
|
13
|
-
async internalRelease() {
|
|
14
|
-
if (this.hasOpenTransaction) {
|
|
15
|
-
throwPendingTransaction();
|
|
16
|
-
}
|
|
17
|
-
}
|
|
11
|
+
async internalRelease() { }
|
|
18
12
|
}
|
|
19
13
|
/**
|
|
20
14
|
* Querier for the SQLite drivers that expose prepared statements: `better-sqlite3`, `bun:sqlite`
|
|
@@ -48,6 +48,11 @@ export declare class HranaQuerier extends AbstractSqliteQuerier {
|
|
|
48
48
|
internalRun(query: string, values?: unknown[]): Promise<import("../type/query.js").QueryUpdateResult>;
|
|
49
49
|
get hasOpenTransaction(): boolean;
|
|
50
50
|
beginTransaction(_opts?: TransactionOptions): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Both drop the handle before the call, not after: one that outlived a failed commit or rollback left
|
|
53
|
+
* the querier unreleasable. The optional call in the rollback is also what makes it a no-op when
|
|
54
|
+
* there is nothing open.
|
|
55
|
+
*/
|
|
51
56
|
commitTransaction(): Promise<void>;
|
|
52
57
|
rollbackTransaction(): Promise<void>;
|
|
53
58
|
internalRelease(): Promise<void>;
|
|
@@ -41,22 +41,26 @@ export class HranaQuerier extends AbstractSqliteQuerier {
|
|
|
41
41
|
this.tx = await this.client.transaction('write');
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* Both drop the handle before the call, not after: one that outlived a failed commit or rollback left
|
|
46
|
+
* the querier unreleasable. The optional call in the rollback is also what makes it a no-op when
|
|
47
|
+
* there is nothing open.
|
|
48
|
+
*/
|
|
44
49
|
async commitTransaction() {
|
|
45
50
|
return this.serialize(async () => {
|
|
46
|
-
|
|
51
|
+
const tx = this.tx;
|
|
52
|
+
if (!tx) {
|
|
47
53
|
throwNoPendingTransaction();
|
|
48
54
|
}
|
|
49
|
-
await this.tx.commit();
|
|
50
55
|
this.tx = undefined;
|
|
56
|
+
await tx.commit();
|
|
51
57
|
});
|
|
52
58
|
}
|
|
53
59
|
async rollbackTransaction() {
|
|
54
60
|
return this.serialize(async () => {
|
|
55
|
-
|
|
56
|
-
throwNoPendingTransaction();
|
|
57
|
-
}
|
|
58
|
-
await this.tx.rollback();
|
|
61
|
+
const tx = this.tx;
|
|
59
62
|
this.tx = undefined;
|
|
63
|
+
await tx?.rollback();
|
|
60
64
|
});
|
|
61
65
|
}
|
|
62
66
|
async internalRelease() {
|
package/dist/type/querier.d.ts
CHANGED
|
@@ -15,6 +15,13 @@ export type IsolationLevel = 'read uncommitted' | 'read committed' | 'repeatable
|
|
|
15
15
|
* Options for starting a transaction.
|
|
16
16
|
*/
|
|
17
17
|
export type TransactionOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* Applies to this transaction only.
|
|
20
|
+
*
|
|
21
|
+
* @remarks MySQL and MariaDB set it as a statement of its own ahead of `START TRANSACTION`, so a
|
|
22
|
+
* `START TRANSACTION` that then fails leaves the level applied to whatever the pooled connection
|
|
23
|
+
* runs next. Set it per transaction that needs it rather than relying on what a connection carries.
|
|
24
|
+
*/
|
|
18
25
|
readonly isolationLevel?: IsolationLevel;
|
|
19
26
|
};
|
|
20
27
|
export type DialectName = SqlDialectName | 'mongodb';
|
|
@@ -80,11 +87,14 @@ export interface Querier extends UniversalQuerier {
|
|
|
80
87
|
*/
|
|
81
88
|
commitTransaction(): Promise<void>;
|
|
82
89
|
/**
|
|
83
|
-
* aborts the currently active transaction
|
|
90
|
+
* aborts the currently active transaction, or does nothing when there is none, so it is safe from a
|
|
91
|
+
* `catch` / `finally` without checking {@link hasOpenTransaction} first. `commitTransaction` is strict
|
|
92
|
+
* instead: a caller who believes their work was committed has to hear that it was not.
|
|
84
93
|
*/
|
|
85
94
|
rollbackTransaction(): Promise<void>;
|
|
86
95
|
/**
|
|
87
|
-
*
|
|
96
|
+
* rolls back any unfinished transaction and releases the querier to the pool. A pooled querier is
|
|
97
|
+
* finished afterwards: using it again throws rather than taking a second connection nothing owns.
|
|
88
98
|
*/
|
|
89
99
|
release(): Promise<void>;
|
|
90
100
|
/**
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uql-orm",
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
|
-
"description": "JSON-native TypeScript ORM
|
|
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.
|
|
6
|
+
"version": "0.29.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"
|
|
@@ -158,40 +157,45 @@
|
|
|
158
157
|
},
|
|
159
158
|
"keywords": [
|
|
160
159
|
"orm",
|
|
160
|
+
"uql",
|
|
161
|
+
"sql",
|
|
161
162
|
"query-builder",
|
|
162
163
|
"data-mapper",
|
|
163
|
-
"persistence",
|
|
164
164
|
"database",
|
|
165
165
|
"db",
|
|
166
|
+
"typescript",
|
|
167
|
+
"ts",
|
|
168
|
+
"javascript",
|
|
166
169
|
"typesafe",
|
|
167
|
-
"
|
|
168
|
-
"
|
|
169
|
-
"
|
|
170
|
-
"mariadb-orm",
|
|
171
|
-
"mysql",
|
|
172
|
-
"mysql-orm",
|
|
170
|
+
"type-safe",
|
|
171
|
+
"json",
|
|
172
|
+
"postgres",
|
|
173
173
|
"postgresql",
|
|
174
|
-
"
|
|
174
|
+
"pg",
|
|
175
|
+
"mysql",
|
|
176
|
+
"mariadb",
|
|
175
177
|
"sqlite",
|
|
176
|
-
"
|
|
178
|
+
"sqlite3",
|
|
177
179
|
"cockroachdb",
|
|
178
|
-
"cockroachdb-orm",
|
|
179
180
|
"mongodb",
|
|
180
|
-
"
|
|
181
|
+
"mongo",
|
|
181
182
|
"libsql",
|
|
182
183
|
"turso",
|
|
183
184
|
"neon",
|
|
184
185
|
"d1",
|
|
185
186
|
"cloudflare",
|
|
186
187
|
"bun",
|
|
188
|
+
"deno",
|
|
189
|
+
"node",
|
|
190
|
+
"nodejs",
|
|
187
191
|
"edge",
|
|
192
|
+
"serverless",
|
|
188
193
|
"entity",
|
|
189
|
-
"
|
|
190
|
-
"transaction",
|
|
191
|
-
"repository",
|
|
192
|
-
"service",
|
|
194
|
+
"transactions",
|
|
193
195
|
"migrations",
|
|
194
196
|
"vector-search",
|
|
197
|
+
"semantic-search",
|
|
198
|
+
"embeddings",
|
|
195
199
|
"nestjs",
|
|
196
200
|
"express"
|
|
197
201
|
],
|
|
@@ -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
|
-
}
|