@shaferllc/keel 0.59.0 → 0.68.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 (166) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +14 -2
  6. package/dist/core/auth.d.ts +47 -0
  7. package/dist/core/auth.js +77 -0
  8. package/dist/core/authorization.d.ts +9 -1
  9. package/dist/core/authorization.js +22 -2
  10. package/dist/core/cache.d.ts +82 -5
  11. package/dist/core/cache.js +181 -23
  12. package/dist/core/cli/stubs.d.ts +12 -0
  13. package/dist/core/cli/stubs.js +120 -0
  14. package/dist/core/container.d.ts +20 -0
  15. package/dist/core/container.js +52 -0
  16. package/dist/core/cors.d.ts +29 -0
  17. package/dist/core/cors.js +72 -0
  18. package/dist/core/crypto.d.ts +40 -4
  19. package/dist/core/crypto.js +66 -6
  20. package/dist/core/csrf.d.ts +25 -0
  21. package/dist/core/csrf.js +78 -0
  22. package/dist/core/database.d.ts +49 -4
  23. package/dist/core/database.js +89 -21
  24. package/dist/core/events.d.ts +129 -5
  25. package/dist/core/events.js +165 -7
  26. package/dist/core/health.d.ts +141 -0
  27. package/dist/core/health.js +226 -0
  28. package/dist/core/helpers.d.ts +15 -3
  29. package/dist/core/helpers.js +23 -3
  30. package/dist/core/index.d.ts +33 -18
  31. package/dist/core/index.js +16 -8
  32. package/dist/core/lock.d.ts +139 -0
  33. package/dist/core/lock.js +215 -0
  34. package/dist/core/logger.d.ts +82 -4
  35. package/dist/core/logger.js +141 -23
  36. package/dist/core/mail.d.ts +128 -7
  37. package/dist/core/mail.js +264 -16
  38. package/dist/core/model.d.ts +2 -0
  39. package/dist/core/model.js +16 -14
  40. package/dist/core/provider.d.ts +7 -0
  41. package/dist/core/provider.js +7 -0
  42. package/dist/core/queue.d.ts +134 -9
  43. package/dist/core/queue.js +304 -14
  44. package/dist/core/rate-limit.js +3 -0
  45. package/dist/core/relations.js +13 -13
  46. package/dist/core/request.d.ts +26 -0
  47. package/dist/core/request.js +77 -0
  48. package/dist/core/shield.d.ts +39 -0
  49. package/dist/core/shield.js +60 -0
  50. package/dist/core/social.d.ts +173 -0
  51. package/dist/core/social.js +337 -0
  52. package/dist/core/storage.d.ts +159 -6
  53. package/dist/core/storage.js +287 -7
  54. package/dist/core/tokens.d.ts +74 -0
  55. package/dist/core/tokens.js +155 -0
  56. package/dist/db/d1.d.ts +32 -0
  57. package/dist/db/d1.js +26 -0
  58. package/dist/db/libsql.d.ts +29 -0
  59. package/dist/db/libsql.js +32 -0
  60. package/dist/db/pg.d.ts +29 -0
  61. package/dist/db/pg.js +33 -0
  62. package/dist/mcp/server.d.ts +19 -0
  63. package/dist/mcp/server.js +355 -0
  64. package/docs/ai-manifest.json +2472 -0
  65. package/docs/ai.md +128 -0
  66. package/docs/architecture.md +331 -0
  67. package/docs/authentication.md +453 -0
  68. package/docs/authorization.md +167 -0
  69. package/docs/broadcasting.md +137 -0
  70. package/docs/broker.md +500 -0
  71. package/docs/cache.md +558 -0
  72. package/docs/configuration.md +311 -0
  73. package/docs/console.md +356 -0
  74. package/docs/container.md +467 -0
  75. package/docs/controllers.md +265 -0
  76. package/docs/cors.md +51 -0
  77. package/docs/database.md +530 -0
  78. package/docs/debugging.md +129 -0
  79. package/docs/decorators.md +127 -0
  80. package/docs/errors.md +395 -0
  81. package/docs/events.md +496 -0
  82. package/docs/examples/architecture-app.ts +27 -0
  83. package/docs/examples/authentication.ts +61 -0
  84. package/docs/examples/authorization.ts +79 -0
  85. package/docs/examples/broadcasting.ts +60 -0
  86. package/docs/examples/broker-cache-validate.ts +34 -0
  87. package/docs/examples/broker-fault-tolerance.ts +29 -0
  88. package/docs/examples/broker-middleware.ts +27 -0
  89. package/docs/examples/broker.ts +203 -0
  90. package/docs/examples/cache.ts +222 -0
  91. package/docs/examples/configuration.ts +81 -0
  92. package/docs/examples/container.ts +134 -0
  93. package/docs/examples/controllers.ts +86 -0
  94. package/docs/examples/database.ts +118 -0
  95. package/docs/examples/debugging.ts +41 -0
  96. package/docs/examples/decorators.ts +40 -0
  97. package/docs/examples/errors.ts +121 -0
  98. package/docs/examples/events.ts +204 -0
  99. package/docs/examples/factories.ts +84 -0
  100. package/docs/examples/hashing.ts +71 -0
  101. package/docs/examples/health.ts +94 -0
  102. package/docs/examples/helpers.ts +171 -0
  103. package/docs/examples/hooks.ts +54 -0
  104. package/docs/examples/inertia.ts +81 -0
  105. package/docs/examples/locks.ts +120 -0
  106. package/docs/examples/logger.ts +92 -0
  107. package/docs/examples/mail.ts +160 -0
  108. package/docs/examples/middleware.ts +119 -0
  109. package/docs/examples/migrations.ts +126 -0
  110. package/docs/examples/models.ts +239 -0
  111. package/docs/examples/notification.ts +124 -0
  112. package/docs/examples/providers.ts +123 -0
  113. package/docs/examples/queues.ts +254 -0
  114. package/docs/examples/rate-limiting.ts +42 -0
  115. package/docs/examples/redis.ts +99 -0
  116. package/docs/examples/request-response.ts +197 -0
  117. package/docs/examples/routing.ts +186 -0
  118. package/docs/examples/scheduling.ts +62 -0
  119. package/docs/examples/sessions.ts +102 -0
  120. package/docs/examples/static-files.ts +63 -0
  121. package/docs/examples/storage.ts +132 -0
  122. package/docs/examples/templates.ts +58 -0
  123. package/docs/examples/testing.ts +66 -0
  124. package/docs/examples/transformer.ts +141 -0
  125. package/docs/examples/transformers.ts +49 -0
  126. package/docs/examples/url-builder.ts +86 -0
  127. package/docs/examples/validation.ts +102 -0
  128. package/docs/examples/views.tsx +62 -0
  129. package/docs/examples/vite.ts +106 -0
  130. package/docs/factories.md +166 -0
  131. package/docs/getting-started.md +290 -0
  132. package/docs/hashing.md +259 -0
  133. package/docs/health.md +225 -0
  134. package/docs/helpers.md +347 -0
  135. package/docs/hono.md +186 -0
  136. package/docs/hooks.md +118 -0
  137. package/docs/inertia.md +241 -0
  138. package/docs/locks.md +323 -0
  139. package/docs/logger.md +290 -0
  140. package/docs/mail.md +678 -0
  141. package/docs/middleware.md +425 -0
  142. package/docs/migrations.md +476 -0
  143. package/docs/models.md +810 -0
  144. package/docs/notifications.md +474 -0
  145. package/docs/providers.md +363 -0
  146. package/docs/queues.md +679 -0
  147. package/docs/rate-limiting.md +155 -0
  148. package/docs/redis.md +178 -0
  149. package/docs/request-response.md +953 -0
  150. package/docs/routing.md +804 -0
  151. package/docs/scheduling.md +110 -0
  152. package/docs/security.md +85 -0
  153. package/docs/sessions.md +354 -0
  154. package/docs/social-auth.md +174 -0
  155. package/docs/static-files.md +211 -0
  156. package/docs/storage.md +450 -0
  157. package/docs/templates.md +315 -0
  158. package/docs/testing.md +125 -0
  159. package/docs/transformers.md +381 -0
  160. package/docs/url-builder.md +295 -0
  161. package/docs/validation.md +288 -0
  162. package/docs/views.md +267 -0
  163. package/docs/vite.md +434 -0
  164. package/llms-full.txt +17694 -0
  165. package/llms.txt +116 -0
  166. package/package.json +38 -7
package/dist/core/mail.js CHANGED
@@ -13,15 +13,33 @@
13
13
  * .html("<h1>Hi</h1>")
14
14
  * .send();
15
15
  *
16
- * In tests, register an `ArrayTransport` and assert on `transport.sent`.
16
+ * Sending is slow and it fails which is exactly what a queue is for. `sendLater()`
17
+ * puts the message on the queue instead of holding the request open for an SMTP
18
+ * round trip:
19
+ *
20
+ * await mail().to(user.email).subject("Welcome").html(body).sendLater();
21
+ *
22
+ * In tests, `fakeMail()` swaps the transport for one that records and asserts.
23
+ */
24
+ import { logger, emit, hasApplication } from "./helpers.js";
25
+ import { dispatch, Job } from "./queue.js";
26
+ import { contentTypeFor } from "./storage.js";
27
+ /**
28
+ * Fire a mail lifecycle event, but only if there's an application to fire it on —
29
+ * the mailer has to work in a unit test that never bootstrapped one. A listener
30
+ * that throws still surfaces; it's only the missing-app case we skip.
17
31
  */
18
- import { logger } from "./helpers.js";
32
+ async function notify(event, message) {
33
+ if (hasApplication())
34
+ await emit(event, message);
35
+ }
19
36
  function list(value) {
20
37
  if (value === undefined)
21
38
  return [];
22
39
  return Array.isArray(value) ? value : [value];
23
40
  }
24
- /** A fluent, immutable-ish builder chain setters, then `send()`. */
41
+ /* --------------------------------- builder -------------------------------- */
42
+ /** A fluent, immutable-ish builder — chain setters, then `send()` or `sendLater()`. */
25
43
  export class PendingMail {
26
44
  mailer;
27
45
  message = { to: [], subject: "" };
@@ -64,6 +82,29 @@ export class PendingMail {
64
82
  (this.message.headers ??= {})[name] = value;
65
83
  return this;
66
84
  }
85
+ /** Attach a file. The content type is inferred from the extension if omitted. */
86
+ attach(filename, content, contentType) {
87
+ (this.message.attachments ??= []).push({
88
+ filename,
89
+ content,
90
+ contentType: contentType ?? contentTypeFor(filename),
91
+ });
92
+ return this;
93
+ }
94
+ /**
95
+ * Attach an image and give it a content id, so the HTML body can show it inline:
96
+ *
97
+ * .embed("logo", bytes, "logo.png").html(`<img src="cid:logo">`)
98
+ */
99
+ embed(cid, content, filename = cid, contentType) {
100
+ (this.message.attachments ??= []).push({
101
+ filename,
102
+ content,
103
+ contentType: contentType ?? contentTypeFor(filename),
104
+ cid,
105
+ });
106
+ return this;
107
+ }
67
108
  /** Seed several fields at once (merges into what's been chained). */
68
109
  fill(partial) {
69
110
  const { to, cc, bcc, ...rest } = partial;
@@ -76,25 +117,79 @@ export class PendingMail {
76
117
  Object.assign(this.message, rest);
77
118
  return this;
78
119
  }
120
+ /** The message as composed so far — before the mailer applies its defaults. */
121
+ toMessage() {
122
+ return { ...this.message };
123
+ }
79
124
  /** Freeze and hand the message to the mailer. */
80
125
  async send() {
81
126
  return this.mailer.send(this.message);
82
127
  }
128
+ /**
129
+ * Queue the message instead of sending it now — the request returns without
130
+ * waiting on the provider, and a failed send retries on the queue rather than
131
+ * failing the user's action.
132
+ *
133
+ * With the default `SyncDriver` this still sends inline; register a real queue
134
+ * driver to actually defer it.
135
+ */
136
+ async sendLater() {
137
+ await this.mailer.sendLater(this.message);
138
+ }
139
+ }
140
+ /* ------------------------------- class mails ------------------------------ */
141
+ /**
142
+ * A reusable, testable email as a class — the mail equivalent of a `Job`.
143
+ * Implement `build()` to compose the message; the mailer calls it for you.
144
+ *
145
+ * export class WelcomeEmail extends BaseMail {
146
+ * constructor(private user: User) { super(); }
147
+ *
148
+ * build(message: PendingMail) {
149
+ * message.to(this.user.email).subject("Welcome").html(`<h1>Hi ${this.user.name}</h1>`);
150
+ * }
151
+ * }
152
+ *
153
+ * await send(new WelcomeEmail(user)); // or sendLater(new WelcomeEmail(user))
154
+ */
155
+ export class BaseMail {
156
+ }
157
+ /** The job that carries a queued message. Exported so a driver can recognize it. */
158
+ export class SendMailJob extends Job {
159
+ message;
160
+ mailerName;
161
+ constructor(message, mailerName) {
162
+ super();
163
+ this.message = message;
164
+ this.mailerName = mailerName;
165
+ }
166
+ async handle() {
167
+ await mailer(this.mailerName).send(this.message);
168
+ }
83
169
  }
170
+ /* --------------------------------- mailer --------------------------------- */
84
171
  export class Mailer {
85
172
  transport;
86
173
  options;
87
- constructor(transport, options = {}) {
174
+ name;
175
+ constructor(transport, options = {},
176
+ /** The name this mailer is registered under — carried onto queued jobs. */
177
+ name = "default") {
88
178
  this.transport = transport;
89
179
  this.options = options;
180
+ this.name = name;
90
181
  }
91
182
  /** Start composing a message. */
92
183
  message() {
93
184
  return new PendingMail(this);
94
185
  }
95
- /** Validate, apply defaults, and dispatch through the transport. */
96
- async send(message) {
97
- const final = { ...message, from: message.from ?? this.options.from };
186
+ /** Apply this mailer's defaults and check the message is sendable. */
187
+ prepare(message) {
188
+ const final = {
189
+ ...message,
190
+ from: message.from ?? this.options.from,
191
+ replyTo: message.replyTo ?? this.options.replyTo,
192
+ };
98
193
  if (!final.to.length)
99
194
  throw new Error("Mail: at least one recipient (to) is required.");
100
195
  if (!final.subject)
@@ -103,9 +198,29 @@ export class Mailer {
103
198
  throw new Error("Mail: a text or html body is required.");
104
199
  if (!final.from)
105
200
  throw new Error("Mail: a from address is required (set one or a default).");
201
+ return final;
202
+ }
203
+ /** Validate, apply defaults, and dispatch through the transport. */
204
+ async send(message) {
205
+ const final = this.prepare(message);
206
+ await notify("mail.sending", final);
106
207
  await this.transport.send(final);
208
+ await notify("mail.sent", final);
209
+ return final;
210
+ }
211
+ /** Validate, apply defaults, and put the message on the queue. */
212
+ async sendLater(message) {
213
+ // Validate *now*, not on the worker: a malformed message should blow up at
214
+ // the call site, where the stack trace means something.
215
+ const final = this.prepare(message);
216
+ await dispatch(new SendMailJob(final, this.name));
217
+ await notify("mail.queued", final);
107
218
  return final;
108
219
  }
220
+ /** The underlying transport, for provider-specific operations. */
221
+ get driver() {
222
+ return this.transport;
223
+ }
109
224
  }
110
225
  /* ------------------------------- transports ------------------------------- */
111
226
  /** Collects sent messages in memory — the default, and ideal for tests. */
@@ -140,18 +255,151 @@ export function fetchTransport(options) {
140
255
  },
141
256
  };
142
257
  }
258
+ /**
259
+ * A `Mailer` that records instead of delivering, with assertions — what
260
+ * `fakeMail()` installs so tests never talk to a provider.
261
+ */
262
+ export class FakeMailer extends Mailer {
263
+ mails = [];
264
+ constructor(name = "default", options = {}) {
265
+ // A from address so a fake never trips the "from is required" check.
266
+ super(new ArrayTransport(), { from: "fake@example.com", ...options }, name);
267
+ }
268
+ async send(message) {
269
+ const final = await super.send(message);
270
+ this.mails.push({ message: final, queued: false });
271
+ return final;
272
+ }
273
+ async sendLater(message) {
274
+ // Validate exactly as a real mailer would, but don't touch the queue —
275
+ // recording the intent *is* the point.
276
+ const final = this.prepare(message);
277
+ this.mails.push({ message: final, queued: true });
278
+ await notify("mail.queued", final);
279
+ return final;
280
+ }
281
+ /** Everything sent immediately. */
282
+ sent() {
283
+ return this.mails.filter((m) => !m.queued).map((m) => m.message);
284
+ }
285
+ /** Everything queued with `sendLater()`. */
286
+ queued() {
287
+ return this.mails.filter((m) => m.queued).map((m) => m.message);
288
+ }
289
+ assertSent(where) {
290
+ const matches = this.sent().filter((m) => where?.(m) ?? true);
291
+ if (matches.length)
292
+ return;
293
+ throw new Error(where && this.sent().length
294
+ ? `Expected a sent mail matching the predicate. ${this.sent().length} were sent, but none matched.`
295
+ : `Expected a mail to be sent, but none was. ${this.summary()}`);
296
+ }
297
+ assertNotSent(where) {
298
+ const found = this.sent().filter((m) => where?.(m) ?? true).length;
299
+ if (found)
300
+ throw new Error(`Expected no matching mail to be sent, but ${found} was.`);
301
+ }
302
+ assertSentCount(expected) {
303
+ const found = this.sent().length;
304
+ if (found !== expected) {
305
+ throw new Error(`Expected ${expected} mail(s) to be sent, but ${found} were.`);
306
+ }
307
+ }
308
+ assertQueued(where) {
309
+ const matches = this.queued().filter((m) => where?.(m) ?? true);
310
+ if (matches.length)
311
+ return;
312
+ throw new Error(where && this.queued().length
313
+ ? `Expected a queued mail matching the predicate. ${this.queued().length} were queued, but none matched.`
314
+ : `Expected a mail to be queued, but none was. ${this.summary()}`);
315
+ }
316
+ assertNotQueued(where) {
317
+ const found = this.queued().filter((m) => where?.(m) ?? true).length;
318
+ if (found)
319
+ throw new Error(`Expected no matching mail to be queued, but ${found} was.`);
320
+ }
321
+ assertQueuedCount(expected) {
322
+ const found = this.queued().length;
323
+ if (found !== expected) {
324
+ throw new Error(`Expected ${expected} mail(s) to be queued, but ${found} were.`);
325
+ }
326
+ }
327
+ /** Nothing sent and nothing queued. */
328
+ assertNothingSent() {
329
+ if (this.mails.length) {
330
+ throw new Error(`Expected no mail at all, but ${this.mails.length} were recorded. ${this.summary()}`);
331
+ }
332
+ }
333
+ summary() {
334
+ if (!this.mails.length)
335
+ return "No mail was sent or queued.";
336
+ const subjects = this.mails.map((m) => `"${m.message.subject}"`).join(", ");
337
+ return `Recorded: ${subjects}.`;
338
+ }
339
+ }
143
340
  /* --------------------------------- global --------------------------------- */
144
- let mailer = new Mailer(new ArrayTransport());
145
- /** Register the default mailer used by `mail()`. */
146
- export function setMailer(transport, options = {}) {
147
- mailer = new Mailer(transport, options);
148
- return mailer;
341
+ const mailers = new Map([["default", new Mailer(new ArrayTransport())]]);
342
+ /** Mailers displaced by `fakeMail()`, so `restoreMail()` can put them back. */
343
+ const realMailers = new Map();
344
+ /** Register a mailer, optionally under a name (default: `"default"`). */
345
+ export function setMailer(transport, options = {}, name = "default") {
346
+ const instance = new Mailer(transport, options, name);
347
+ mailers.set(name, instance);
348
+ return instance;
349
+ }
350
+ /** The default mailer, or a named one registered with `setMailer(…, …, name)`. */
351
+ export function mailer(name = "default") {
352
+ const instance = mailers.get(name);
353
+ if (!instance)
354
+ throw new Error(`No mailer named "${name}". Register it with setMailer().`);
355
+ return instance;
149
356
  }
150
357
  /** The default mailer instance. */
151
358
  export function getMailer() {
152
- return mailer;
359
+ return mailer();
360
+ }
361
+ /** Start a message on a mailer: `mail().to(…).subject(…).send()`. */
362
+ export function mail(name = "default") {
363
+ return mailer(name).message();
364
+ }
365
+ /** Build a `BaseMail` into a message on the given mailer. */
366
+ async function build(email, name) {
367
+ const pending = mailer(name).message();
368
+ await email.build(pending);
369
+ return pending.toMessage();
153
370
  }
154
- /** Start a message on the default mailer: `mail().to(…).subject(…).send()`. */
155
- export function mail() {
156
- return mailer.message();
371
+ /** Send a class-based mail now. */
372
+ export async function send(email, name = "default") {
373
+ return mailer(name).send(await build(email, name));
374
+ }
375
+ /** Queue a class-based mail. */
376
+ export async function sendLater(email, name = "default") {
377
+ return mailer(name).sendLater(await build(email, name));
378
+ }
379
+ /**
380
+ * Swap a mailer for a `FakeMailer` so tests never talk to a provider, and assert
381
+ * on what was sent or queued. Undo with `restoreMail()`.
382
+ *
383
+ * const mail = fakeMail();
384
+ * await registerUser();
385
+ * mail.assertQueued((m) => m.subject === "Welcome");
386
+ */
387
+ export function fakeMail(name = "default") {
388
+ const existing = mailers.get(name);
389
+ // Only remember the *real* mailer — faking twice must not stash a fake.
390
+ if (existing && !realMailers.has(name))
391
+ realMailers.set(name, existing);
392
+ const fake = new FakeMailer(name);
393
+ mailers.set(name, fake);
394
+ return fake;
395
+ }
396
+ /** Restore the real mailer after `fakeMail()`. With no name, restores them all. */
397
+ export function restoreMail(name) {
398
+ const names = name ? [name] : [...realMailers.keys()];
399
+ for (const key of names) {
400
+ const real = realMailers.get(key);
401
+ if (real)
402
+ mailers.set(key, real);
403
+ realMailers.delete(key);
404
+ }
157
405
  }
@@ -22,6 +22,8 @@ type ModelClass<T extends Model> = (new (attributes?: Row) => T) & typeof Model;
22
22
  export declare class Model {
23
23
  static table: string;
24
24
  static primaryKey: string;
25
+ /** Which registered connection this model uses; the default when unset. */
26
+ static connection: string | undefined;
25
27
  /** Columns mass-assignable via `create`/`fill` (allowlist). */
26
28
  static fillable: string[];
27
29
  /** Columns NOT mass-assignable (denylist). Ignored when `fillable` is set. */
@@ -34,6 +34,8 @@ function serialize(value) {
34
34
  export class Model {
35
35
  static table = "";
36
36
  static primaryKey = "id";
37
+ /** Which registered connection this model uses; the default when unset. */
38
+ static connection = undefined;
37
39
  /** Columns mass-assignable via `create`/`fill` (allowlist). */
38
40
  static fillable = [];
39
41
  /** Columns NOT mass-assignable (denylist). Ignored when `fillable` is set. */
@@ -51,7 +53,7 @@ export class Model {
51
53
  /* ------------------------------ static -------------------------------- */
52
54
  /** A raw query builder scoped to this model's table. */
53
55
  static query() {
54
- return db(this.table);
56
+ return db(this.table, this.connection);
55
57
  }
56
58
  /** Keep only the attributes mass-assignment allows (fillable / guarded). */
57
59
  static filterFillable(attributes) {
@@ -86,11 +88,11 @@ export class Model {
86
88
  return out;
87
89
  }
88
90
  static async all() {
89
- const rows = await db(this.table).get();
91
+ const rows = await db(this.table, this.connection).get();
90
92
  return rows.map((row) => new this(row));
91
93
  }
92
94
  static async find(id) {
93
- const row = await db(this.table).where(this.primaryKey, id).first();
95
+ const row = await db(this.table, this.connection).where(this.primaryKey, id).first();
94
96
  return row ? new this(row) : null;
95
97
  }
96
98
  static async findOrFail(id) {
@@ -100,18 +102,18 @@ export class Model {
100
102
  return model;
101
103
  }
102
104
  static async first() {
103
- const row = await db(this.table).first();
105
+ const row = await db(this.table, this.connection).first();
104
106
  return row ? new this(row) : null;
105
107
  }
106
108
  /** Fetch models matching a simple equality condition. */
107
109
  static async where(column, value) {
108
- const rows = await db(this.table).where(column, value).get();
110
+ const rows = await db(this.table, this.connection).where(column, value).get();
109
111
  return rows.map((row) => new this(row));
110
112
  }
111
113
  static async create(attributes) {
112
114
  const filtered = this.filterFillable(attributes);
113
115
  const write = this.stampTimestamps(this.toDatabase(filtered), true);
114
- const id = await db(this.table).insertGetId(write);
116
+ const id = await db(this.table, this.connection).insertGetId(write);
115
117
  const model = new this(filtered);
116
118
  if (this.timestamps) {
117
119
  model[this.createdAtColumn] = write[this.createdAtColumn];
@@ -123,7 +125,7 @@ export class Model {
123
125
  }
124
126
  /** A page of models plus pagination metadata. */
125
127
  static async paginate(page = 1, perPage = 15) {
126
- const result = await db(this.table).paginate(page, perPage);
128
+ const result = await db(this.table, this.connection).paginate(page, perPage);
127
129
  return { ...result, data: result.data.map((row) => new this(row)) };
128
130
  }
129
131
  /** Find the first row matching `match`, or create one from `{ ...match, ...values }`. */
@@ -145,7 +147,7 @@ export class Model {
145
147
  }
146
148
  /** A query scoped to every column/value in `match`. */
147
149
  static matching(match) {
148
- let q = db(this.table);
150
+ let q = db(this.table, this.connection);
149
151
  for (const [column, value] of Object.entries(match))
150
152
  q = q.where(column, value);
151
153
  return q;
@@ -209,7 +211,7 @@ export class Model {
209
211
  /** Insert (no primary key) or update (has one). */
210
212
  async save() {
211
213
  const ctor = this.ctor();
212
- const { table, primaryKey } = ctor;
214
+ const { table, primaryKey, connection } = ctor;
213
215
  const idValue = this[primaryKey];
214
216
  const forInsert = idValue == null;
215
217
  const data = ctor.stampTimestamps(ctor.toDatabase({ ...this }), forInsert);
@@ -221,10 +223,10 @@ export class Model {
221
223
  }
222
224
  delete data[primaryKey];
223
225
  if (!forInsert) {
224
- await db(table).where(primaryKey, idValue).update(data);
226
+ await db(table, connection).where(primaryKey, idValue).update(data);
225
227
  }
226
228
  else {
227
- const id = await db(table).insertGetId(data);
229
+ const id = await db(table, connection).insertGetId(data);
228
230
  if (id != null)
229
231
  this[primaryKey] = id;
230
232
  }
@@ -237,14 +239,14 @@ export class Model {
237
239
  /** Reload this model's columns from the database. */
238
240
  async refresh() {
239
241
  const ctor = this.ctor();
240
- const row = await db(ctor.table).where(ctor.primaryKey, this[ctor.primaryKey]).first();
242
+ const row = await db(ctor.table, ctor.connection).where(ctor.primaryKey, this[ctor.primaryKey]).first();
241
243
  if (row)
242
244
  Object.assign(this, applyCasts(row, ctor.casts, castGet));
243
245
  return this;
244
246
  }
245
247
  async delete() {
246
- const { table, primaryKey } = this.ctor();
247
- await db(table).where(primaryKey, this[primaryKey]).delete();
248
+ const { table, primaryKey, connection } = this.ctor();
249
+ await db(table, connection).where(primaryKey, this[primaryKey]).delete();
248
250
  }
249
251
  /** Merge mass-assignable attributes into the model (cast, not saved). */
250
252
  fill(attributes) {
@@ -6,6 +6,11 @@
6
6
  * here — nothing is guaranteed to be registered yet.
7
7
  * boot(): called after every provider has registered. Safe to resolve
8
8
  * and wire things together here.
9
+ * ready(): called once the whole app has booted (after every provider's
10
+ * boot() and any onReady hooks). For work that needs a fully-live
11
+ * app — attaching to the running server, warming a cache.
12
+ * shutdown(): called during graceful termination, in reverse registration
13
+ * order (LIFO). Close connections, flush queues, cancel timers.
9
14
  *
10
15
  * Register with options to make a provider reusable:
11
16
  *
@@ -21,5 +26,7 @@ export declare abstract class ServiceProvider<O = Record<string, unknown>> {
21
26
  constructor(app: Application, options?: O);
22
27
  register(): void | Promise<void>;
23
28
  boot(): void | Promise<void>;
29
+ ready(): void | Promise<void>;
30
+ shutdown(): void | Promise<void>;
24
31
  }
25
32
  export type ProviderClass = new (app: Application, options?: any) => ServiceProvider;
@@ -6,6 +6,11 @@
6
6
  * here — nothing is guaranteed to be registered yet.
7
7
  * boot(): called after every provider has registered. Safe to resolve
8
8
  * and wire things together here.
9
+ * ready(): called once the whole app has booted (after every provider's
10
+ * boot() and any onReady hooks). For work that needs a fully-live
11
+ * app — attaching to the running server, warming a cache.
12
+ * shutdown(): called during graceful termination, in reverse registration
13
+ * order (LIFO). Close connections, flush queues, cancel timers.
9
14
  *
10
15
  * Register with options to make a provider reusable:
11
16
  *
@@ -23,4 +28,6 @@ export class ServiceProvider {
23
28
  }
24
29
  register() { }
25
30
  boot() { }
31
+ ready() { }
32
+ shutdown() { }
26
33
  }