@shaferllc/keel 0.80.0 → 0.81.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/accounts/accounts.config.stub +50 -0
- package/dist/accounts/config.d.ts +46 -0
- package/dist/accounts/config.js +39 -0
- package/dist/accounts/flows.d.ts +50 -0
- package/dist/accounts/flows.js +133 -0
- package/dist/accounts/index.d.ts +28 -0
- package/dist/accounts/index.js +23 -0
- package/dist/accounts/migration.d.ts +14 -0
- package/dist/accounts/migration.js +39 -0
- package/dist/accounts/provider.d.ts +18 -0
- package/dist/accounts/provider.js +37 -0
- package/dist/accounts/routes.d.ts +15 -0
- package/dist/accounts/routes.js +116 -0
- package/dist/accounts/store.d.ts +33 -0
- package/dist/accounts/store.js +37 -0
- package/dist/accounts/tokens.d.ts +60 -0
- package/dist/accounts/tokens.js +116 -0
- package/dist/accounts/totp.d.ts +58 -0
- package/dist/accounts/totp.js +134 -0
- package/dist/accounts/two-factor.d.ts +56 -0
- package/dist/accounts/two-factor.js +146 -0
- package/dist/core/database.d.ts +36 -0
- package/dist/core/database.js +141 -4
- package/dist/core/index.d.ts +5 -2
- package/dist/core/index.js +3 -2
- package/dist/core/migrations.d.ts +52 -2
- package/dist/core/migrations.js +134 -3
- package/dist/core/model-events.d.ts +34 -0
- package/dist/core/model-events.js +71 -0
- package/dist/core/model-query.d.ts +68 -0
- package/dist/core/model-query.js +234 -0
- package/dist/core/model.d.ts +91 -4
- package/dist/core/model.js +217 -32
- package/dist/core/relations.d.ts +53 -0
- package/dist/core/relations.js +242 -0
- package/docs/accounts.md +214 -0
- package/docs/ai-manifest.json +63 -1
- package/docs/database.md +33 -0
- package/docs/examples/accounts.ts +150 -0
- package/docs/migrations.md +32 -3
- package/docs/models.md +133 -3
- package/llms-full.txt +419 -6
- package/llms.txt +2 -0
- package/package.json +6 -2
package/dist/core/database.js
CHANGED
|
@@ -122,6 +122,10 @@ export class QueryBuilder {
|
|
|
122
122
|
columns = "*";
|
|
123
123
|
_limit;
|
|
124
124
|
_offset;
|
|
125
|
+
joins = [];
|
|
126
|
+
groups = [];
|
|
127
|
+
havings = [];
|
|
128
|
+
_distinct = false;
|
|
125
129
|
// The connection is resolved lazily, when a query actually runs — so building
|
|
126
130
|
// a query never throws, and an unregistered connection surfaces as a rejected
|
|
127
131
|
// read/write rather than a synchronous error at construction.
|
|
@@ -177,10 +181,57 @@ export class QueryBuilder {
|
|
|
177
181
|
this.wheres.push({ boolean: "AND", sql: `${column} LIKE ?`, bindings: [pattern] });
|
|
178
182
|
return this;
|
|
179
183
|
}
|
|
184
|
+
/** Compare two columns (no binding): `whereColumn("updated_at", ">", "created_at")`. */
|
|
185
|
+
whereColumn(first, opOrSecond, second) {
|
|
186
|
+
const [op, col] = second === undefined ? ["=", opOrSecond] : [opOrSecond, second];
|
|
187
|
+
this.wheres.push({ boolean: "AND", sql: `${first} ${op} ${col}`, bindings: [] });
|
|
188
|
+
return this;
|
|
189
|
+
}
|
|
190
|
+
/** A raw WHERE fragment with its own bindings. */
|
|
191
|
+
whereRaw(sql, bindings = []) {
|
|
192
|
+
this.wheres.push({ boolean: "AND", sql, bindings });
|
|
193
|
+
return this;
|
|
194
|
+
}
|
|
195
|
+
join(table, first, opOrSecond, second) {
|
|
196
|
+
const [op, col] = second === undefined ? ["=", opOrSecond] : [opOrSecond, second];
|
|
197
|
+
this.joins.push(`INNER JOIN ${table} ON ${first} ${op} ${col}`);
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
leftJoin(table, first, opOrSecond, second) {
|
|
201
|
+
const [op, col] = second === undefined ? ["=", opOrSecond] : [opOrSecond, second];
|
|
202
|
+
this.joins.push(`LEFT JOIN ${table} ON ${first} ${op} ${col}`);
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
groupBy(...columns) {
|
|
206
|
+
this.groups.push(...columns);
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
having(column, opOrValue, value) {
|
|
210
|
+
const [op, val] = value === undefined ? ["=", opOrValue] : [opOrValue, value];
|
|
211
|
+
this.havings.push({ boolean: "AND", sql: `${column} ${op} ?`, bindings: [val] });
|
|
212
|
+
return this;
|
|
213
|
+
}
|
|
214
|
+
distinct() {
|
|
215
|
+
this._distinct = true;
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
/** Apply `then` only when `condition` is truthy (optionally `otherwise`). */
|
|
219
|
+
when(condition, then, otherwise) {
|
|
220
|
+
if (condition)
|
|
221
|
+
then(this, condition);
|
|
222
|
+
else
|
|
223
|
+
otherwise?.(this, condition);
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
180
226
|
orderBy(column, direction = "asc") {
|
|
181
227
|
this.orders.push(`${column} ${direction.toUpperCase()}`);
|
|
182
228
|
return this;
|
|
183
229
|
}
|
|
230
|
+
/** A raw ORDER BY fragment, e.g. `orderByRaw("LENGTH(name) DESC")`. */
|
|
231
|
+
orderByRaw(sql) {
|
|
232
|
+
this.orders.push(sql);
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
184
235
|
/** Newest-first / oldest-first by a timestamp column (default `created_at`). */
|
|
185
236
|
latest(column = "created_at") {
|
|
186
237
|
return this.orderBy(column, "desc");
|
|
@@ -203,17 +254,32 @@ export class QueryBuilder {
|
|
|
203
254
|
this.wheres.map((w, i) => (i === 0 ? "" : `${w.boolean} `) + w.sql).join(" ");
|
|
204
255
|
return { sql, bindings: this.wheres.flatMap((w) => w.bindings) };
|
|
205
256
|
}
|
|
257
|
+
havingClause() {
|
|
258
|
+
if (!this.havings.length)
|
|
259
|
+
return { sql: "", bindings: [] };
|
|
260
|
+
const sql = " HAVING " +
|
|
261
|
+
this.havings.map((w, i) => (i === 0 ? "" : `${w.boolean} `) + w.sql).join(" ");
|
|
262
|
+
return { sql, bindings: this.havings.flatMap((w) => w.bindings) };
|
|
263
|
+
}
|
|
264
|
+
joinSql() {
|
|
265
|
+
return this.joins.length ? ` ${this.joins.join(" ")}` : "";
|
|
266
|
+
}
|
|
206
267
|
/* ------------------------------- reads ------------------------------- */
|
|
207
268
|
async get() {
|
|
208
269
|
const where = this.whereClause();
|
|
209
|
-
|
|
270
|
+
const having = this.havingClause();
|
|
271
|
+
let sql = `SELECT ${this._distinct ? "DISTINCT " : ""}${this.columns} FROM ${this.table}`;
|
|
272
|
+
sql += this.joinSql() + where.sql;
|
|
273
|
+
if (this.groups.length)
|
|
274
|
+
sql += ` GROUP BY ${this.groups.join(", ")}`;
|
|
275
|
+
sql += having.sql;
|
|
210
276
|
if (this.orders.length)
|
|
211
277
|
sql += ` ORDER BY ${this.orders.join(", ")}`;
|
|
212
278
|
if (this._limit != null)
|
|
213
279
|
sql += ` LIMIT ${this._limit}`;
|
|
214
280
|
if (this._offset != null)
|
|
215
281
|
sql += ` OFFSET ${this._offset}`;
|
|
216
|
-
return (await this.runSelect(sql, where.bindings));
|
|
282
|
+
return (await this.runSelect(sql, [...where.bindings, ...having.bindings]));
|
|
217
283
|
}
|
|
218
284
|
async first() {
|
|
219
285
|
this._limit = 1;
|
|
@@ -222,7 +288,7 @@ export class QueryBuilder {
|
|
|
222
288
|
}
|
|
223
289
|
async count() {
|
|
224
290
|
const where = this.whereClause();
|
|
225
|
-
const rows = (await this.runSelect(`SELECT COUNT(*) AS count FROM ${this.table}${where.sql}`, where.bindings));
|
|
291
|
+
const rows = (await this.runSelect(`SELECT COUNT(*) AS count FROM ${this.table}${this.joinSql()}${where.sql}`, where.bindings));
|
|
226
292
|
return Number(rows[0]?.count ?? 0);
|
|
227
293
|
}
|
|
228
294
|
async exists() {
|
|
@@ -230,7 +296,7 @@ export class QueryBuilder {
|
|
|
230
296
|
}
|
|
231
297
|
async aggregate(fn, column) {
|
|
232
298
|
const where = this.whereClause();
|
|
233
|
-
const rows = (await this.runSelect(`SELECT ${fn}(${column}) AS agg FROM ${this.table}${where.sql}`, where.bindings));
|
|
299
|
+
const rows = (await this.runSelect(`SELECT ${fn}(${column}) AS agg FROM ${this.table}${this.joinSql()}${where.sql}`, where.bindings));
|
|
234
300
|
return Number(rows[0]?.agg ?? 0);
|
|
235
301
|
}
|
|
236
302
|
sum(column) {
|
|
@@ -295,6 +361,77 @@ export class QueryBuilder {
|
|
|
295
361
|
const where = this.whereClause();
|
|
296
362
|
return this.runWrite(`DELETE FROM ${this.table}${where.sql}`, where.bindings);
|
|
297
363
|
}
|
|
364
|
+
/** Atomically add to a numeric column (optionally setting other columns too). */
|
|
365
|
+
increment(column, amount = 1, extra = {}) {
|
|
366
|
+
const where = this.whereClause();
|
|
367
|
+
const sets = [`${column} = ${column} + ?`, ...Object.keys(extra).map((k) => `${k} = ?`)];
|
|
368
|
+
const bindings = [amount, ...Object.values(extra), ...where.bindings];
|
|
369
|
+
return this.runWrite(`UPDATE ${this.table} SET ${sets.join(", ")}${where.sql}`, bindings);
|
|
370
|
+
}
|
|
371
|
+
/** Atomically subtract from a numeric column. */
|
|
372
|
+
decrement(column, amount = 1, extra = {}) {
|
|
373
|
+
const where = this.whereClause();
|
|
374
|
+
const sets = [`${column} = ${column} - ?`, ...Object.keys(extra).map((k) => `${k} = ?`)];
|
|
375
|
+
const bindings = [amount, ...Object.values(extra), ...where.bindings];
|
|
376
|
+
return this.runWrite(`UPDATE ${this.table} SET ${sets.join(", ")}${where.sql}`, bindings);
|
|
377
|
+
}
|
|
378
|
+
/** Insert one or more rows, ignoring any that violate a unique constraint. */
|
|
379
|
+
async insertOrIgnore(rows) {
|
|
380
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
381
|
+
if (!list.length)
|
|
382
|
+
return { rowsAffected: 0 };
|
|
383
|
+
const dialect = this.getSource().dialect;
|
|
384
|
+
const keys = Object.keys(list[0]);
|
|
385
|
+
const values = list.map(() => `(${keys.map(() => "?").join(", ")})`).join(", ");
|
|
386
|
+
const bindings = list.flatMap((r) => keys.map((k) => r[k]));
|
|
387
|
+
const verb = dialect === "mysql" ? "INSERT IGNORE" : "INSERT";
|
|
388
|
+
const suffix = dialect === "postgres" || dialect === "sqlite" ? " ON CONFLICT DO NOTHING" : "";
|
|
389
|
+
return this.runWrite(`${verb} INTO ${this.table} (${keys.join(", ")}) VALUES ${values}${suffix}`, bindings);
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Insert rows, updating `update` columns on a conflict against `uniqueBy`.
|
|
393
|
+
* Dialect-aware: `ON CONFLICT … DO UPDATE` (sqlite/postgres) or
|
|
394
|
+
* `ON DUPLICATE KEY UPDATE` (mysql).
|
|
395
|
+
*/
|
|
396
|
+
async upsert(rows, uniqueBy, update) {
|
|
397
|
+
const list = Array.isArray(rows) ? rows : [rows];
|
|
398
|
+
if (!list.length)
|
|
399
|
+
return { rowsAffected: 0 };
|
|
400
|
+
const dialect = this.getSource().dialect;
|
|
401
|
+
const keys = Object.keys(list[0]);
|
|
402
|
+
const cols = update ?? keys.filter((k) => !uniqueBy.includes(k));
|
|
403
|
+
const values = list.map(() => `(${keys.map(() => "?").join(", ")})`).join(", ");
|
|
404
|
+
const bindings = list.flatMap((r) => keys.map((k) => r[k]));
|
|
405
|
+
let sql = `INSERT INTO ${this.table} (${keys.join(", ")}) VALUES ${values}`;
|
|
406
|
+
if (dialect === "mysql") {
|
|
407
|
+
sql += ` ON DUPLICATE KEY UPDATE ${cols.map((c) => `${c} = VALUES(${c})`).join(", ")}`;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
sql += ` ON CONFLICT (${uniqueBy.join(", ")}) DO UPDATE SET ${cols
|
|
411
|
+
.map((c) => `${c} = excluded.${c}`)
|
|
412
|
+
.join(", ")}`;
|
|
413
|
+
}
|
|
414
|
+
return this.runWrite(sql, bindings);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Process results in pages of `size`, so a large table never loads at once.
|
|
418
|
+
* The callback runs per page; return `false` from it to stop early.
|
|
419
|
+
*/
|
|
420
|
+
async chunk(size, callback) {
|
|
421
|
+
let offset = 0;
|
|
422
|
+
for (;;) {
|
|
423
|
+
this._limit = size;
|
|
424
|
+
this._offset = offset;
|
|
425
|
+
const rows = await this.get();
|
|
426
|
+
if (!rows.length)
|
|
427
|
+
return;
|
|
428
|
+
if ((await callback(rows)) === false)
|
|
429
|
+
return;
|
|
430
|
+
if (rows.length < size)
|
|
431
|
+
return;
|
|
432
|
+
offset += size;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
298
435
|
}
|
|
299
436
|
/** Start a query against a table, on the default connection or a named one. */
|
|
300
437
|
export function db(table, connectionName) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -40,8 +40,11 @@ export type { CsrfOptions } from "./csrf.js";
|
|
|
40
40
|
export { db, connection, getConnection, setConnection, addConnection, setDefaultConnection, connectionNames, clearConnections, transaction, inTransaction, QueryBuilder, } from "./database.js";
|
|
41
41
|
export type { Connection, TransactionConnection, TransactionHandle, ConnectionHandle, WriteResult, Row, Dialect, Operator, Paginated, } from "./database.js";
|
|
42
42
|
export { Model } from "./model.js";
|
|
43
|
+
export type { GlobalScope } from "./model.js";
|
|
44
|
+
export { ModelQuery } from "./model-query.js";
|
|
45
|
+
export type { ModelEvent, ModelHook, ModelObserver } from "./model-events.js";
|
|
43
46
|
export type { CastType, Casts } from "./casts.js";
|
|
44
|
-
export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
|
|
47
|
+
export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany, MorphOne, MorphMany, MorphTo, registerMorphType, } from "./relations.js";
|
|
45
48
|
export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
|
|
46
49
|
export type { Definition } from "./factory.js";
|
|
47
50
|
export { Mailer, FakeMailer, PendingMail, BaseMail, SendMailJob, ArrayTransport, LogTransport, fetchTransport, mail, mailer, send, sendLater, setMailer, getMailer, fakeMail, restoreMail, } from "./mail.js";
|
|
@@ -55,7 +58,7 @@ export { Redis, MemoryRedis, redis, setRedis, redisStore } from "./redis.js";
|
|
|
55
58
|
export type { RedisConnection, SetOptions } from "./redis.js";
|
|
56
59
|
export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
|
|
57
60
|
export type { Notifiable, MailContent, Channel } from "./notification.js";
|
|
58
|
-
export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
|
|
61
|
+
export { SchemaBuilder, Migrator, TableBuilder, AlterTableBuilder, ForeignKeyBuilder, Column, } from "./migrations.js";
|
|
59
62
|
export type { Migration } from "./migrations.js";
|
|
60
63
|
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
61
64
|
export { decorateRequest, hasRequestDecorator, decorated, setRequestValue, clearRequestDecorators, } from "./decorators.js";
|
package/dist/core/index.js
CHANGED
|
@@ -22,7 +22,8 @@ export { securityHeaders } from "./shield.js";
|
|
|
22
22
|
export { csrf, csrfToken, csrfField } from "./csrf.js";
|
|
23
23
|
export { db, connection, getConnection, setConnection, addConnection, setDefaultConnection, connectionNames, clearConnections, transaction, inTransaction, QueryBuilder, } from "./database.js";
|
|
24
24
|
export { Model } from "./model.js";
|
|
25
|
-
export {
|
|
25
|
+
export { ModelQuery } from "./model-query.js";
|
|
26
|
+
export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany, MorphOne, MorphMany, MorphTo, registerMorphType, } from "./relations.js";
|
|
26
27
|
export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
|
|
27
28
|
export { Mailer, FakeMailer, PendingMail, BaseMail, SendMailJob, ArrayTransport, LogTransport, fetchTransport, mail, mailer, send, sendLater, setMailer, getMailer, fakeMail, restoreMail, } from "./mail.js";
|
|
28
29
|
export { Job, Queue, FakeQueue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, fakeQueue, restoreQueue, exponentialBackoff, linearBackoff, fixedBackoff, noBackoff, } from "./queue.js";
|
|
@@ -30,7 +31,7 @@ export { Scheduler, ScheduledTask, scheduler, setScheduler, schedule, cronMatche
|
|
|
30
31
|
export { MemoryBroadcaster, broadcast, setBroadcaster, getBroadcaster, channelAuth, authorizeChannel, clearChannels, } from "./broadcasting.js";
|
|
31
32
|
export { Redis, MemoryRedis, redis, setRedis, redisStore } from "./redis.js";
|
|
32
33
|
export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
|
|
33
|
-
export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
|
|
34
|
+
export { SchemaBuilder, Migrator, TableBuilder, AlterTableBuilder, ForeignKeyBuilder, Column, } from "./migrations.js";
|
|
34
35
|
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
35
36
|
export { decorateRequest, hasRequestDecorator, decorated, setRequestValue, clearRequestDecorators, } from "./decorators.js";
|
|
36
37
|
export { View } from "./view.js";
|
|
@@ -31,9 +31,29 @@ export declare class Column {
|
|
|
31
31
|
default(value: unknown): this;
|
|
32
32
|
toSql(dialect: Dialect): string;
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
/** A foreign-key constraint, built fluently: `foreign("user_id").references("id").on("users")`. */
|
|
35
|
+
export declare class ForeignKeyBuilder {
|
|
36
|
+
private column;
|
|
37
|
+
private _refTable;
|
|
38
|
+
private _refColumn;
|
|
39
|
+
private _onDelete?;
|
|
40
|
+
private _onUpdate?;
|
|
41
|
+
constructor(column: string);
|
|
42
|
+
references(column: string): this;
|
|
43
|
+
on(table: string): this;
|
|
44
|
+
onDelete(action: string): this;
|
|
45
|
+
onUpdate(action: string): this;
|
|
46
|
+
toSql(): string;
|
|
47
|
+
}
|
|
48
|
+
interface IndexDef {
|
|
49
|
+
columns: string[];
|
|
50
|
+
unique: boolean;
|
|
51
|
+
name?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Column-declaring methods shared by `createTable` and `alterTable` builders. */
|
|
54
|
+
declare abstract class ColumnBag {
|
|
35
55
|
readonly columns: Column[];
|
|
36
|
-
|
|
56
|
+
protected add(name: string, type: ColumnType, length?: number): Column;
|
|
37
57
|
id(name?: string): Column;
|
|
38
58
|
string(name: string, length?: number): Column;
|
|
39
59
|
text(name: string): Column;
|
|
@@ -44,13 +64,43 @@ export declare class TableBuilder {
|
|
|
44
64
|
json(name: string): Column;
|
|
45
65
|
/** Adds nullable created_at + updated_at columns. */
|
|
46
66
|
timestamps(): void;
|
|
67
|
+
}
|
|
68
|
+
export declare class TableBuilder extends ColumnBag {
|
|
69
|
+
readonly indexes: IndexDef[];
|
|
70
|
+
readonly foreignKeys: ForeignKeyBuilder[];
|
|
71
|
+
/** A (possibly composite) index. */
|
|
72
|
+
index(columns: string | string[], name?: string): this;
|
|
73
|
+
/** A (possibly composite) unique index. */
|
|
74
|
+
uniqueIndex(columns: string | string[], name?: string): this;
|
|
75
|
+
/** A foreign key: `foreign("user_id").references("id").on("users")`. */
|
|
76
|
+
foreign(column: string): ForeignKeyBuilder;
|
|
77
|
+
/** The `CREATE TABLE` plus any `CREATE INDEX` statements, in order. */
|
|
78
|
+
toStatements(table: string, dialect: Dialect): string[];
|
|
79
|
+
/** @deprecated Use `toStatements`. Kept for callers that build the SQL directly. */
|
|
47
80
|
toCreateSql(table: string, dialect: Dialect): string;
|
|
48
81
|
}
|
|
82
|
+
/** Alterations to an existing table: add/drop/rename columns, add/drop indexes. */
|
|
83
|
+
export declare class AlterTableBuilder extends ColumnBag {
|
|
84
|
+
private readonly drops;
|
|
85
|
+
private readonly renames;
|
|
86
|
+
private readonly addIndexes;
|
|
87
|
+
private readonly dropIndexes;
|
|
88
|
+
/** Drop a column. */
|
|
89
|
+
dropColumn(name: string): this;
|
|
90
|
+
/** Rename a column. */
|
|
91
|
+
renameColumn(from: string, to: string): this;
|
|
92
|
+
index(columns: string | string[], name?: string): this;
|
|
93
|
+
uniqueIndex(columns: string | string[], name?: string): this;
|
|
94
|
+
dropIndex(name: string): this;
|
|
95
|
+
toStatements(table: string, dialect: Dialect): string[];
|
|
96
|
+
}
|
|
49
97
|
export declare class SchemaBuilder {
|
|
50
98
|
private conn;
|
|
51
99
|
private dialect;
|
|
52
100
|
constructor(conn: Connection, dialect: Dialect);
|
|
53
101
|
createTable(name: string, build: (table: TableBuilder) => void): Promise<void>;
|
|
102
|
+
/** Alter an existing table — add/drop/rename columns, add/drop indexes. */
|
|
103
|
+
alterTable(name: string, build: (table: AlterTableBuilder) => void): Promise<void>;
|
|
54
104
|
dropTable(name: string): Promise<void>;
|
|
55
105
|
raw(sql: string, bindings?: unknown[]): Promise<void>;
|
|
56
106
|
}
|
package/dist/core/migrations.js
CHANGED
|
@@ -87,7 +87,49 @@ export class Column {
|
|
|
87
87
|
return sql;
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
-
|
|
90
|
+
/** A foreign-key constraint, built fluently: `foreign("user_id").references("id").on("users")`. */
|
|
91
|
+
export class ForeignKeyBuilder {
|
|
92
|
+
column;
|
|
93
|
+
_refTable = "";
|
|
94
|
+
_refColumn = "id";
|
|
95
|
+
_onDelete;
|
|
96
|
+
_onUpdate;
|
|
97
|
+
constructor(column) {
|
|
98
|
+
this.column = column;
|
|
99
|
+
}
|
|
100
|
+
references(column) {
|
|
101
|
+
this._refColumn = column;
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
on(table) {
|
|
105
|
+
this._refTable = table;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
onDelete(action) {
|
|
109
|
+
this._onDelete = action;
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
onUpdate(action) {
|
|
113
|
+
this._onUpdate = action;
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
toSql() {
|
|
117
|
+
let sql = `FOREIGN KEY (${this.column}) REFERENCES ${this._refTable}(${this._refColumn})`;
|
|
118
|
+
if (this._onDelete)
|
|
119
|
+
sql += ` ON DELETE ${this._onDelete}`;
|
|
120
|
+
if (this._onUpdate)
|
|
121
|
+
sql += ` ON UPDATE ${this._onUpdate}`;
|
|
122
|
+
return sql;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function toArray(value) {
|
|
126
|
+
return Array.isArray(value) ? value : [value];
|
|
127
|
+
}
|
|
128
|
+
function indexName(table, columns, unique) {
|
|
129
|
+
return `${table}_${columns.join("_")}_${unique ? "unique" : "index"}`;
|
|
130
|
+
}
|
|
131
|
+
/** Column-declaring methods shared by `createTable` and `alterTable` builders. */
|
|
132
|
+
class ColumnBag {
|
|
91
133
|
columns = [];
|
|
92
134
|
add(name, type, length) {
|
|
93
135
|
const col = new Column(name, type, length);
|
|
@@ -123,8 +165,89 @@ export class TableBuilder {
|
|
|
123
165
|
this.timestamp("created_at").nullable();
|
|
124
166
|
this.timestamp("updated_at").nullable();
|
|
125
167
|
}
|
|
168
|
+
}
|
|
169
|
+
export class TableBuilder extends ColumnBag {
|
|
170
|
+
indexes = [];
|
|
171
|
+
foreignKeys = [];
|
|
172
|
+
/** A (possibly composite) index. */
|
|
173
|
+
index(columns, name) {
|
|
174
|
+
this.indexes.push({ columns: toArray(columns), unique: false, ...(name ? { name } : {}) });
|
|
175
|
+
return this;
|
|
176
|
+
}
|
|
177
|
+
/** A (possibly composite) unique index. */
|
|
178
|
+
uniqueIndex(columns, name) {
|
|
179
|
+
this.indexes.push({ columns: toArray(columns), unique: true, ...(name ? { name } : {}) });
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
/** A foreign key: `foreign("user_id").references("id").on("users")`. */
|
|
183
|
+
foreign(column) {
|
|
184
|
+
const fk = new ForeignKeyBuilder(column);
|
|
185
|
+
this.foreignKeys.push(fk);
|
|
186
|
+
return fk;
|
|
187
|
+
}
|
|
188
|
+
/** The `CREATE TABLE` plus any `CREATE INDEX` statements, in order. */
|
|
189
|
+
toStatements(table, dialect) {
|
|
190
|
+
const parts = [
|
|
191
|
+
...this.columns.map((c) => c.toSql(dialect)),
|
|
192
|
+
...this.foreignKeys.map((fk) => fk.toSql()),
|
|
193
|
+
];
|
|
194
|
+
const create = `CREATE TABLE ${table} (${parts.join(", ")})`;
|
|
195
|
+
return [create, ...this.indexes.map((i) => createIndexSql(table, i))];
|
|
196
|
+
}
|
|
197
|
+
/** @deprecated Use `toStatements`. Kept for callers that build the SQL directly. */
|
|
126
198
|
toCreateSql(table, dialect) {
|
|
127
|
-
return
|
|
199
|
+
return this.toStatements(table, dialect)[0];
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function createIndexSql(table, index) {
|
|
203
|
+
const name = index.name ?? indexName(table, index.columns, index.unique);
|
|
204
|
+
return `CREATE ${index.unique ? "UNIQUE " : ""}INDEX ${name} ON ${table} (${index.columns.join(", ")})`;
|
|
205
|
+
}
|
|
206
|
+
/** Alterations to an existing table: add/drop/rename columns, add/drop indexes. */
|
|
207
|
+
export class AlterTableBuilder extends ColumnBag {
|
|
208
|
+
drops = [];
|
|
209
|
+
renames = [];
|
|
210
|
+
addIndexes = [];
|
|
211
|
+
dropIndexes = [];
|
|
212
|
+
/** Drop a column. */
|
|
213
|
+
dropColumn(name) {
|
|
214
|
+
this.drops.push(name);
|
|
215
|
+
return this;
|
|
216
|
+
}
|
|
217
|
+
/** Rename a column. */
|
|
218
|
+
renameColumn(from, to) {
|
|
219
|
+
this.renames.push({ from, to });
|
|
220
|
+
return this;
|
|
221
|
+
}
|
|
222
|
+
index(columns, name) {
|
|
223
|
+
this.addIndexes.push({ columns: toArray(columns), unique: false, ...(name ? { name } : {}) });
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
uniqueIndex(columns, name) {
|
|
227
|
+
this.addIndexes.push({ columns: toArray(columns), unique: true, ...(name ? { name } : {}) });
|
|
228
|
+
return this;
|
|
229
|
+
}
|
|
230
|
+
dropIndex(name) {
|
|
231
|
+
this.dropIndexes.push(name);
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
234
|
+
toStatements(table, dialect) {
|
|
235
|
+
const stmts = [];
|
|
236
|
+
// Drop indexes first, so a column an index references can then be dropped.
|
|
237
|
+
for (const name of this.dropIndexes) {
|
|
238
|
+
// MySQL drops indexes through ALTER TABLE; sqlite/postgres use DROP INDEX.
|
|
239
|
+
stmts.push(dialect === "mysql" ? `ALTER TABLE ${table} DROP INDEX ${name}` : `DROP INDEX ${name}`);
|
|
240
|
+
}
|
|
241
|
+
for (const col of this.columns)
|
|
242
|
+
stmts.push(`ALTER TABLE ${table} ADD COLUMN ${col.toSql(dialect)}`);
|
|
243
|
+
for (const { from, to } of this.renames) {
|
|
244
|
+
stmts.push(`ALTER TABLE ${table} RENAME COLUMN ${from} TO ${to}`);
|
|
245
|
+
}
|
|
246
|
+
for (const name of this.drops)
|
|
247
|
+
stmts.push(`ALTER TABLE ${table} DROP COLUMN ${name}`);
|
|
248
|
+
for (const index of this.addIndexes)
|
|
249
|
+
stmts.push(createIndexSql(table, index));
|
|
250
|
+
return stmts;
|
|
128
251
|
}
|
|
129
252
|
}
|
|
130
253
|
export class SchemaBuilder {
|
|
@@ -137,7 +260,15 @@ export class SchemaBuilder {
|
|
|
137
260
|
async createTable(name, build) {
|
|
138
261
|
const table = new TableBuilder();
|
|
139
262
|
build(table);
|
|
140
|
-
|
|
263
|
+
for (const sql of table.toStatements(name, this.dialect))
|
|
264
|
+
await this.conn.write(sql, []);
|
|
265
|
+
}
|
|
266
|
+
/** Alter an existing table — add/drop/rename columns, add/drop indexes. */
|
|
267
|
+
async alterTable(name, build) {
|
|
268
|
+
const table = new AlterTableBuilder();
|
|
269
|
+
build(table);
|
|
270
|
+
for (const sql of table.toStatements(name, this.dialect))
|
|
271
|
+
await this.conn.write(sql, []);
|
|
141
272
|
}
|
|
142
273
|
async dropTable(name) {
|
|
143
274
|
await this.conn.write(`DROP TABLE IF EXISTS ${name}`, []);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model lifecycle events. A model fires these as it is retrieved, saved, and
|
|
3
|
+
* deleted, so an app can hook behaviour onto a model without editing every call
|
|
4
|
+
* site — slug a title on `creating`, bust a cache on `saved`, cascade on
|
|
5
|
+
* `deleting`. The `*ing` events are cancelable: a hook returning `false` aborts
|
|
6
|
+
* the operation (the write never runs).
|
|
7
|
+
*
|
|
8
|
+
* User.creating((user) => { user.uuid = crypto.randomUUID(); });
|
|
9
|
+
* User.deleting((user) => user.isAdmin ? false : undefined); // veto
|
|
10
|
+
* User.observe(new UserObserver());
|
|
11
|
+
*
|
|
12
|
+
* Hooks are keyed by the exact model class (subclasses don't inherit a parent's
|
|
13
|
+
* hooks), stored off the class in a WeakMap so they never leak into instances or
|
|
14
|
+
* `save()`'s column spread.
|
|
15
|
+
*/
|
|
16
|
+
import type { Model } from "./model.js";
|
|
17
|
+
export type ModelEvent = "retrieved" | "creating" | "created" | "updating" | "updated" | "saving" | "saved" | "deleting" | "deleted" | "restoring" | "restored";
|
|
18
|
+
/** A lifecycle hook. Returning `false` from a cancelable event aborts the op. */
|
|
19
|
+
export type ModelHook<T extends Model = Model> = (model: T) => void | boolean | Promise<void | boolean>;
|
|
20
|
+
/** An observer: an object whose methods are named after the events they handle. */
|
|
21
|
+
export type ModelObserver<T extends Model = Model> = Partial<Record<ModelEvent, ModelHook<T>>>;
|
|
22
|
+
/** The full event list, for wiring up an observer. */
|
|
23
|
+
export declare const MODEL_EVENTS: ModelEvent[];
|
|
24
|
+
/** Register a hook for `event` on a model class. */
|
|
25
|
+
export declare function addModelHook(cls: object, event: ModelEvent, hook: ModelHook): void;
|
|
26
|
+
/** Attach every matching method of an observer object as a hook. */
|
|
27
|
+
export declare function addModelObserver(cls: object, observer: ModelObserver): void;
|
|
28
|
+
/** Drop a class's hooks (or all of them) — a test helper for a clean slate. */
|
|
29
|
+
export declare function clearModelHooks(cls?: object): void;
|
|
30
|
+
/**
|
|
31
|
+
* Fire an event's hooks in registration order. Returns `false` only when a
|
|
32
|
+
* cancelable event had a hook return `false` — the caller then skips the write.
|
|
33
|
+
*/
|
|
34
|
+
export declare function fireModelEvent(cls: object, event: ModelEvent, model: Model): Promise<boolean>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model lifecycle events. A model fires these as it is retrieved, saved, and
|
|
3
|
+
* deleted, so an app can hook behaviour onto a model without editing every call
|
|
4
|
+
* site — slug a title on `creating`, bust a cache on `saved`, cascade on
|
|
5
|
+
* `deleting`. The `*ing` events are cancelable: a hook returning `false` aborts
|
|
6
|
+
* the operation (the write never runs).
|
|
7
|
+
*
|
|
8
|
+
* User.creating((user) => { user.uuid = crypto.randomUUID(); });
|
|
9
|
+
* User.deleting((user) => user.isAdmin ? false : undefined); // veto
|
|
10
|
+
* User.observe(new UserObserver());
|
|
11
|
+
*
|
|
12
|
+
* Hooks are keyed by the exact model class (subclasses don't inherit a parent's
|
|
13
|
+
* hooks), stored off the class in a WeakMap so they never leak into instances or
|
|
14
|
+
* `save()`'s column spread.
|
|
15
|
+
*/
|
|
16
|
+
/** Events whose hooks can veto the operation by returning `false`. */
|
|
17
|
+
const CANCELABLE = new Set(["creating", "updating", "saving", "deleting", "restoring"]);
|
|
18
|
+
/** The full event list, for wiring up an observer. */
|
|
19
|
+
export const MODEL_EVENTS = [
|
|
20
|
+
"retrieved",
|
|
21
|
+
"creating",
|
|
22
|
+
"created",
|
|
23
|
+
"updating",
|
|
24
|
+
"updated",
|
|
25
|
+
"saving",
|
|
26
|
+
"saved",
|
|
27
|
+
"deleting",
|
|
28
|
+
"deleted",
|
|
29
|
+
"restoring",
|
|
30
|
+
"restored",
|
|
31
|
+
];
|
|
32
|
+
// Class object → event → hooks. A WeakMap so unloaded classes are collectable.
|
|
33
|
+
const registry = new WeakMap();
|
|
34
|
+
/** Register a hook for `event` on a model class. */
|
|
35
|
+
export function addModelHook(cls, event, hook) {
|
|
36
|
+
let byEvent = registry.get(cls);
|
|
37
|
+
if (!byEvent)
|
|
38
|
+
registry.set(cls, (byEvent = new Map()));
|
|
39
|
+
const hooks = byEvent.get(event) ?? [];
|
|
40
|
+
hooks.push(hook);
|
|
41
|
+
byEvent.set(event, hooks);
|
|
42
|
+
}
|
|
43
|
+
/** Attach every matching method of an observer object as a hook. */
|
|
44
|
+
export function addModelObserver(cls, observer) {
|
|
45
|
+
for (const event of MODEL_EVENTS) {
|
|
46
|
+
const hook = observer[event];
|
|
47
|
+
if (typeof hook === "function")
|
|
48
|
+
addModelHook(cls, event, hook);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Drop a class's hooks (or all of them) — a test helper for a clean slate. */
|
|
52
|
+
export function clearModelHooks(cls) {
|
|
53
|
+
if (cls)
|
|
54
|
+
registry.delete(cls);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Fire an event's hooks in registration order. Returns `false` only when a
|
|
58
|
+
* cancelable event had a hook return `false` — the caller then skips the write.
|
|
59
|
+
*/
|
|
60
|
+
export async function fireModelEvent(cls, event, model) {
|
|
61
|
+
const hooks = registry.get(cls)?.get(event);
|
|
62
|
+
if (!hooks?.length)
|
|
63
|
+
return true;
|
|
64
|
+
const cancelable = CANCELABLE.has(event);
|
|
65
|
+
for (const hook of hooks) {
|
|
66
|
+
const result = await hook(model);
|
|
67
|
+
if (cancelable && result === false)
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A model-aware query builder — what `Model.query()` sugar and eager loading are
|
|
3
|
+
* built on. It wraps the plain `QueryBuilder`, hydrates rows into models (firing
|
|
4
|
+
* `retrieved`), and adds the relationship-aware operations Eloquent has and a raw
|
|
5
|
+
* builder can't: `with()` (nested eager loading), `withCount()`, and existence
|
|
6
|
+
* filters `has()` / `whereHas()` / `doesntHave()`.
|
|
7
|
+
*
|
|
8
|
+
* const users = await User.query()
|
|
9
|
+
* .where("active", true)
|
|
10
|
+
* .with("posts.comments")
|
|
11
|
+
* .withCount("posts")
|
|
12
|
+
* .whereHas("posts", (q) => q.where("published", true))
|
|
13
|
+
* .get();
|
|
14
|
+
*
|
|
15
|
+
* Existence filters use the same two-query strategy as the relations themselves
|
|
16
|
+
* (a `whereIn` over keys gathered from the related table), so everything stays on
|
|
17
|
+
* the driver-agnostic builder — no JOIN or correlated subquery required. They're
|
|
18
|
+
* recorded and resolved at `get()`/`first()`/`count()` time so the chain stays
|
|
19
|
+
* synchronous.
|
|
20
|
+
*/
|
|
21
|
+
import type { QueryBuilder, Row, Paginated } from "./database.js";
|
|
22
|
+
import type { Model } from "./model.js";
|
|
23
|
+
type ModelClass<T extends Model> = new (attributes?: Row) => T;
|
|
24
|
+
type Constrain = (q: QueryBuilder) => void;
|
|
25
|
+
export declare class ModelQuery<T extends Model> {
|
|
26
|
+
private cls;
|
|
27
|
+
private builder;
|
|
28
|
+
private eagers;
|
|
29
|
+
private counts;
|
|
30
|
+
private hasConstraints;
|
|
31
|
+
constructor(cls: ModelClass<T>, builder: QueryBuilder);
|
|
32
|
+
/** The wrapped builder, for anything `ModelQuery` doesn't proxy. */
|
|
33
|
+
toBase(): QueryBuilder;
|
|
34
|
+
where(column: string, opOrValue: unknown, value?: unknown): this;
|
|
35
|
+
orWhere(column: string, opOrValue: unknown, value?: unknown): this;
|
|
36
|
+
whereIn(column: string, values: unknown[]): this;
|
|
37
|
+
whereNotIn(column: string, values: unknown[]): this;
|
|
38
|
+
whereNull(column: string): this;
|
|
39
|
+
whereNotNull(column: string): this;
|
|
40
|
+
whereBetween(column: string, range: [unknown, unknown]): this;
|
|
41
|
+
whereLike(column: string, pattern: string): this;
|
|
42
|
+
orderBy(column: string, direction?: "asc" | "desc"): this;
|
|
43
|
+
latest(column?: string): this;
|
|
44
|
+
oldest(column?: string): this;
|
|
45
|
+
limit(n: number): this;
|
|
46
|
+
offset(n: number): this;
|
|
47
|
+
/** Eager-load relations (dotted paths for nesting: `"posts.comments"`). */
|
|
48
|
+
with(...names: string[]): this;
|
|
49
|
+
/** Add a `<relation>_count` to each result. */
|
|
50
|
+
withCount(...names: string[]): this;
|
|
51
|
+
/** Constrain to models that have at least one related row. */
|
|
52
|
+
has(name: string): this;
|
|
53
|
+
/** Constrain to models whose related rows match `constrain`. */
|
|
54
|
+
whereHas(name: string, constrain?: Constrain): this;
|
|
55
|
+
/** Constrain to models with no matching related row. */
|
|
56
|
+
doesntHave(name: string, constrain?: Constrain): this;
|
|
57
|
+
get(): Promise<T[]>;
|
|
58
|
+
first(): Promise<T | null>;
|
|
59
|
+
count(): Promise<number>;
|
|
60
|
+
exists(): Promise<boolean>;
|
|
61
|
+
paginate(page?: number, perPage?: number): Promise<Paginated<T>>;
|
|
62
|
+
private hydrate;
|
|
63
|
+
private relationFor;
|
|
64
|
+
/** Turn recorded has/doesntHave constraints into `whereIn`/`whereNotIn` on the builder. */
|
|
65
|
+
private resolveHasConstraints;
|
|
66
|
+
private loadCounts;
|
|
67
|
+
}
|
|
68
|
+
export {};
|