@shaferllc/keel 0.12.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +28 -3
  2. package/dist/core/application.js +9 -0
  3. package/dist/core/auth.d.ts +41 -0
  4. package/dist/core/auth.js +70 -0
  5. package/dist/core/cache.d.ts +41 -0
  6. package/dist/core/cache.js +81 -0
  7. package/dist/core/casts.d.ts +19 -0
  8. package/dist/core/casts.js +69 -0
  9. package/dist/core/crypto.d.ts +24 -0
  10. package/dist/core/crypto.js +97 -0
  11. package/dist/core/database.d.ts +58 -0
  12. package/dist/core/database.js +140 -0
  13. package/dist/core/debug.d.ts +12 -0
  14. package/dist/core/debug.js +55 -0
  15. package/dist/core/events.d.ts +21 -0
  16. package/dist/core/events.js +46 -0
  17. package/dist/core/exceptions.d.ts +10 -1
  18. package/dist/core/exceptions.js +10 -1
  19. package/dist/core/factory.d.ts +84 -0
  20. package/dist/core/factory.js +154 -0
  21. package/dist/core/helpers.d.ts +13 -0
  22. package/dist/core/helpers.js +24 -0
  23. package/dist/core/http/kernel.js +21 -2
  24. package/dist/core/http/router.d.ts +32 -8
  25. package/dist/core/http/router.js +74 -5
  26. package/dist/core/index.d.ts +31 -2
  27. package/dist/core/index.js +17 -1
  28. package/dist/core/logger.d.ts +29 -0
  29. package/dist/core/logger.js +49 -0
  30. package/dist/core/mail.d.ts +94 -0
  31. package/dist/core/mail.js +157 -0
  32. package/dist/core/migrations.d.ts +76 -0
  33. package/dist/core/migrations.js +211 -0
  34. package/dist/core/model.d.ts +75 -0
  35. package/dist/core/model.js +202 -0
  36. package/dist/core/queue.d.ts +73 -0
  37. package/dist/core/queue.js +88 -0
  38. package/dist/core/rate-limit.d.ts +23 -0
  39. package/dist/core/rate-limit.js +50 -0
  40. package/dist/core/relations.d.ts +90 -0
  41. package/dist/core/relations.js +229 -0
  42. package/dist/core/request.d.ts +53 -1
  43. package/dist/core/request.js +187 -0
  44. package/dist/core/session.d.ts +46 -0
  45. package/dist/core/session.js +112 -0
  46. package/dist/core/static.d.ts +27 -0
  47. package/dist/core/static.js +61 -0
  48. package/package.json +1 -1
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Schema builder + migrator. Define migrations as `{ name, up, down }`, describe
3
+ * tables with a fluent schema builder, and run them through your registered
4
+ * connection. Dialect-aware SQL (sqlite / mysql / postgres), edge-safe.
5
+ *
6
+ * const migrations: Migration[] = [{
7
+ * name: "01_create_users",
8
+ * up: (s) => s.createTable("users", (t) => {
9
+ * t.id();
10
+ * t.string("email").unique();
11
+ * t.timestamps();
12
+ * }),
13
+ * down: (s) => s.dropTable("users"),
14
+ * }];
15
+ *
16
+ * await new Migrator(connection, "postgres").up(migrations);
17
+ */
18
+ import type { Connection, Dialect } from "./database.js";
19
+ type ColumnType = "id" | "string" | "text" | "integer" | "bigInteger" | "boolean" | "timestamp" | "json";
20
+ export declare class Column {
21
+ private column;
22
+ private type;
23
+ private length?;
24
+ private _nullable;
25
+ private _unique;
26
+ private _default;
27
+ private hasDefault;
28
+ constructor(column: string, type: ColumnType, length?: number | undefined);
29
+ nullable(): this;
30
+ unique(): this;
31
+ default(value: unknown): this;
32
+ toSql(dialect: Dialect): string;
33
+ }
34
+ export declare class TableBuilder {
35
+ readonly columns: Column[];
36
+ private add;
37
+ id(name?: string): Column;
38
+ string(name: string, length?: number): Column;
39
+ text(name: string): Column;
40
+ integer(name: string): Column;
41
+ bigInteger(name: string): Column;
42
+ boolean(name: string): Column;
43
+ timestamp(name: string): Column;
44
+ json(name: string): Column;
45
+ /** Adds nullable created_at + updated_at columns. */
46
+ timestamps(): void;
47
+ toCreateSql(table: string, dialect: Dialect): string;
48
+ }
49
+ export declare class SchemaBuilder {
50
+ private conn;
51
+ private dialect;
52
+ constructor(conn: Connection, dialect: Dialect);
53
+ createTable(name: string, build: (table: TableBuilder) => void): Promise<void>;
54
+ dropTable(name: string): Promise<void>;
55
+ raw(sql: string, bindings?: unknown[]): Promise<void>;
56
+ }
57
+ export interface Migration {
58
+ name: string;
59
+ up(schema: SchemaBuilder): void | Promise<void>;
60
+ down(schema: SchemaBuilder): void | Promise<void>;
61
+ }
62
+ export declare class Migrator {
63
+ private conn;
64
+ private dialect;
65
+ constructor(conn: Connection, dialect?: Dialect);
66
+ private ph;
67
+ private ensure;
68
+ /** Names of migrations already applied. */
69
+ ran(): Promise<string[]>;
70
+ private maxBatch;
71
+ /** Run all pending migrations. Returns the names applied. */
72
+ up(migrations: Migration[]): Promise<string[]>;
73
+ /** Roll back the most recent batch. Returns the names rolled back. */
74
+ down(migrations: Migration[]): Promise<string[]>;
75
+ }
76
+ export {};
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Schema builder + migrator. Define migrations as `{ name, up, down }`, describe
3
+ * tables with a fluent schema builder, and run them through your registered
4
+ * connection. Dialect-aware SQL (sqlite / mysql / postgres), edge-safe.
5
+ *
6
+ * const migrations: Migration[] = [{
7
+ * name: "01_create_users",
8
+ * up: (s) => s.createTable("users", (t) => {
9
+ * t.id();
10
+ * t.string("email").unique();
11
+ * t.timestamps();
12
+ * }),
13
+ * down: (s) => s.dropTable("users"),
14
+ * }];
15
+ *
16
+ * await new Migrator(connection, "postgres").up(migrations);
17
+ */
18
+ function sqlType(type, dialect, length) {
19
+ switch (type) {
20
+ case "id":
21
+ return dialect === "postgres"
22
+ ? "SERIAL PRIMARY KEY"
23
+ : dialect === "mysql"
24
+ ? "INT AUTO_INCREMENT PRIMARY KEY"
25
+ : "INTEGER PRIMARY KEY AUTOINCREMENT";
26
+ case "string":
27
+ return `VARCHAR(${length ?? 255})`;
28
+ case "text":
29
+ return "TEXT";
30
+ case "integer":
31
+ return "INTEGER";
32
+ case "bigInteger":
33
+ return "BIGINT";
34
+ case "boolean":
35
+ return dialect === "sqlite" ? "INTEGER" : "BOOLEAN";
36
+ case "timestamp":
37
+ return dialect === "sqlite" ? "DATETIME" : "TIMESTAMP";
38
+ case "json":
39
+ return dialect === "postgres" ? "JSONB" : "TEXT";
40
+ }
41
+ }
42
+ export class Column {
43
+ column;
44
+ type;
45
+ length;
46
+ _nullable = false;
47
+ _unique = false;
48
+ _default;
49
+ hasDefault = false;
50
+ constructor(column, type, length) {
51
+ this.column = column;
52
+ this.type = type;
53
+ this.length = length;
54
+ }
55
+ nullable() {
56
+ this._nullable = true;
57
+ return this;
58
+ }
59
+ unique() {
60
+ this._unique = true;
61
+ return this;
62
+ }
63
+ default(value) {
64
+ this._default = value;
65
+ this.hasDefault = true;
66
+ return this;
67
+ }
68
+ toSql(dialect) {
69
+ let sql = `${this.column} ${sqlType(this.type, dialect, this.length)}`;
70
+ if (this.type !== "id")
71
+ sql += this._nullable ? "" : " NOT NULL";
72
+ if (this._unique)
73
+ sql += " UNIQUE";
74
+ if (this.hasDefault) {
75
+ const v = this._default;
76
+ const rendered = typeof v === "string"
77
+ ? `'${v}'`
78
+ : typeof v === "boolean"
79
+ ? dialect === "sqlite"
80
+ ? v
81
+ ? "1"
82
+ : "0"
83
+ : String(v)
84
+ : String(v);
85
+ sql += ` DEFAULT ${rendered}`;
86
+ }
87
+ return sql;
88
+ }
89
+ }
90
+ export class TableBuilder {
91
+ columns = [];
92
+ add(name, type, length) {
93
+ const col = new Column(name, type, length);
94
+ this.columns.push(col);
95
+ return col;
96
+ }
97
+ id(name = "id") {
98
+ return this.add(name, "id");
99
+ }
100
+ string(name, length = 255) {
101
+ return this.add(name, "string", length);
102
+ }
103
+ text(name) {
104
+ return this.add(name, "text");
105
+ }
106
+ integer(name) {
107
+ return this.add(name, "integer");
108
+ }
109
+ bigInteger(name) {
110
+ return this.add(name, "bigInteger");
111
+ }
112
+ boolean(name) {
113
+ return this.add(name, "boolean");
114
+ }
115
+ timestamp(name) {
116
+ return this.add(name, "timestamp");
117
+ }
118
+ json(name) {
119
+ return this.add(name, "json");
120
+ }
121
+ /** Adds nullable created_at + updated_at columns. */
122
+ timestamps() {
123
+ this.timestamp("created_at").nullable();
124
+ this.timestamp("updated_at").nullable();
125
+ }
126
+ toCreateSql(table, dialect) {
127
+ return `CREATE TABLE ${table} (${this.columns.map((c) => c.toSql(dialect)).join(", ")})`;
128
+ }
129
+ }
130
+ export class SchemaBuilder {
131
+ conn;
132
+ dialect;
133
+ constructor(conn, dialect) {
134
+ this.conn = conn;
135
+ this.dialect = dialect;
136
+ }
137
+ async createTable(name, build) {
138
+ const table = new TableBuilder();
139
+ build(table);
140
+ await this.conn.write(table.toCreateSql(name, this.dialect), []);
141
+ }
142
+ async dropTable(name) {
143
+ await this.conn.write(`DROP TABLE IF EXISTS ${name}`, []);
144
+ }
145
+ async raw(sql, bindings = []) {
146
+ await this.conn.write(sql, bindings);
147
+ }
148
+ }
149
+ export class Migrator {
150
+ conn;
151
+ dialect;
152
+ constructor(conn, dialect = "sqlite") {
153
+ this.conn = conn;
154
+ this.dialect = dialect;
155
+ }
156
+ ph(sql) {
157
+ if (this.dialect !== "postgres")
158
+ return sql;
159
+ let i = 0;
160
+ return sql.replace(/\?/g, () => `$${++i}`);
161
+ }
162
+ async ensure() {
163
+ await this.conn.write("CREATE TABLE IF NOT EXISTS migrations (name VARCHAR(255) PRIMARY KEY, batch INTEGER NOT NULL)", []);
164
+ }
165
+ /** Names of migrations already applied. */
166
+ async ran() {
167
+ await this.ensure();
168
+ const rows = await this.conn.select("SELECT name FROM migrations", []);
169
+ return rows.map((r) => String(r.name));
170
+ }
171
+ async maxBatch() {
172
+ const rows = await this.conn.select("SELECT MAX(batch) AS b FROM migrations", []);
173
+ return Number(rows[0]?.b ?? 0);
174
+ }
175
+ /** Run all pending migrations. Returns the names applied. */
176
+ async up(migrations) {
177
+ const ran = await this.ran();
178
+ const batch = (await this.maxBatch()) + 1;
179
+ const schema = new SchemaBuilder(this.conn, this.dialect);
180
+ const applied = [];
181
+ for (const m of migrations) {
182
+ if (ran.includes(m.name))
183
+ continue;
184
+ await m.up(schema);
185
+ await this.conn.write(this.ph("INSERT INTO migrations (name, batch) VALUES (?, ?)"), [
186
+ m.name,
187
+ batch,
188
+ ]);
189
+ applied.push(m.name);
190
+ }
191
+ return applied;
192
+ }
193
+ /** Roll back the most recent batch. Returns the names rolled back. */
194
+ async down(migrations) {
195
+ await this.ensure();
196
+ const batch = await this.maxBatch();
197
+ if (!batch)
198
+ return [];
199
+ const rows = await this.conn.select(this.ph("SELECT name FROM migrations WHERE batch = ?"), [batch]);
200
+ const schema = new SchemaBuilder(this.conn, this.dialect);
201
+ const rolled = [];
202
+ for (const name of rows.map((r) => String(r.name)).reverse()) {
203
+ const migration = migrations.find((m) => m.name === name);
204
+ if (migration)
205
+ await migration.down(schema);
206
+ await this.conn.write(this.ph("DELETE FROM migrations WHERE name = ?"), [name]);
207
+ rolled.push(name);
208
+ }
209
+ return rolled;
210
+ }
211
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * A tiny active-record Model over the query builder. Subclass it, point it at a
3
+ * table, and get `find` / `all` / `create` / `save` / `delete` — no ORM to
4
+ * configure. Backed by whatever `Connection` you registered with
5
+ * `setConnection()`, so it runs on Node and the edge.
6
+ *
7
+ * class User extends Model {
8
+ * static table = "users";
9
+ * declare id: number;
10
+ * declare email: string;
11
+ * }
12
+ *
13
+ * const user = await User.find(1);
14
+ * const created = await User.create({ email });
15
+ * user.email = "new@x.com";
16
+ * await user.save();
17
+ */
18
+ import { type QueryBuilder, type Row } from "./database.js";
19
+ import { BelongsTo, BelongsToMany, HasMany, HasOne } from "./relations.js";
20
+ import { type Casts } from "./casts.js";
21
+ type ModelClass<T extends Model> = (new (attributes?: Row) => T) & typeof Model;
22
+ export declare class Model {
23
+ static table: string;
24
+ static primaryKey: string;
25
+ /** Columns mass-assignable via `create`/`fill` (allowlist). */
26
+ static fillable: string[];
27
+ /** Columns NOT mass-assignable (denylist). Ignored when `fillable` is set. */
28
+ static guarded: string[];
29
+ /** Column -> cast type; values round-trip as real JS types. */
30
+ static casts: Casts;
31
+ [key: string]: unknown;
32
+ constructor(attributes?: Row);
33
+ /** A raw query builder scoped to this model's table. */
34
+ static query(): QueryBuilder;
35
+ /** Keep only the attributes mass-assignment allows (fillable / guarded). */
36
+ static filterFillable(attributes: Row): Row;
37
+ /** Cast attributes to their storable primitives for a write. */
38
+ static toDatabase(attributes: Row): Row;
39
+ static all<T extends Model>(this: ModelClass<T>): Promise<T[]>;
40
+ static find<T extends Model>(this: ModelClass<T>, id: unknown): Promise<T | null>;
41
+ static findOrFail<T extends Model>(this: ModelClass<T>, id: unknown): Promise<T>;
42
+ static first<T extends Model>(this: ModelClass<T>): Promise<T | null>;
43
+ /** Fetch models matching a simple equality condition. */
44
+ static where<T extends Model>(this: ModelClass<T>, column: string, value: unknown): Promise<T[]>;
45
+ static create<T extends Model>(this: ModelClass<T>, attributes: Row): Promise<T>;
46
+ /**
47
+ * Eager-load relations onto an array of models with one extra query each —
48
+ * the fix for N+1. Each name must be a relationship method on the model.
49
+ */
50
+ static load<T extends Model>(models: T[], ...names: string[]): Promise<T[]>;
51
+ private ctor;
52
+ /** The default foreign key a child of this model would carry (e.g. `user_id`). */
53
+ private foreignKeyName;
54
+ /** This model has many `related` rows, joined by a foreign key on `related`. */
55
+ hasMany<T extends Model>(related: ModelClass<T>, foreignKey?: string, localKey?: string): HasMany<T>;
56
+ /** This model has one `related` row, joined by a foreign key on `related`. */
57
+ hasOne<T extends Model>(related: ModelClass<T>, foreignKey?: string, localKey?: string): HasOne<T>;
58
+ /** This model belongs to a `related` row via a foreign key it carries. */
59
+ belongsTo<T extends Model>(related: ModelClass<T>, foreignKey?: string, ownerKey?: string): BelongsTo<T>;
60
+ /** Many-to-many through a pivot table (default name: the two tables, sorted). */
61
+ belongsToMany<T extends Model>(related: ModelClass<T>, pivotTable?: string, foreignPivotKey?: string, relatedPivotKey?: string, parentKey?: string, relatedKey?: string): BelongsToMany<T>;
62
+ /** Store an eager-loaded relation result under `name`. */
63
+ setRelation(name: string, value: unknown): this;
64
+ /** Read a previously loaded relation (returns undefined if not loaded). */
65
+ getRelation<T = unknown>(name: string): T | undefined;
66
+ /** Insert (no primary key) or update (has one). */
67
+ save(): Promise<this>;
68
+ delete(): Promise<void>;
69
+ /** Merge mass-assignable attributes into the model (cast, not saved). */
70
+ fill(attributes: Row): this;
71
+ /** Force-assign attributes, bypassing mass-assignment guarding (still cast). */
72
+ forceFill(attributes: Row): this;
73
+ toJSON(): Row;
74
+ }
75
+ export {};
@@ -0,0 +1,202 @@
1
+ /**
2
+ * A tiny active-record Model over the query builder. Subclass it, point it at a
3
+ * table, and get `find` / `all` / `create` / `save` / `delete` — no ORM to
4
+ * configure. Backed by whatever `Connection` you registered with
5
+ * `setConnection()`, so it runs on Node and the edge.
6
+ *
7
+ * class User extends Model {
8
+ * static table = "users";
9
+ * declare id: number;
10
+ * declare email: string;
11
+ * }
12
+ *
13
+ * const user = await User.find(1);
14
+ * const created = await User.create({ email });
15
+ * user.email = "new@x.com";
16
+ * await user.save();
17
+ */
18
+ import { db } from "./database.js";
19
+ import { NotFoundException } from "./exceptions.js";
20
+ import { BelongsTo, BelongsToMany, HasMany, HasOne } from "./relations.js";
21
+ import { applyCasts, castGet, castSet } from "./casts.js";
22
+ /**
23
+ * Loaded relations live off the model itself so they never leak into `save()`
24
+ * (which spreads own columns) — they're keyed here by the owning instance.
25
+ */
26
+ const relationStore = new WeakMap();
27
+ function serialize(value) {
28
+ if (Array.isArray(value))
29
+ return value.map(serialize);
30
+ if (value instanceof Model)
31
+ return value.toJSON();
32
+ return value;
33
+ }
34
+ export class Model {
35
+ static table = "";
36
+ static primaryKey = "id";
37
+ /** Columns mass-assignable via `create`/`fill` (allowlist). */
38
+ static fillable = [];
39
+ /** Columns NOT mass-assignable (denylist). Ignored when `fillable` is set. */
40
+ static guarded = [];
41
+ /** Column -> cast type; values round-trip as real JS types. */
42
+ static casts = {};
43
+ constructor(attributes = {}) {
44
+ // Hydration is unguarded (rows come from the database) but always cast.
45
+ Object.assign(this, applyCasts(attributes, this.constructor.casts, castGet));
46
+ }
47
+ /* ------------------------------ static -------------------------------- */
48
+ /** A raw query builder scoped to this model's table. */
49
+ static query() {
50
+ return db(this.table);
51
+ }
52
+ /** Keep only the attributes mass-assignment allows (fillable / guarded). */
53
+ static filterFillable(attributes) {
54
+ if (this.fillable.length) {
55
+ const out = {};
56
+ for (const key of this.fillable)
57
+ if (key in attributes)
58
+ out[key] = attributes[key];
59
+ return out;
60
+ }
61
+ if (this.guarded.length) {
62
+ const out = { ...attributes };
63
+ for (const key of this.guarded)
64
+ delete out[key];
65
+ return out;
66
+ }
67
+ return { ...attributes };
68
+ }
69
+ /** Cast attributes to their storable primitives for a write. */
70
+ static toDatabase(attributes) {
71
+ return applyCasts(attributes, this.casts, castSet);
72
+ }
73
+ static async all() {
74
+ const rows = await db(this.table).get();
75
+ return rows.map((row) => new this(row));
76
+ }
77
+ static async find(id) {
78
+ const row = await db(this.table).where(this.primaryKey, id).first();
79
+ return row ? new this(row) : null;
80
+ }
81
+ static async findOrFail(id) {
82
+ const model = await this.find(id);
83
+ if (!model)
84
+ throw new NotFoundException(`${this.name} ${String(id)} not found`);
85
+ return model;
86
+ }
87
+ static async first() {
88
+ const row = await db(this.table).first();
89
+ return row ? new this(row) : null;
90
+ }
91
+ /** Fetch models matching a simple equality condition. */
92
+ static async where(column, value) {
93
+ const rows = await db(this.table).where(column, value).get();
94
+ return rows.map((row) => new this(row));
95
+ }
96
+ static async create(attributes) {
97
+ const filtered = this.filterFillable(attributes);
98
+ const id = await db(this.table).insertGetId(this.toDatabase(filtered));
99
+ const model = new this(filtered);
100
+ if (id != null)
101
+ model[this.primaryKey] = id;
102
+ return model;
103
+ }
104
+ /**
105
+ * Eager-load relations onto an array of models with one extra query each —
106
+ * the fix for N+1. Each name must be a relationship method on the model.
107
+ */
108
+ static async load(models, ...names) {
109
+ if (!models.length)
110
+ return models;
111
+ for (const name of names) {
112
+ const method = models[0][name];
113
+ if (typeof method !== "function") {
114
+ throw new Error(`${models[0].constructor.name} has no relation "${name}"`);
115
+ }
116
+ const relation = method.call(models[0]);
117
+ await relation.eager(models, name);
118
+ }
119
+ return models;
120
+ }
121
+ /* ----------------------------- instance ------------------------------- */
122
+ ctor() {
123
+ return this.constructor;
124
+ }
125
+ /* --------------------------- relationships ---------------------------- */
126
+ /** The default foreign key a child of this model would carry (e.g. `user_id`). */
127
+ foreignKeyName() {
128
+ return `${this.constructor.name.toLowerCase()}_${this.ctor().primaryKey}`;
129
+ }
130
+ /** This model has many `related` rows, joined by a foreign key on `related`. */
131
+ hasMany(related, foreignKey = this.foreignKeyName(), localKey = this.ctor().primaryKey) {
132
+ return new HasMany(this, related, foreignKey, localKey);
133
+ }
134
+ /** This model has one `related` row, joined by a foreign key on `related`. */
135
+ hasOne(related, foreignKey = this.foreignKeyName(), localKey = this.ctor().primaryKey) {
136
+ return new HasOne(this, related, foreignKey, localKey);
137
+ }
138
+ /** This model belongs to a `related` row via a foreign key it carries. */
139
+ belongsTo(related, foreignKey = `${related.name.toLowerCase()}_${related.primaryKey}`, ownerKey = related.primaryKey) {
140
+ return new BelongsTo(this, related, foreignKey, ownerKey);
141
+ }
142
+ /** Many-to-many through a pivot table (default name: the two tables, sorted). */
143
+ belongsToMany(related, pivotTable = [this.constructor.name, related.name]
144
+ .map((n) => n.toLowerCase())
145
+ .sort()
146
+ .join("_"), foreignPivotKey = `${this.constructor.name.toLowerCase()}_${this.ctor().primaryKey}`, relatedPivotKey = `${related.name.toLowerCase()}_${related.primaryKey}`, parentKey = this.ctor().primaryKey, relatedKey = related.primaryKey) {
147
+ return new BelongsToMany(this, related, pivotTable, foreignPivotKey, relatedPivotKey, parentKey, relatedKey);
148
+ }
149
+ /** Store an eager-loaded relation result under `name`. */
150
+ setRelation(name, value) {
151
+ const bag = relationStore.get(this) ?? {};
152
+ bag[name] = value;
153
+ relationStore.set(this, bag);
154
+ return this;
155
+ }
156
+ /** Read a previously loaded relation (returns undefined if not loaded). */
157
+ getRelation(name) {
158
+ return relationStore.get(this)?.[name];
159
+ }
160
+ /** Insert (no primary key) or update (has one). */
161
+ async save() {
162
+ const ctor = this.ctor();
163
+ const { table, primaryKey } = ctor;
164
+ const data = ctor.toDatabase({ ...this });
165
+ const idValue = data[primaryKey];
166
+ delete data[primaryKey];
167
+ if (idValue != null) {
168
+ await db(table).where(primaryKey, idValue).update(data);
169
+ }
170
+ else {
171
+ const id = await db(table).insertGetId(data);
172
+ if (id != null)
173
+ this[primaryKey] = id;
174
+ }
175
+ return this;
176
+ }
177
+ async delete() {
178
+ const { table, primaryKey } = this.ctor();
179
+ await db(table).where(primaryKey, this[primaryKey]).delete();
180
+ }
181
+ /** Merge mass-assignable attributes into the model (cast, not saved). */
182
+ fill(attributes) {
183
+ const ctor = this.ctor();
184
+ Object.assign(this, applyCasts(ctor.filterFillable(attributes), ctor.casts, castGet));
185
+ return this;
186
+ }
187
+ /** Force-assign attributes, bypassing mass-assignment guarding (still cast). */
188
+ forceFill(attributes) {
189
+ const ctor = this.ctor();
190
+ Object.assign(this, applyCasts(attributes, ctor.casts, castGet));
191
+ return this;
192
+ }
193
+ toJSON() {
194
+ const data = applyCasts({ ...this }, this.ctor().casts, castGet);
195
+ const relations = relationStore.get(this);
196
+ if (relations) {
197
+ for (const [name, value] of Object.entries(relations))
198
+ data[name] = serialize(value);
199
+ }
200
+ return data;
201
+ }
202
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A small queue for background work. Dispatch a `Job` (or a plain function) and
3
+ * a pluggable `QueueDriver` decides when it runs — immediately (the default),
4
+ * held in memory for a worker to drain, or handed to a real broker. The API
5
+ * mirrors the database and mail layers (`setQueue` / `dispatch` are to queues
6
+ * what `setConnection` / `db()` are to the database). The core imports no broker,
7
+ * so it stays edge-safe.
8
+ *
9
+ * class SendWelcome extends Job {
10
+ * constructor(private userId: number) { super(); }
11
+ * async handle() { await mail().to(...).send(); }
12
+ * }
13
+ *
14
+ * await dispatch(new SendWelcome(user.id)); // runs now with the sync driver
15
+ *
16
+ * // Defer instead, and drain with a worker:
17
+ * setQueue(new MemoryDriver());
18
+ * await dispatch(new SendWelcome(user.id)); // queued
19
+ * await work(); // runs everything pending
20
+ */
21
+ /** A unit of background work. Subclass and implement `handle`. */
22
+ export declare abstract class Job {
23
+ abstract handle(): void | Promise<void>;
24
+ }
25
+ /** Anything dispatchable: a `Job` instance or a plain function. */
26
+ export type Dispatchable = Job | (() => void | Promise<void>);
27
+ export interface JobOptions {
28
+ /** Seconds to wait before the job becomes available (drivers may honor it). */
29
+ delay?: number;
30
+ /** Named queue/lane to place the job on. */
31
+ queue?: string;
32
+ }
33
+ /** The bridge to your queue backend. */
34
+ export interface QueueDriver {
35
+ push(job: Dispatchable, options: JobOptions): Promise<void>;
36
+ }
37
+ /** A driver that holds jobs locally and can run them on demand. */
38
+ export interface Drainable {
39
+ readonly size: number;
40
+ work(): Promise<number>;
41
+ }
42
+ /** Runs jobs the moment they're dispatched — the default; ideal for dev/tests. */
43
+ export declare class SyncDriver implements QueueDriver {
44
+ push(job: Dispatchable, _options: JobOptions): Promise<void>;
45
+ }
46
+ export interface QueuedJob {
47
+ job: Dispatchable;
48
+ options: JobOptions;
49
+ }
50
+ /** Holds jobs in memory; `work()` drains them. Assert on `.jobs` in tests. */
51
+ export declare class MemoryDriver implements QueueDriver, Drainable {
52
+ readonly jobs: QueuedJob[];
53
+ push(job: Dispatchable, options: JobOptions): Promise<void>;
54
+ get size(): number;
55
+ /** Run every pending job in FIFO order; returns how many ran. */
56
+ work(): Promise<number>;
57
+ }
58
+ export declare class Queue {
59
+ readonly driver: QueueDriver;
60
+ constructor(driver: QueueDriver);
61
+ /** Place a job on the queue (the driver decides when it runs). */
62
+ dispatch(job: Dispatchable, options?: JobOptions): Promise<void>;
63
+ /** Drain the driver if it holds jobs locally; returns how many ran. */
64
+ work(): Promise<number>;
65
+ }
66
+ /** Register the default queue driver used by `dispatch()`. */
67
+ export declare function setQueue(driver: QueueDriver): Queue;
68
+ /** The default queue instance. */
69
+ export declare function getQueue(): Queue;
70
+ /** Dispatch a job (or function) onto the default queue. */
71
+ export declare function dispatch(job: Dispatchable, options?: JobOptions): Promise<void>;
72
+ /** Drain the default queue's pending jobs (no-op for immediate drivers). */
73
+ export declare function work(): Promise<number>;