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.
- package/dist/browser/querier/httpQuerier.d.ts +5 -5
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/dialect/abstractSqlDialect.d.ts +6 -4
- package/dist/dialect/abstractSqlDialect.js +29 -29
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +9 -0
- package/dist/dialect/vectorSqlDialect.js +8 -1
- package/dist/mongo/mongoDialect.d.ts +3 -3
- package/dist/mongo/mongoDialect.js +17 -9
- package/dist/mongo/mongodbQuerier.d.ts +9 -4
- 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 +45 -16
- package/dist/querier/abstractQuerier.js +99 -40
- package/dist/querier/abstractQuerierPool.d.ts +7 -7
- package/dist/querier/abstractSqlQuerier.d.ts +17 -6
- package/dist/querier/abstractSqlQuerier.js +61 -34
- 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/entity.d.ts +34 -11
- package/dist/type/querier.d.ts +26 -10
- package/dist/type/query.d.ts +22 -9
- package/dist/type/queryAggregate.d.ts +67 -24
- package/dist/type/queryWhere.d.ts +4 -1
- package/dist/type/universalQuerier.d.ts +11 -11
- package/dist/util/dialect.util.d.ts +26 -4
- package/dist/util/dialect.util.js +42 -0
- package/package.json +4 -5
- 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
|
@@ -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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtraOptions, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QueryPopulate, QuerySearch, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
1
|
+
import type { EntityData, ExtraOptions, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QueryPopulate, QuerySearch, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
2
|
import { LoggerWrapper } from '../util/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all database queriers.
|
|
@@ -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;
|
|
@@ -80,16 +85,16 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
80
85
|
*/
|
|
81
86
|
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>[]>;
|
|
82
87
|
protected abstract internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
83
|
-
insertOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E> | undefined>;
|
|
84
|
-
insertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
|
|
85
|
-
protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
|
|
88
|
+
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
89
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
90
|
+
protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
86
91
|
updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
87
92
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
88
93
|
protected abstract internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
89
94
|
restoreOneById<E extends object>(entity: Type<E>, id: IdValue<E>): Promise<number>;
|
|
90
95
|
restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
|
|
91
|
-
abstract upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E): Promise<QueryUpdateResult>;
|
|
92
|
-
abstract upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E[]): Promise<QueryUpdateResult>;
|
|
96
|
+
abstract upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
97
|
+
abstract upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
93
98
|
deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts?: QueryOptions): Promise<number>;
|
|
94
99
|
/**
|
|
95
100
|
* Delete records matching the query. Soft-deletes when the entity has a soft-delete field (unless
|
|
@@ -99,9 +104,18 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
99
104
|
deleteMany<E extends object>(q: QuerySearch<E> & {
|
|
100
105
|
$entity: Type<E>;
|
|
101
106
|
}, opts?: QueryOptions): Promise<number>;
|
|
107
|
+
/**
|
|
108
|
+
* The rows a delete is about to take, loaded only when a hook or listener is there to receive
|
|
109
|
+
* them: the round trip is pure overhead for the (common) delete nobody is watching, and
|
|
110
|
+
* `internalDeleteMany` has its own fast path that never reads the rows at all.
|
|
111
|
+
*
|
|
112
|
+
* `undefined` means nobody was watching, which is not the same as the empty array meaning nothing
|
|
113
|
+
* matched - the caller deletes by `q` for the first and skips the statement entirely for the second.
|
|
114
|
+
*/
|
|
115
|
+
private findDoomed;
|
|
102
116
|
protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
103
|
-
saveOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E>>;
|
|
104
|
-
saveMany<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
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
105
119
|
protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
|
|
106
120
|
private fillToManyThroughRelation;
|
|
107
121
|
private fillToManyOneToMany;
|
|
@@ -121,8 +135,8 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
121
135
|
* below were got wrong by code that hand-rolled it:
|
|
122
136
|
*
|
|
123
137
|
* - `beginTransaction` connects before it begins, so a refused connection lands in the catch with no
|
|
124
|
-
* transaction open.
|
|
125
|
-
*
|
|
138
|
+
* transaction open. `rollbackTransaction` is a no-op there rather than an error, which is why a
|
|
139
|
+
* wrong password no longer surfaces as a transaction-state error.
|
|
126
140
|
* - A rollback that fails too is a consequence of the original failure, not news, so it must not
|
|
127
141
|
* replace it either.
|
|
128
142
|
*
|
|
@@ -132,18 +146,19 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
132
146
|
* one afterwards.
|
|
133
147
|
*/
|
|
134
148
|
transaction<T>(callback: () => Promise<T>, opts?: TransactionOptions): Promise<T>;
|
|
149
|
+
/** Whether anything at all - a global listener or the entity itself - handles `event`. */
|
|
150
|
+
private hasHook;
|
|
135
151
|
/**
|
|
136
152
|
* Emit a lifecycle hook event for the given entity.
|
|
137
153
|
* Fires global listeners first, then entity-level hooks.
|
|
138
154
|
*/
|
|
139
155
|
private emitHook;
|
|
140
|
-
releaseIfFree(): Promise<void>;
|
|
141
156
|
/**
|
|
142
|
-
*
|
|
143
|
-
* This is used by the @Serialized decorator to protect database-level operations.
|
|
157
|
+
* Runs `task` after everything already queued on this querier, one at a time.
|
|
144
158
|
*
|
|
145
|
-
* @
|
|
146
|
-
*
|
|
159
|
+
* @remarks Not re-entrant: only one task runs at a time, so a serialized method awaited from inside
|
|
160
|
+
* another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
|
|
161
|
+
* sequential rather than nested.
|
|
147
162
|
*/
|
|
148
163
|
protected serialize<T>(task: () => Promise<T>): Promise<T>;
|
|
149
164
|
/**
|
|
@@ -157,9 +172,23 @@ export declare abstract class AbstractQuerier implements Querier {
|
|
|
157
172
|
*/
|
|
158
173
|
protected timed<T>(query: string, values: unknown[] | undefined, task: () => Promise<T>): Promise<T>;
|
|
159
174
|
abstract beginTransaction(opts?: TransactionOptions): Promise<void>;
|
|
175
|
+
/** Strict: this is the check that catches a forgotten `beginTransaction`. */
|
|
160
176
|
abstract commitTransaction(): Promise<void>;
|
|
177
|
+
/**
|
|
178
|
+
* Rolls the open transaction back, or does nothing when there is none: it is called from `catch` and
|
|
179
|
+
* `finally`, where the caller cannot know whether `beginTransaction` got far enough to open one.
|
|
180
|
+
*/
|
|
161
181
|
abstract rollbackTransaction(): Promise<void>;
|
|
162
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Rolls back an unfinished transaction, then hands the connection back.
|
|
184
|
+
*
|
|
185
|
+
* @remarks Refusing to release was the opposite of safe: the throw came *before* the connection went
|
|
186
|
+
* back, so it destroyed the error that got here and cost the pool a connection with a live `BEGIN` on
|
|
187
|
+
* it. It is also the only option `await using` can reach, which calls `Symbol.asyncDispose` with no
|
|
188
|
+
* arguments and discards what it returns.
|
|
189
|
+
*/
|
|
163
190
|
release(): Promise<void>;
|
|
164
191
|
[Symbol.asyncDispose](): Promise<void>;
|
|
192
|
+
/** `discard` means the connection must not be reused; backends with a pool evict it instead. */
|
|
193
|
+
protected abstract internalRelease(discard: boolean): Promise<void>;
|
|
165
194
|
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { getMeta } from '../entity/index.js';
|
|
2
|
-
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, } from '../util/index.js';
|
|
2
|
+
import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
3
3
|
import { enrichError } from './queryError.js';
|
|
4
|
+
/**
|
|
5
|
+
* Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
|
|
6
|
+
* `{ $where: id }`, and a nullish `$where` is *no filter*, so an unchecked one addresses the whole
|
|
7
|
+
* table. An entity declares its id optional, which puts `undefined` inside `IdValue<E>`, and the
|
|
8
|
+
* HTTP layer reaches these methods with parsed JSON regardless, so the guard belongs at runtime.
|
|
9
|
+
*
|
|
10
|
+
* Its callers are all `async` so this surfaces as a rejection on every one of them: a guard that
|
|
11
|
+
* threw synchronously from some and rejected from others would escape a caller's `.catch()`.
|
|
12
|
+
*/
|
|
13
|
+
function assertIdValue(entity, id) {
|
|
14
|
+
if (id === undefined || id === null) {
|
|
15
|
+
throw new TypeError(`'${entity.name}' was addressed by id, but the id is ${String(id)}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
/**
|
|
5
19
|
* Base class for all database queriers.
|
|
6
20
|
* It provides a standardized way to execute tasks serially to prevent race conditions on database connections.
|
|
@@ -13,6 +27,11 @@ export class AbstractQuerier {
|
|
|
13
27
|
* and ensuring that the database connection is used safely across concurrent calls.
|
|
14
28
|
*/
|
|
15
29
|
taskQueue = Promise.resolve();
|
|
30
|
+
/**
|
|
31
|
+
* A querier is one unit of work, so releasing ends it. Checked where each backend reaches for its
|
|
32
|
+
* connection, on every driver and not just the pooled ones.
|
|
33
|
+
*/
|
|
34
|
+
released = false;
|
|
16
35
|
logger;
|
|
17
36
|
constructor(extra) {
|
|
18
37
|
this.extra = extra;
|
|
@@ -60,6 +79,7 @@ export class AbstractQuerier {
|
|
|
60
79
|
return [$entity, query, maybeQueryOrOpts];
|
|
61
80
|
}
|
|
62
81
|
async findOneById(entity, id, q = {}, opts) {
|
|
82
|
+
assertIdValue(entity, id);
|
|
63
83
|
return this.findOne(entity, { ...q, $where: augmentWhere(getMeta(entity), q.$where, id) }, opts);
|
|
64
84
|
}
|
|
65
85
|
async findOne(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
|
|
@@ -110,7 +130,8 @@ export class AbstractQuerier {
|
|
|
110
130
|
await this.emitHook(entity, 'afterInsert', payload);
|
|
111
131
|
return ids;
|
|
112
132
|
}
|
|
113
|
-
updateOneById(entity, id, payload, opts) {
|
|
133
|
+
async updateOneById(entity, id, payload, opts) {
|
|
134
|
+
assertIdValue(entity, id);
|
|
114
135
|
return this.updateMany(entity, { $where: id }, payload, opts);
|
|
115
136
|
}
|
|
116
137
|
async updateMany(entity, q, payload, opts) {
|
|
@@ -119,7 +140,8 @@ export class AbstractQuerier {
|
|
|
119
140
|
await this.emitHook(entity, 'afterUpdate', [payload]);
|
|
120
141
|
return changes;
|
|
121
142
|
}
|
|
122
|
-
restoreOneById(entity, id) {
|
|
143
|
+
async restoreOneById(entity, id) {
|
|
144
|
+
assertIdValue(entity, id);
|
|
123
145
|
return this.restoreMany(entity, { $where: id });
|
|
124
146
|
}
|
|
125
147
|
async restoreMany(entity, q) {
|
|
@@ -132,16 +154,41 @@ export class AbstractQuerier {
|
|
|
132
154
|
filters: { softDelete: false },
|
|
133
155
|
});
|
|
134
156
|
}
|
|
135
|
-
deleteOneById(entity, id, opts) {
|
|
157
|
+
async deleteOneById(entity, id, opts) {
|
|
158
|
+
assertIdValue(entity, id);
|
|
136
159
|
return this.deleteMany(entity, { $where: id }, opts);
|
|
137
160
|
}
|
|
138
161
|
async deleteMany(entityOrQuery, qOrOpts, maybeOpts) {
|
|
139
162
|
const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, qOrOpts, maybeOpts);
|
|
140
|
-
await this.
|
|
141
|
-
|
|
142
|
-
|
|
163
|
+
const doomed = await this.findDoomed(entity, q, opts);
|
|
164
|
+
if (doomed?.length === 0) {
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
// Where a snapshot was taken, the statement names those rows rather than resolving `q` a second
|
|
168
|
+
// time. Two reads of one `$limit` with no total order are free to disagree, which would fire the
|
|
169
|
+
// hooks for one row and delete another; naming them also spares the second read.
|
|
170
|
+
const target = doomed ? { $where: doomed.map((it) => it[getMeta(entity).id]) } : q;
|
|
171
|
+
await this.emitHook(entity, 'beforeDelete', doomed ?? []);
|
|
172
|
+
const changes = await this.internalDeleteMany(entity, target, opts);
|
|
173
|
+
await this.emitHook(entity, 'afterDelete', doomed ?? []);
|
|
143
174
|
return changes;
|
|
144
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* The rows a delete is about to take, loaded only when a hook or listener is there to receive
|
|
178
|
+
* them: the round trip is pure overhead for the (common) delete nobody is watching, and
|
|
179
|
+
* `internalDeleteMany` has its own fast path that never reads the rows at all.
|
|
180
|
+
*
|
|
181
|
+
* `undefined` means nobody was watching, which is not the same as the empty array meaning nothing
|
|
182
|
+
* matched - the caller deletes by `q` for the first and skips the statement entirely for the second.
|
|
183
|
+
*/
|
|
184
|
+
async findDoomed(entity, q, opts) {
|
|
185
|
+
if (!this.hasHook(entity, 'beforeDelete') && !this.hasHook(entity, 'afterDelete')) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
// A hard delete takes already-soft-deleted rows too, so reading them back has to see them.
|
|
189
|
+
const findOpts = opts?.hardDelete ? { ...opts, filters: withoutSoftDeleteFilter(opts.filters) } : opts;
|
|
190
|
+
return this.internalFindMany(entity, q, findOpts);
|
|
191
|
+
}
|
|
145
192
|
async saveOne(entity, payload) {
|
|
146
193
|
const [id] = await this.saveMany(entity, [payload]);
|
|
147
194
|
return id;
|
|
@@ -365,8 +412,8 @@ export class AbstractQuerier {
|
|
|
365
412
|
* below were got wrong by code that hand-rolled it:
|
|
366
413
|
*
|
|
367
414
|
* - `beginTransaction` connects before it begins, so a refused connection lands in the catch with no
|
|
368
|
-
* transaction open.
|
|
369
|
-
*
|
|
415
|
+
* transaction open. `rollbackTransaction` is a no-op there rather than an error, which is why a
|
|
416
|
+
* wrong password no longer surfaces as a transaction-state error.
|
|
370
417
|
* - A rollback that fails too is a consequence of the original failure, not news, so it must not
|
|
371
418
|
* replace it either.
|
|
372
419
|
*
|
|
@@ -386,50 +433,42 @@ export class AbstractQuerier {
|
|
|
386
433
|
return res;
|
|
387
434
|
}
|
|
388
435
|
catch (err) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
436
|
+
// Reported rather than thrown: the error being unwound is the useful one. Inline rather than
|
|
437
|
+
// shared with `release` below, because this method is grafted onto plain objects in tests and
|
|
438
|
+
// every `this.x` it reaches for has to exist there too.
|
|
439
|
+
await this.rollbackTransaction().catch((rollbackErr) => {
|
|
440
|
+
this.logger.logError('rollback failed', rollbackErr);
|
|
441
|
+
});
|
|
392
442
|
throw err;
|
|
393
443
|
}
|
|
394
444
|
}
|
|
445
|
+
/** Whether anything at all - a global listener or the entity itself - handles `event`. */
|
|
446
|
+
hasHook(entity, event) {
|
|
447
|
+
return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
|
|
448
|
+
}
|
|
395
449
|
/**
|
|
396
450
|
* Emit a lifecycle hook event for the given entity.
|
|
397
451
|
* Fires global listeners first, then entity-level hooks.
|
|
398
452
|
*/
|
|
399
453
|
async emitHook(entity, event, payloads) {
|
|
400
|
-
|
|
401
|
-
const meta = getMeta(entity);
|
|
402
|
-
const registrations = meta.hooks?.[event];
|
|
403
|
-
// Fast bail-out: skip if no listeners and no entity hooks
|
|
404
|
-
if (!listeners?.length && !registrations?.length)
|
|
454
|
+
if (!this.hasHook(entity, event))
|
|
405
455
|
return;
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
410
|
-
if (
|
|
411
|
-
|
|
412
|
-
if (result instanceof Promise)
|
|
413
|
-
await result;
|
|
414
|
-
}
|
|
456
|
+
for (const listener of this.extra?.listeners ?? []) {
|
|
457
|
+
const fn = listener[event];
|
|
458
|
+
if (fn) {
|
|
459
|
+
const result = fn({ entity, querier: this, payloads, event });
|
|
460
|
+
if (result instanceof Promise)
|
|
461
|
+
await result;
|
|
415
462
|
}
|
|
416
463
|
}
|
|
417
|
-
|
|
418
|
-
if (registrations?.length) {
|
|
419
|
-
await runHooks(entity, event, payloads, { querier: this });
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
async releaseIfFree() {
|
|
423
|
-
if (!this.hasOpenTransaction) {
|
|
424
|
-
await this.internalRelease();
|
|
425
|
-
}
|
|
464
|
+
await runHooks(entity, event, payloads, { querier: this });
|
|
426
465
|
}
|
|
427
466
|
/**
|
|
428
|
-
*
|
|
429
|
-
* This is used by the @Serialized decorator to protect database-level operations.
|
|
467
|
+
* Runs `task` after everything already queued on this querier, one at a time.
|
|
430
468
|
*
|
|
431
|
-
* @
|
|
432
|
-
*
|
|
469
|
+
* @remarks Not re-entrant: only one task runs at a time, so a serialized method awaited from inside
|
|
470
|
+
* another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
|
|
471
|
+
* sequential rather than nested.
|
|
433
472
|
*/
|
|
434
473
|
async serialize(task) {
|
|
435
474
|
const res = this.taskQueue.then(task);
|
|
@@ -457,8 +496,28 @@ export class AbstractQuerier {
|
|
|
457
496
|
this.logger.logQuery(query, values, Math.round(performance.now() - startTime));
|
|
458
497
|
}
|
|
459
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Rolls back an unfinished transaction, then hands the connection back.
|
|
501
|
+
*
|
|
502
|
+
* @remarks Refusing to release was the opposite of safe: the throw came *before* the connection went
|
|
503
|
+
* back, so it destroyed the error that got here and cost the pool a connection with a live `BEGIN` on
|
|
504
|
+
* it. It is also the only option `await using` can reach, which calls `Symbol.asyncDispose` with no
|
|
505
|
+
* arguments and discards what it returns.
|
|
506
|
+
*/
|
|
460
507
|
async release() {
|
|
461
|
-
|
|
508
|
+
let discard = false;
|
|
509
|
+
if (this.hasOpenTransaction) {
|
|
510
|
+
this.logger.logWarn('rolling back a transaction left open at release');
|
|
511
|
+
// The rollback doubles as a health check. One that succeeds proves the connection round-trips and
|
|
512
|
+
// left no transaction behind, so it is safe to reuse. One that fails leaves a session state
|
|
513
|
+
// nothing here can name, and the next borrower would inherit it.
|
|
514
|
+
await this.rollbackTransaction().catch((err) => {
|
|
515
|
+
this.logger.logError('rollback failed; discarding the connection', err);
|
|
516
|
+
discard = true;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
this.released = true;
|
|
520
|
+
return this.serialize(() => this.internalRelease(discard));
|
|
462
521
|
}
|
|
463
522
|
async [Symbol.asyncDispose]() {
|
|
464
523
|
return this.release();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractDialect } from '../dialect/index.js';
|
|
2
|
-
import type { ExtraOptions, IdValue, PoolRunOptions, Querier, QuerierPool, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QuerySearch, QueryUpdateResult, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import type { EntityData, ExtraOptions, IdValue, PoolRunOptions, Querier, QuerierPool, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOne, QueryOptions, QuerySearch, QueryUpdateResult, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
|
|
5
5
|
*/
|
|
@@ -35,14 +35,14 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
|
|
|
35
35
|
findManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
|
|
36
36
|
count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
37
37
|
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>[]>;
|
|
38
|
-
insertOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E> | undefined>;
|
|
39
|
-
insertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
|
|
38
|
+
insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
|
|
39
|
+
insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
40
40
|
updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
41
41
|
updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
42
|
-
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E): Promise<QueryUpdateResult>;
|
|
43
|
-
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E[]): Promise<QueryUpdateResult>;
|
|
44
|
-
saveOne<E extends object>(entity: Type<E>, payload: E): Promise<IdValue<E>>;
|
|
45
|
-
saveMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
|
|
42
|
+
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
43
|
+
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
44
|
+
saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E>>;
|
|
45
|
+
saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
46
46
|
deleteOneById<E extends object>(entity: Type<E>, id: IdValue<E>, opts?: QueryOptions): Promise<number>;
|
|
47
47
|
deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
48
48
|
restoreOneById<E extends object>(entity: Type<E>, id: IdValue<E>): Promise<number>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
|
-
import type { ExtraOptions, IdValue, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
2
|
+
import type { EntityData, ExtraOptions, IdValue, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
|
|
3
3
|
import type { BuildUpdateResultPayload } from '../util/sql.util.js';
|
|
4
4
|
import { AbstractQuerier } from './abstractQuerier.js';
|
|
5
5
|
export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
|
|
@@ -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[]>;
|
|
@@ -63,13 +64,23 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
|
|
|
63
64
|
private hydrateFields;
|
|
64
65
|
protected internalCount<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
65
66
|
protected internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
|
|
66
|
-
internalInsertMany<E extends object>(entity: Type<E>, payload: E[]): Promise<IdValue<E>[]>;
|
|
67
|
+
internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
|
|
67
68
|
internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
/** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
|
|
70
|
+
private settleIds;
|
|
71
|
+
upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
|
|
72
|
+
upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
|
|
70
73
|
protected internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
|
|
71
74
|
get hasOpenTransaction(): boolean;
|
|
72
75
|
beginTransaction(opts?: TransactionOptions): Promise<void>;
|
|
73
76
|
commitTransaction(): Promise<void>;
|
|
74
77
|
rollbackTransaction(): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Only a statement that succeeded ends the transaction. A `COMMIT` that fails can leave it open
|
|
80
|
+
* (SQLite answers `SQLITE_BUSY` and keeps it), so the flag has to stay set for the `catch` in
|
|
81
|
+
* {@link AbstractQuerier.transaction} or {@link AbstractQuerier.release} to roll it back.
|
|
82
|
+
*/
|
|
83
|
+
private endTransactionWith;
|
|
84
|
+
/** Transaction statements skip `timed()`, so they attach their own query context to a failure. */
|
|
85
|
+
private runTransactionCommand;
|
|
75
86
|
}
|