@c9up/atlas 0.2.5 → 0.2.7
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/db.win32-x64-msvc.node +0 -0
- package/dist/BaseEntity.d.ts +40 -27
- package/dist/BaseEntity.d.ts.map +1 -1
- package/dist/BaseEntity.js +82 -81
- package/dist/BaseEntity.js.map +1 -1
- package/dist/BaseModel.d.ts +10 -0
- package/dist/BaseModel.d.ts.map +1 -1
- package/dist/BaseModel.js +10 -0
- package/dist/BaseModel.js.map +1 -1
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +31 -5
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ConnectionManager.d.ts.map +1 -1
- package/dist/ConnectionManager.js +22 -1
- package/dist/ConnectionManager.js.map +1 -1
- package/dist/ModelQuery.d.ts +22 -1
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +43 -20
- package/dist/ModelQuery.js.map +1 -1
- package/dist/Transaction.d.ts +9 -0
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +14 -0
- package/dist/Transaction.js.map +1 -1
- package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
- package/dist/adapters/NapiDbAdapter.js +7 -0
- package/dist/adapters/NapiDbAdapter.js.map +1 -1
- package/dist/decorators/entity.d.ts +4 -0
- package/dist/decorators/entity.d.ts.map +1 -1
- package/dist/decorators/entity.js +9 -1
- package/dist/decorators/entity.js.map +1 -1
- package/dist/decorators/hooks.d.ts +28 -0
- package/dist/decorators/hooks.d.ts.map +1 -1
- package/dist/decorators/hooks.js +43 -0
- package/dist/decorators/hooks.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/query/DatabaseQueryBuilder.d.ts +24 -0
- package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -1
- package/dist/query/DatabaseQueryBuilder.js +98 -3
- package/dist/query/DatabaseQueryBuilder.js.map +1 -1
- package/dist/schema/SchemaBuilder.d.ts +2 -1
- package/dist/schema/SchemaBuilder.d.ts.map +1 -1
- package/dist/schema/SchemaBuilder.js +2 -1
- package/dist/schema/SchemaBuilder.js.map +1 -1
- package/dist/schema/TableBuilder.d.ts +140 -132
- package/dist/schema/TableBuilder.d.ts.map +1 -1
- package/dist/schema/TableBuilder.js +232 -231
- package/dist/schema/TableBuilder.js.map +1 -1
- package/dist/services/db.d.ts +7 -0
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/db.js.map +1 -1
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +4 -4
- package/src/BaseEntity.ts +115 -85
- package/src/BaseModel.ts +10 -0
- package/src/BaseRepository.ts +36 -4
- package/src/ConnectionManager.ts +26 -1
- package/src/ModelQuery.ts +73 -23
- package/src/Transaction.ts +23 -0
- package/src/adapters/NapiDbAdapter.ts +7 -0
- package/src/decorators/entity.ts +13 -1
- package/src/decorators/hooks.ts +67 -0
- package/src/index.ts +8 -1
- package/src/query/DatabaseQueryBuilder.ts +118 -7
- package/src/schema/SchemaBuilder.ts +2 -1
- package/src/schema/TableBuilder.ts +331 -303
- package/src/services/db.ts +7 -0
package/src/ModelQuery.ts
CHANGED
|
@@ -2231,41 +2231,77 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2231
2231
|
|
|
2232
2232
|
// --- Top-level scalar executors (Story 29.5) ---
|
|
2233
2233
|
|
|
2234
|
-
/**
|
|
2235
|
-
|
|
2234
|
+
/**
|
|
2235
|
+
* `COUNT` — a terminal scalar, or a chainable projection when given an
|
|
2236
|
+
* alias.
|
|
2237
|
+
*
|
|
2238
|
+
* Lucid types this `count: Aggregate<this>`: `count('* as total')` returns
|
|
2239
|
+
* the BUILDER and the value lands in `$extras`. The no-alias form returning
|
|
2240
|
+
* a number is atlas's own, and matches what `DatabaseQueryBuilder` already
|
|
2241
|
+
* does — same method, same two shapes, on both builders.
|
|
2242
|
+
*/
|
|
2243
|
+
count(aliasExpr: `${string} as ${string}`): this;
|
|
2244
|
+
count(column?: string): Promise<number>;
|
|
2245
|
+
count(column?: string): Promise<number> | this {
|
|
2246
|
+
if (column !== undefined && ALIASED.test(column)) {
|
|
2247
|
+
return this.#projectAggregate("COUNT", column);
|
|
2248
|
+
}
|
|
2249
|
+
const col = column ?? "*";
|
|
2236
2250
|
const expr =
|
|
2237
|
-
|
|
2251
|
+
col === "*"
|
|
2238
2252
|
? "COUNT(*)"
|
|
2239
|
-
: `COUNT(${this.#quoteCol(this.#resolveColumn(
|
|
2240
|
-
return
|
|
2253
|
+
: `COUNT(${this.#quoteCol(this.#resolveColumn(col))})`;
|
|
2254
|
+
return this.#runScalar(expr).then((v) => Number(v ?? 0));
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
/** Project `FN(col) AS alias` and keep chaining — the Lucid `Aggregate` form. */
|
|
2258
|
+
#projectAggregate(fn: string, aliasExpr: string): this {
|
|
2259
|
+
const match = ALIASED.exec(aliasExpr.trim());
|
|
2260
|
+
const column = (match?.[1] ?? "").trim();
|
|
2261
|
+
const alias = match?.[2] ?? "";
|
|
2262
|
+
const inner =
|
|
2263
|
+
column === "*" ? "*" : this.#quoteCol(this.#resolveColumn(column));
|
|
2264
|
+
this.#selectRaw.push({
|
|
2265
|
+
sql: `${fn}(${inner}) AS ${this.#quoteAliasName(alias)}`,
|
|
2266
|
+
params: [],
|
|
2267
|
+
});
|
|
2268
|
+
return this;
|
|
2241
2269
|
}
|
|
2242
2270
|
|
|
2243
|
-
|
|
2244
|
-
|
|
2271
|
+
sum(aliasExpr: `${string} as ${string}`): this;
|
|
2272
|
+
sum(column: string): Promise<number | null>;
|
|
2273
|
+
sum(column: string): Promise<number | null> | this {
|
|
2274
|
+
if (ALIASED.test(column)) return this.#projectAggregate("SUM", column);
|
|
2275
|
+
return this.#runScalar(
|
|
2245
2276
|
`SUM(${this.#quoteCol(this.#resolveColumn(column))})`,
|
|
2246
|
-
);
|
|
2247
|
-
return v === null || v === undefined ? null : Number(v);
|
|
2277
|
+
).then((v) => (v === null || v === undefined ? null : Number(v)));
|
|
2248
2278
|
}
|
|
2249
2279
|
|
|
2250
|
-
|
|
2251
|
-
|
|
2280
|
+
avg(aliasExpr: `${string} as ${string}`): this;
|
|
2281
|
+
avg(column: string): Promise<number | null>;
|
|
2282
|
+
avg(column: string): Promise<number | null> | this {
|
|
2283
|
+
if (ALIASED.test(column)) return this.#projectAggregate("AVG", column);
|
|
2284
|
+
return this.#runScalar(
|
|
2252
2285
|
`AVG(${this.#quoteCol(this.#resolveColumn(column))})`,
|
|
2253
|
-
);
|
|
2254
|
-
return v === null || v === undefined ? null : Number(v);
|
|
2286
|
+
).then((v) => (v === null || v === undefined ? null : Number(v)));
|
|
2255
2287
|
}
|
|
2256
2288
|
|
|
2257
|
-
|
|
2258
|
-
|
|
2289
|
+
min(aliasExpr: `${string} as ${string}`): this;
|
|
2290
|
+
min(column: string): Promise<number | null>;
|
|
2291
|
+
min(column: string): Promise<number | null> | this {
|
|
2292
|
+
if (ALIASED.test(column)) return this.#projectAggregate("MIN", column);
|
|
2293
|
+
return this.#runScalar(
|
|
2259
2294
|
`MIN(${this.#quoteCol(this.#resolveColumn(column))})`,
|
|
2260
|
-
);
|
|
2261
|
-
return v === null || v === undefined ? null : Number(v);
|
|
2295
|
+
).then((v) => (v === null || v === undefined ? null : Number(v)));
|
|
2262
2296
|
}
|
|
2263
2297
|
|
|
2264
|
-
|
|
2265
|
-
|
|
2298
|
+
max(aliasExpr: `${string} as ${string}`): this;
|
|
2299
|
+
max(column: string): Promise<number | null>;
|
|
2300
|
+
max(column: string): Promise<number | null> | this {
|
|
2301
|
+
if (ALIASED.test(column)) return this.#projectAggregate("MAX", column);
|
|
2302
|
+
return this.#runScalar(
|
|
2266
2303
|
`MAX(${this.#quoteCol(this.#resolveColumn(column))})`,
|
|
2267
|
-
);
|
|
2268
|
-
return v === null || v === undefined ? null : Number(v);
|
|
2304
|
+
).then((v) => (v === null || v === undefined ? null : Number(v)));
|
|
2269
2305
|
}
|
|
2270
2306
|
|
|
2271
2307
|
/**
|
|
@@ -4589,6 +4625,14 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4589
4625
|
cursor?: string;
|
|
4590
4626
|
limit: number;
|
|
4591
4627
|
orderBy: string | string[];
|
|
4628
|
+
/**
|
|
4629
|
+
* Which way the cursor walks. Default `asc`.
|
|
4630
|
+
*
|
|
4631
|
+
* `desc` is what a newest-first feed needs — the common case for cursor
|
|
4632
|
+
* pagination. The direction was hardcoded to `asc`, and any `orderBy`
|
|
4633
|
+
* already set on the query was overwritten by it without a word.
|
|
4634
|
+
*/
|
|
4635
|
+
direction?: "asc" | "desc";
|
|
4592
4636
|
}): Promise<{ items: T[]; nextCursor: string | null; hasMore: boolean }> {
|
|
4593
4637
|
// Keep BOTH forms: `props` (model property names) to read the cursor value
|
|
4594
4638
|
// off the hydrated entity, and `cols` (resolved DB columns) for the SQL
|
|
@@ -4600,6 +4644,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4600
4644
|
if (cols.length === 0)
|
|
4601
4645
|
throw new Error("cursorPaginate requires at least one orderBy column");
|
|
4602
4646
|
const lim = Math.max(1, Math.floor(opts.limit));
|
|
4647
|
+
const descending = opts.direction === "desc";
|
|
4603
4648
|
const clone = this.clone();
|
|
4604
4649
|
|
|
4605
4650
|
if (opts.cursor) {
|
|
@@ -4626,7 +4671,9 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4626
4671
|
for (let i = 0; i < cols.length; i++) {
|
|
4627
4672
|
q.orWhere((inner) => {
|
|
4628
4673
|
for (let j = 0; j < i; j++) inner.where(cols[j], decoded.v[j]);
|
|
4629
|
-
|
|
4674
|
+
// The comparison has to follow the walk: `>` reads forward
|
|
4675
|
+
// through an ascending order, `<` through a descending one.
|
|
4676
|
+
inner.where(cols[i], descending ? "<" : ">", decoded.v[i]);
|
|
4630
4677
|
});
|
|
4631
4678
|
}
|
|
4632
4679
|
});
|
|
@@ -4634,7 +4681,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4634
4681
|
|
|
4635
4682
|
clone.#orderBys = cols.map((column) => ({
|
|
4636
4683
|
column,
|
|
4637
|
-
direction: "asc" as const,
|
|
4684
|
+
direction: descending ? ("desc" as const) : ("asc" as const),
|
|
4638
4685
|
}));
|
|
4639
4686
|
clone.#limit = lim + 1;
|
|
4640
4687
|
// `#doExec` (not `exec`) — cursorPaginate is an atlas-specific terminal, not a
|
|
@@ -5485,3 +5532,6 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
5485
5532
|
}
|
|
5486
5533
|
}
|
|
5487
5534
|
}
|
|
5535
|
+
|
|
5536
|
+
/** `col as alias` — the Lucid aggregate spelling. */
|
|
5537
|
+
const ALIASED = /^(.*?)\s+as\s+(\S+)$/i;
|
package/src/Transaction.ts
CHANGED
|
@@ -26,6 +26,15 @@ export interface TransactionClient
|
|
|
26
26
|
TransactionQueryBuilders {
|
|
27
27
|
commit(): Promise<void>;
|
|
28
28
|
rollback(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Whether this transaction has already committed or rolled back (Lucid
|
|
31
|
+
* `trx.isCompleted`).
|
|
32
|
+
*
|
|
33
|
+
* A helper handed a transaction cannot otherwise tell whether it is still
|
|
34
|
+
* usable, and issuing a statement on a finished one fails at the driver with
|
|
35
|
+
* an error that says nothing about the transaction.
|
|
36
|
+
*/
|
|
37
|
+
readonly isCompleted: boolean;
|
|
29
38
|
/**
|
|
30
39
|
* Register a side effect to run AFTER the transaction is durable (Lucid
|
|
31
40
|
* `trx.after('commit' | 'rollback', cb)`). A `commit` hook fires only once the
|
|
@@ -127,17 +136,25 @@ export async function openSavepoint(
|
|
|
127
136
|
const commitHooks: AfterHook[] = [];
|
|
128
137
|
const rollbackHooks: AfterHook[] = [];
|
|
129
138
|
const evt = makeTrxEvents();
|
|
139
|
+
// Flipped by whichever of commit/rollback runs first, so a helper handed
|
|
140
|
+
// this client can tell whether it is still usable (Lucid `isCompleted`).
|
|
141
|
+
let completed = false;
|
|
130
142
|
const base = {
|
|
131
143
|
execute: parent.execute.bind(parent),
|
|
132
144
|
query: parent.query.bind(parent),
|
|
145
|
+
get isCompleted(): boolean {
|
|
146
|
+
return completed;
|
|
147
|
+
},
|
|
133
148
|
async commit(): Promise<void> {
|
|
134
149
|
await parent.execute(`RELEASE SAVEPOINT ${name}`, []);
|
|
150
|
+
completed = true;
|
|
135
151
|
evt.emit("commit"); // synchronous EventEmitter notification (this savepoint)
|
|
136
152
|
for (const hook of commitHooks) parent.after("commit", hook);
|
|
137
153
|
for (const hook of rollbackHooks) parent.after("rollback", hook);
|
|
138
154
|
},
|
|
139
155
|
async rollback(): Promise<void> {
|
|
140
156
|
await parent.execute(`ROLLBACK TO SAVEPOINT ${name}`, []);
|
|
157
|
+
completed = true;
|
|
141
158
|
try {
|
|
142
159
|
await parent.execute(`RELEASE SAVEPOINT ${name}`, []);
|
|
143
160
|
} catch {
|
|
@@ -250,16 +267,22 @@ export async function transaction<T>(
|
|
|
250
267
|
const rollbackHooks: AfterHook[] = [];
|
|
251
268
|
const evt = makeTrxEvents();
|
|
252
269
|
|
|
270
|
+
let completed = false;
|
|
253
271
|
const base = {
|
|
254
272
|
execute: db.execute.bind(db),
|
|
255
273
|
query: db.query.bind(db),
|
|
274
|
+
get isCompleted(): boolean {
|
|
275
|
+
return completed;
|
|
276
|
+
},
|
|
256
277
|
async commit() {
|
|
257
278
|
await db.execute("COMMIT", []);
|
|
279
|
+
completed = true;
|
|
258
280
|
evt.emit("commit"); // synchronous EventEmitter notification
|
|
259
281
|
await runAfterHooks(commitHooks);
|
|
260
282
|
},
|
|
261
283
|
async rollback() {
|
|
262
284
|
await db.execute("ROLLBACK", []);
|
|
285
|
+
completed = true;
|
|
263
286
|
evt.emit("rollback");
|
|
264
287
|
await runAfterHooks(rollbackHooks);
|
|
265
288
|
},
|
|
@@ -291,6 +291,8 @@ export async function createNapiConnection(
|
|
|
291
291
|
const native = await db.begin(isolationLevel);
|
|
292
292
|
// Root (non-nested) transaction: after-hooks fire once the underlying
|
|
293
293
|
// COMMIT / ROLLBACK is durable (Lucid `trx.after(...)`), errors swallowed.
|
|
294
|
+
// Flipped by whichever of commit/rollback runs first (Lucid isCompleted).
|
|
295
|
+
let completed = false;
|
|
294
296
|
const commitHooks: AfterHook[] = [];
|
|
295
297
|
const rollbackHooks: AfterHook[] = [];
|
|
296
298
|
const evt = makeTrxEvents();
|
|
@@ -320,13 +322,18 @@ export async function createNapiConnection(
|
|
|
320
322
|
);
|
|
321
323
|
return JSON.parse(json, napiReviver) as T[];
|
|
322
324
|
},
|
|
325
|
+
get isCompleted(): boolean {
|
|
326
|
+
return completed;
|
|
327
|
+
},
|
|
323
328
|
async commit(): Promise<void> {
|
|
324
329
|
await native.commit();
|
|
330
|
+
completed = true;
|
|
325
331
|
evt.emit("commit"); // synchronous EventEmitter notification (Lucid trx.on)
|
|
326
332
|
await runAfterHooks(commitHooks);
|
|
327
333
|
},
|
|
328
334
|
async rollback(): Promise<void> {
|
|
329
335
|
await native.rollback();
|
|
336
|
+
completed = true;
|
|
330
337
|
evt.emit("rollback");
|
|
331
338
|
await runAfterHooks(rollbackHooks);
|
|
332
339
|
},
|
package/src/decorators/entity.ts
CHANGED
|
@@ -125,8 +125,12 @@ export interface ManyToManyOptions {
|
|
|
125
125
|
pivotTable: string;
|
|
126
126
|
/** Foreign key in the pivot table pointing to THIS entity (default: `${thisTable}_id`). */
|
|
127
127
|
foreignKey?: string;
|
|
128
|
+
/** AdonisJS spelling of {@link foreignKey}. Both are accepted. */
|
|
129
|
+
pivotForeignKey?: string;
|
|
128
130
|
/** Foreign key in the pivot table pointing to the RELATED entity (default: `${relatedTable}_id`). */
|
|
129
131
|
otherKey?: string;
|
|
132
|
+
/** AdonisJS spelling of {@link otherKey}. Both are accepted. */
|
|
133
|
+
pivotRelatedForeignKey?: string;
|
|
130
134
|
/**
|
|
131
135
|
* The column ON THE RELATED model that `otherKey` references (Adonis Lucid
|
|
132
136
|
* `relatedKey`). Defaults to the related model's primary key; override when the
|
|
@@ -533,7 +537,15 @@ export function ManyToMany(
|
|
|
533
537
|
propertyKey: String(propertyKey),
|
|
534
538
|
type: "manyToMany",
|
|
535
539
|
target,
|
|
536
|
-
|
|
540
|
+
// Normalised HERE, once: AdonisJS names these `pivotForeignKey` /
|
|
541
|
+
// `pivotRelatedForeignKey`. Folding them in at the decorator means the
|
|
542
|
+
// half-dozen places that read the pivot keys never learn there are two
|
|
543
|
+
// spellings — and cannot each forget one.
|
|
544
|
+
pivot: {
|
|
545
|
+
...options,
|
|
546
|
+
foreignKey: options.foreignKey ?? options.pivotForeignKey,
|
|
547
|
+
otherKey: options.otherKey ?? options.pivotRelatedForeignKey,
|
|
548
|
+
},
|
|
537
549
|
// `localKey` selects which parent column the pivot FK references
|
|
538
550
|
// (default: the parent PK). Without copying it here the value the type
|
|
539
551
|
// accepts is silently dropped and the pivot always targets the PK.
|
package/src/decorators/hooks.ts
CHANGED
|
@@ -59,6 +59,24 @@ export interface HookArgs {
|
|
|
59
59
|
afterPaginate: Paginator<BaseEntity>;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* The event half of a hook kind — what `Model.before(event)` takes. Lucid
|
|
64
|
+
* spells these lower-case (`'save'`, `'create'`) and prefixes them itself.
|
|
65
|
+
*/
|
|
66
|
+
export type HookEvent =
|
|
67
|
+
| "save"
|
|
68
|
+
| "create"
|
|
69
|
+
| "update"
|
|
70
|
+
| "delete"
|
|
71
|
+
| "find"
|
|
72
|
+
| "fetch"
|
|
73
|
+
| "paginate";
|
|
74
|
+
|
|
75
|
+
/** `'save'` → `'Save'`, so `before` + event names the registry key. */
|
|
76
|
+
export function capitalize<E extends string>(event: E): Capitalize<E> {
|
|
77
|
+
return (event.charAt(0).toUpperCase() + event.slice(1)) as Capitalize<E>;
|
|
78
|
+
}
|
|
79
|
+
|
|
62
80
|
/** A hook handler is a static function that receives the kind-specific arg. */
|
|
63
81
|
export type HookHandler<K extends HookKind = HookKind> = (
|
|
64
82
|
arg: HookArgs[K],
|
|
@@ -169,3 +187,52 @@ export async function fireHooks<K extends HookKind>(
|
|
|
169
187
|
await handler(arg);
|
|
170
188
|
}
|
|
171
189
|
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Register a hook at RUNTIME, without a decorator — Lucid's
|
|
193
|
+
* `Model.before(event, handler)` / `Model.after(event, handler)`, which do
|
|
194
|
+
* `this.$hooks.add(\`before:${event}\`, handler)`.
|
|
195
|
+
*
|
|
196
|
+
* The decorators cover the common case (a static method on the entity), but a
|
|
197
|
+
* hook that comes from somewhere else — a plugin, a test, a package wiring
|
|
198
|
+
* itself into an app's models — has no class body to decorate. Lucid supports
|
|
199
|
+
* both; so does this.
|
|
200
|
+
*
|
|
201
|
+
* Handlers land in the SAME per-class registry the decorators write to, so the
|
|
202
|
+
* ordering rule is unchanged: parent classes fire before child classes, and
|
|
203
|
+
* within a class, registration order.
|
|
204
|
+
*/
|
|
205
|
+
export function addHook<K extends HookKind>(
|
|
206
|
+
entityClass: object,
|
|
207
|
+
kind: K,
|
|
208
|
+
handler: HookHandler<K>,
|
|
209
|
+
): void {
|
|
210
|
+
if (!isHookHandler(handler)) {
|
|
211
|
+
throw new Error(`${kind} hook must be a function`);
|
|
212
|
+
}
|
|
213
|
+
const registry = getOwnRegistry(entityClass);
|
|
214
|
+
const list: HookHandler[] = registry[kind] ?? [];
|
|
215
|
+
list.push(handler as HookHandler);
|
|
216
|
+
registry[kind] = list;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Drop a handler registered with {@link addHook} (or a decorator, if you hold
|
|
221
|
+
* the same reference). Returns whether one was removed — a test that adds a
|
|
222
|
+
* hook needs a way to take it back out.
|
|
223
|
+
*/
|
|
224
|
+
export function removeHook<K extends HookKind>(
|
|
225
|
+
entityClass: object,
|
|
226
|
+
kind: K,
|
|
227
|
+
handler: HookHandler<K>,
|
|
228
|
+
): boolean {
|
|
229
|
+
const registry = Reflect.getOwnMetadata(HOOKS_KEY, entityClass) as
|
|
230
|
+
| HookRegistry
|
|
231
|
+
| undefined;
|
|
232
|
+
const list = registry?.[kind];
|
|
233
|
+
if (!list) return false;
|
|
234
|
+
const index = list.indexOf(handler as HookHandler);
|
|
235
|
+
if (index === -1) return false;
|
|
236
|
+
list.splice(index, 1);
|
|
237
|
+
return true;
|
|
238
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -99,7 +99,10 @@ export {
|
|
|
99
99
|
PrimaryKey,
|
|
100
100
|
SoftDeletes,
|
|
101
101
|
} from "./decorators/entity.js";
|
|
102
|
+
// Runtime hook registration — what `BaseEntity.before()/after()` call, exported
|
|
103
|
+
// for a plugin that wires hooks into models it does not own.
|
|
102
104
|
export {
|
|
105
|
+
addHook,
|
|
103
106
|
afterCreate,
|
|
104
107
|
afterDelete,
|
|
105
108
|
afterFetch,
|
|
@@ -114,6 +117,10 @@ export {
|
|
|
114
117
|
beforePaginate,
|
|
115
118
|
beforeSave,
|
|
116
119
|
beforeUpdate,
|
|
120
|
+
type HookEvent,
|
|
121
|
+
type HookHandler,
|
|
122
|
+
type HookKind,
|
|
123
|
+
removeHook,
|
|
117
124
|
} from "./decorators/hooks.js";
|
|
118
125
|
export type { ScopeFn } from "./decorators/scope.js";
|
|
119
126
|
export { scope } from "./decorators/scope.js";
|
|
@@ -173,7 +180,7 @@ export type {
|
|
|
173
180
|
export { MigrationRunner } from "./schema/MigrationRunner.js";
|
|
174
181
|
export type { DefaultValue } from "./schema/raw.js";
|
|
175
182
|
export type { ColumnDefinition, ColumnType } from "./schema/SchemaBuilder.js";
|
|
176
|
-
export { Schema, TableBuilder } from "./schema/SchemaBuilder.js";
|
|
183
|
+
export { ColumnBuilder, Schema, TableBuilder } from "./schema/SchemaBuilder.js";
|
|
177
184
|
export {
|
|
178
185
|
checkSchema,
|
|
179
186
|
formatSchemaFindings,
|
|
@@ -890,6 +890,59 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
|
|
|
890
890
|
return this;
|
|
891
891
|
}
|
|
892
892
|
|
|
893
|
+
// ─── HAVING EXISTS (Lucid/Knex) ───────────────────────────
|
|
894
|
+
// A correlated subquery in HAVING is how you filter GROUPS by something
|
|
895
|
+
// outside the aggregate — "groups that still have an open order", which no
|
|
896
|
+
// combination of `having(count)` can express.
|
|
897
|
+
|
|
898
|
+
/** HAVING EXISTS (subquery). */
|
|
899
|
+
havingExists(sub: SubqueryArg): this {
|
|
900
|
+
return this.#pushHavingExists("and", false, sub);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/** Alias of {@link havingExists} — AND is the default. */
|
|
904
|
+
andHavingExists(sub: SubqueryArg): this {
|
|
905
|
+
return this.#pushHavingExists("and", false, sub);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** OR EXISTS in HAVING. */
|
|
909
|
+
orHavingExists(sub: SubqueryArg): this {
|
|
910
|
+
return this.#pushHavingExists("or", false, sub);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** HAVING NOT EXISTS (subquery). */
|
|
914
|
+
havingNotExists(sub: SubqueryArg): this {
|
|
915
|
+
return this.#pushHavingExists("and", true, sub);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
/** Alias of {@link havingNotExists}. */
|
|
919
|
+
andHavingNotExists(sub: SubqueryArg): this {
|
|
920
|
+
return this.#pushHavingExists("and", true, sub);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** OR NOT EXISTS in HAVING. */
|
|
924
|
+
orHavingNotExists(sub: SubqueryArg): this {
|
|
925
|
+
return this.#pushHavingExists("or", true, sub);
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
#pushHavingExists(
|
|
929
|
+
type: "and" | "or",
|
|
930
|
+
negated: boolean,
|
|
931
|
+
sub: SubqueryArg,
|
|
932
|
+
): this {
|
|
933
|
+
// Compiled to a raw HAVING rather than a new clause kind: the engine
|
|
934
|
+
// already carries raw havings with their bindings, and EXISTS has no
|
|
935
|
+
// operator/value to model.
|
|
936
|
+
const { sql, params } = this.#resolveSub(sub).#compiledNative();
|
|
937
|
+
this.#havings.push({
|
|
938
|
+
kind: "raw",
|
|
939
|
+
sql: `${negated ? "NOT EXISTS" : "EXISTS"} (${sql})`,
|
|
940
|
+
bindings: params,
|
|
941
|
+
type,
|
|
942
|
+
});
|
|
943
|
+
return this;
|
|
944
|
+
}
|
|
945
|
+
|
|
893
946
|
/** `INNER JOIN` — alias of {@link innerJoin} (Lucid/Knex `join`). */
|
|
894
947
|
join(table: string, left: string, right: string): this;
|
|
895
948
|
join(table: string, left: string, operator: string, right: string): this;
|
|
@@ -1980,6 +2033,47 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
|
|
|
1980
2033
|
return this;
|
|
1981
2034
|
}
|
|
1982
2035
|
|
|
2036
|
+
// ─── Clearing clauses (Lucid/Knex `clear*`) ───────────────
|
|
2037
|
+
// A builder handed to a helper that added a filter, an order or a slice can
|
|
2038
|
+
// be reset clause by clause instead of rebuilt from scratch — which is what
|
|
2039
|
+
// a shared scope or a reusable base query needs.
|
|
2040
|
+
|
|
2041
|
+
/** Drop every selected column, back to `*`. */
|
|
2042
|
+
clearSelect(): this {
|
|
2043
|
+
this.#selects = [];
|
|
2044
|
+
return this;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
2047
|
+
/** Drop every WHERE, including the raw and grouped ones. */
|
|
2048
|
+
clearWhere(): this {
|
|
2049
|
+
this.#wheres = [];
|
|
2050
|
+
return this;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
/** Drop every ORDER BY. */
|
|
2054
|
+
clearOrder(): this {
|
|
2055
|
+
this.#orderBys = [];
|
|
2056
|
+
return this;
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
/** Drop every HAVING. */
|
|
2060
|
+
clearHaving(): this {
|
|
2061
|
+
this.#havings = [];
|
|
2062
|
+
return this;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/** Drop the LIMIT. */
|
|
2066
|
+
clearLimit(): this {
|
|
2067
|
+
this.#limit = undefined;
|
|
2068
|
+
return this;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/** Drop the OFFSET. */
|
|
2072
|
+
clearOffset(): this {
|
|
2073
|
+
this.#offset = undefined;
|
|
2074
|
+
return this;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
1983
2077
|
/** The table, qualified with a schema when {@link withSchema} was used. */
|
|
1984
2078
|
#qualifiedTable(): string {
|
|
1985
2079
|
return this.#schema ? `${this.#schema}.${this.#table}` : this.#table;
|
|
@@ -2672,19 +2766,36 @@ export class DatabaseQueryBuilder<T = Record<string, unknown>> {
|
|
|
2672
2766
|
},
|
|
2673
2767
|
this.#dialect,
|
|
2674
2768
|
);
|
|
2769
|
+
// A flat `SELECT COUNT(*) … GROUP BY x` returns one row PER GROUP, each
|
|
2770
|
+
// holding that group's size — so `rows[0]` would be the first group's
|
|
2771
|
+
// size, not the number of rows. Wrap the grouped query and count ITS
|
|
2772
|
+
// rows, which is what ModelQuery.paginate already does.
|
|
2773
|
+
const countSql =
|
|
2774
|
+
this.#groupBys.length > 0
|
|
2775
|
+
? `SELECT COUNT(*) AS aggregate FROM (${this.#compiledNative().sql}) AS __paginate_count`
|
|
2776
|
+
: countCompiled.statements[0];
|
|
2777
|
+
const countParams =
|
|
2778
|
+
this.#groupBys.length > 0
|
|
2779
|
+
? this.#compiledNative().params
|
|
2780
|
+
: countCompiled.params;
|
|
2675
2781
|
const countRows = await this.#exec.query<{
|
|
2676
2782
|
aggregate: number | string | null;
|
|
2677
|
-
}>(
|
|
2678
|
-
countCompiled.statements[0],
|
|
2679
|
-
countCompiled.params,
|
|
2680
|
-
this.#queryMeta("paginate"),
|
|
2681
|
-
);
|
|
2783
|
+
}>(countSql, countParams, this.#queryMeta("paginate"));
|
|
2682
2784
|
const total = Number(countRows[0]?.aggregate ?? 0);
|
|
2683
2785
|
|
|
2786
|
+
// Restore the slice afterwards: `paginate()` is a read, and a builder
|
|
2787
|
+
// left carrying a LIMIT silently truncated the next query run off it.
|
|
2788
|
+
const previousLimit = this.#limit;
|
|
2789
|
+
const previousOffset = this.#offset;
|
|
2684
2790
|
this.#limit = pp;
|
|
2685
2791
|
this.#offset = (p - 1) * pp;
|
|
2686
|
-
|
|
2687
|
-
|
|
2792
|
+
try {
|
|
2793
|
+
const items = await this.exec();
|
|
2794
|
+
return new Paginator<T>(items, { total, perPage: pp, currentPage: p });
|
|
2795
|
+
} finally {
|
|
2796
|
+
this.#limit = previousLimit;
|
|
2797
|
+
this.#offset = previousOffset;
|
|
2798
|
+
}
|
|
2688
2799
|
}
|
|
2689
2800
|
|
|
2690
2801
|
/** Thenable, so `await db.from('users').where(...)` resolves to the rows. */
|
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
* The classes were split into one-class-per-file:
|
|
5
5
|
* - {@link Schema} → ./Schema.ts
|
|
6
6
|
* - {@link TableBuilder} → ./TableBuilder.ts
|
|
7
|
+
* - {@link ColumnBuilder} → ./TableBuilder.ts
|
|
7
8
|
* - shared types → ./types.ts
|
|
8
9
|
*
|
|
9
10
|
* @implements FR34
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
export { Schema } from "./Schema.js";
|
|
13
|
-
export { TableBuilder } from "./TableBuilder.js";
|
|
14
|
+
export { ColumnBuilder, TableBuilder } from "./TableBuilder.js";
|
|
14
15
|
export type { ColumnDefinition, ColumnType, IndexDefinition } from "./types.js";
|