@shaferllc/keel 0.36.0 → 0.58.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 (55) hide show
  1. package/README.md +14 -2
  2. package/dist/core/application.d.ts +58 -2
  3. package/dist/core/application.js +99 -3
  4. package/dist/core/authorization.d.ts +52 -0
  5. package/dist/core/authorization.js +97 -0
  6. package/dist/core/broadcasting.d.ts +49 -0
  7. package/dist/core/broadcasting.js +84 -0
  8. package/dist/core/broker.d.ts +398 -0
  9. package/dist/core/broker.js +602 -0
  10. package/dist/core/crypto.d.ts +1 -1
  11. package/dist/core/crypto.js +12 -3
  12. package/dist/core/database.d.ts +26 -1
  13. package/dist/core/database.js +65 -2
  14. package/dist/core/decorators.d.ts +39 -0
  15. package/dist/core/decorators.js +72 -0
  16. package/dist/core/exceptions.d.ts +77 -6
  17. package/dist/core/exceptions.js +168 -10
  18. package/dist/core/helpers.d.ts +6 -0
  19. package/dist/core/helpers.js +12 -0
  20. package/dist/core/http/kernel.js +14 -2
  21. package/dist/core/http/router.d.ts +14 -0
  22. package/dist/core/http/router.js +30 -1
  23. package/dist/core/index.d.ts +28 -6
  24. package/dist/core/index.js +15 -3
  25. package/dist/core/logger.d.ts +5 -0
  26. package/dist/core/logger.js +24 -2
  27. package/dist/core/migrations.js +3 -3
  28. package/dist/core/model.d.ts +19 -1
  29. package/dist/core/model.js +72 -4
  30. package/dist/core/provider.d.ts +13 -4
  31. package/dist/core/provider.js +12 -2
  32. package/dist/core/redis.d.ts +78 -0
  33. package/dist/core/redis.js +176 -0
  34. package/dist/core/request-logger.d.ts +26 -0
  35. package/dist/core/request-logger.js +48 -0
  36. package/dist/core/request.d.ts +17 -1
  37. package/dist/core/request.js +27 -1
  38. package/dist/core/scheduler.d.ts +60 -0
  39. package/dist/core/scheduler.js +166 -0
  40. package/dist/core/session.js +17 -2
  41. package/dist/core/storage.d.ts +57 -0
  42. package/dist/core/storage.js +98 -0
  43. package/dist/core/template.d.ts +50 -0
  44. package/dist/core/template.js +753 -0
  45. package/dist/core/testing.d.ts +54 -0
  46. package/dist/core/testing.js +141 -0
  47. package/dist/core/transformer.d.ts +89 -0
  48. package/dist/core/transformer.js +152 -0
  49. package/dist/core/validation.d.ts +20 -0
  50. package/dist/core/validation.js +52 -1
  51. package/dist/core/vite.d.ts +117 -0
  52. package/dist/core/vite.js +258 -0
  53. package/dist/vite/index.d.ts +40 -0
  54. package/dist/vite/index.js +146 -0
  55. package/package.json +16 -1
@@ -70,6 +70,11 @@ export class Route {
70
70
  this.def.domain = pattern;
71
71
  return this;
72
72
  }
73
+ /** Attach arbitrary metadata to the route — read it via `request.route.config`. */
74
+ config(data) {
75
+ Object.assign(this.def.config, data);
76
+ return this;
77
+ }
73
78
  }
74
79
  /** A group of routes sharing a prefix, middleware, and/or name prefix. */
75
80
  export class RouteGroup {
@@ -93,6 +98,12 @@ export class RouteGroup {
93
98
  use(mw) {
94
99
  return this.middleware(mw);
95
100
  }
101
+ /** Attach metadata to every route in the group (a route's own config wins). */
102
+ config(data) {
103
+ for (const r of this.routes)
104
+ r.config = { ...data, ...r.config };
105
+ return this;
106
+ }
96
107
  /** Constrain a parameter across every route in the group. */
97
108
  where(param, matcher) {
98
109
  for (const r of this.routes) {
@@ -317,10 +328,26 @@ export class Router {
317
328
  handler,
318
329
  middleware: [...this.group_mw],
319
330
  wheres: {},
331
+ config: {},
320
332
  };
321
333
  this.routes.push(def);
334
+ for (const hook of this.routeHooks)
335
+ hook(def);
322
336
  return new Route(def);
323
337
  }
338
+ routeHooks = [];
339
+ /**
340
+ * Observe route registration — called with each route's definition as it's
341
+ * added (and replayed for routes already registered). Handy for logging or
342
+ * building an API map. The `def` is live, so reading it later reflects fluent
343
+ * config (`.name()`, `.middleware()`) applied after registration.
344
+ */
345
+ onRoute(hook) {
346
+ for (const def of this.routes)
347
+ hook(def);
348
+ this.routeHooks.push(hook);
349
+ return this;
350
+ }
324
351
  /** Register a global parameter constraint, applied to every matching route. */
325
352
  where(param, matcher) {
326
353
  this.globalWheres[param] = matcherSource(matcher);
@@ -344,7 +371,9 @@ export class Router {
344
371
  throw new Error(`No route named [${name}].`);
345
372
  let path = def.path;
346
373
  for (const [k, v] of Object.entries(params)) {
347
- path = path.replace(new RegExp(`:${k}\\??`), encodeURIComponent(String(v)));
374
+ // Global + word-boundary: replace every `:k` occurrence, and don't let
375
+ // `:id` match inside `:idx`.
376
+ path = path.replace(new RegExp(`:${k}\\b\\??`, "g"), encodeURIComponent(String(v)));
348
377
  }
349
378
  path = path.replace(/\/:[^/]+\?/g, "").replace(/:[^/]+/g, "");
350
379
  if (options.qs && Object.keys(options.qs).length) {
@@ -2,23 +2,27 @@
2
2
  export { Container } from "./container.js";
3
3
  export type { Token, Constructor, Factory } from "./container.js";
4
4
  export { Application } from "./application.js";
5
- export type { BootOptions } from "./application.js";
5
+ export type { BootOptions, LifecycleHook, Configurator } from "./application.js";
6
6
  export { Config, env } from "./config.js";
7
- export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
7
+ export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
8
8
  export { Logger } from "./logger.js";
9
9
  export type { LogLevel, LoggerOptions } from "./logger.js";
10
+ export { requestLogger, requestLog } from "./request-logger.js";
11
+ export type { RequestLoggerOptions } from "./request-logger.js";
10
12
  export { Events } from "./events.js";
11
13
  export type { Listener } from "./events.js";
12
14
  export { Cache, MemoryStore } from "./cache.js";
13
15
  export type { CacheStore } from "./cache.js";
14
16
  export { serveStatic } from "./static.js";
15
17
  export type { StaticOptions } from "./static.js";
18
+ export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
19
+ export type { Disk, Contents } from "./storage.js";
16
20
  export { dump, dd } from "./debug.js";
17
21
  export { hash, encryption } from "./crypto.js";
18
22
  export { rateLimiter } from "./rate-limit.js";
19
23
  export type { RateLimiterOptions } from "./rate-limit.js";
20
24
  export { db, setConnection, QueryBuilder } from "./database.js";
21
- export type { Connection, WriteResult, Row, Dialect, Operator } from "./database.js";
25
+ export type { Connection, WriteResult, Row, Dialect, Operator, Paginated } from "./database.js";
22
26
  export { Model } from "./model.js";
23
27
  export type { CastType, Casts } from "./casts.js";
24
28
  export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
@@ -28,14 +32,23 @@ export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail
28
32
  export type { Message, Transport, MailerOptions, FetchTransportOptions } from "./mail.js";
29
33
  export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
30
34
  export type { Dispatchable, JobOptions, QueueDriver, Drainable, QueuedJob } from "./queue.js";
35
+ export { Scheduler, ScheduledTask, scheduler, setScheduler, schedule, cronMatches } from "./scheduler.js";
36
+ export { MemoryBroadcaster, broadcast, setBroadcaster, getBroadcaster, channelAuth, authorizeChannel, clearChannels, } from "./broadcasting.js";
37
+ export type { Broadcaster, Subscriber, ChannelAuthorizer } from "./broadcasting.js";
38
+ export { Redis, MemoryRedis, redis, setRedis, redisStore } from "./redis.js";
39
+ export type { RedisConnection, SetOptions } from "./redis.js";
31
40
  export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
32
41
  export type { Notifiable, MailContent, Channel } from "./notification.js";
33
42
  export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
34
43
  export type { Migration } from "./migrations.js";
35
44
  export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
45
+ export { decorateRequest, hasRequestDecorator, decorated, setRequestValue, clearRequestDecorators, } from "./decorators.js";
46
+ export type { RequestResolver } from "./decorators.js";
36
47
  export type { ConfigData } from "./config.js";
37
48
  export { View } from "./view.js";
38
49
  export type { Renderable, ViewConfig } from "./view.js";
50
+ export { TemplateEngine, escapeHtml, templates, setTemplateEngine, render, } from "./template.js";
51
+ export type { Filter, RenderContext } from "./template.js";
39
52
  export { ServiceProvider } from "./provider.js";
40
53
  export type { ProviderClass } from "./provider.js";
41
54
  export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
@@ -43,10 +56,19 @@ export type { Ctx, RouteHandler, RouteDefinition, Method, Matcher, MiddlewareRef
43
56
  export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
44
57
  export type { InertiaPage, InertiaOptions } from "./inertia.js";
45
58
  export { HttpKernel } from "./http/kernel.js";
46
- export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
47
- export { validate } from "./validation.js";
48
- export type { Schema } from "./validation.js";
59
+ export { TestClient, TestResponse, testClient } from "./testing.js";
60
+ export { HttpException, BadRequestException, UnauthorizedException, PaymentRequiredException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, LengthRequiredException, ValidationException, TooManyRequestsException, ServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, createError, STATUS_TEXT, } from "./exceptions.js";
61
+ export { validate, validateRequest, validated } from "./validation.js";
62
+ export type { Schema, RequestSchemas } from "./validation.js";
49
63
  export { Session, session, sessionMiddleware } from "./session.js";
50
64
  export type { SessionOptions } from "./session.js";
51
65
  export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
52
66
  export type { UserProvider } from "./auth.js";
67
+ export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
68
+ export type { GateCallback, BeforeCallback } from "./authorization.js";
69
+ export { Transformer } from "./transformer.js";
70
+ export type { Attributes, DocumentOptions } from "./transformer.js";
71
+ export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
72
+ export type { ServiceSchema, Context, ActionHandler, EventHandler, ActionDef, ActionSchema, ActionHooks, EventSchema, ServiceHooks, Visibility, EventType, BeforeHook, AfterHook, ErrorHook, Transporter, BrokerOptions, BrokerMiddleware, CallOptions, EmitOptions, MCallDefs, MCallOptions, } from "./broker.js";
73
+ export { Vite, viteTags, viteAsset, viteReactRefresh } from "./vite.js";
74
+ export type { ViteOptions, Manifest, ManifestChunk, Attributes as ViteAttributes, AttrValue, } from "./vite.js";
@@ -2,11 +2,13 @@
2
2
  export { Container } from "./container.js";
3
3
  export { Application } from "./application.js";
4
4
  export { Config, env } from "./config.js";
5
- export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
5
+ export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
6
6
  export { Logger } from "./logger.js";
7
+ export { requestLogger, requestLog } from "./request-logger.js";
7
8
  export { Events } from "./events.js";
8
9
  export { Cache, MemoryStore } from "./cache.js";
9
10
  export { serveStatic } from "./static.js";
11
+ export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
10
12
  export { dump, dd } from "./debug.js";
11
13
  export { hash, encryption } from "./crypto.js";
12
14
  export { rateLimiter } from "./rate-limit.js";
@@ -16,15 +18,25 @@ export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations
16
18
  export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
17
19
  export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
18
20
  export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
21
+ export { Scheduler, ScheduledTask, scheduler, setScheduler, schedule, cronMatches } from "./scheduler.js";
22
+ export { MemoryBroadcaster, broadcast, setBroadcaster, getBroadcaster, channelAuth, authorizeChannel, clearChannels, } from "./broadcasting.js";
23
+ export { Redis, MemoryRedis, redis, setRedis, redisStore } from "./redis.js";
19
24
  export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
20
25
  export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
21
26
  export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
27
+ export { decorateRequest, hasRequestDecorator, decorated, setRequestValue, clearRequestDecorators, } from "./decorators.js";
22
28
  export { View } from "./view.js";
29
+ export { TemplateEngine, escapeHtml, templates, setTemplateEngine, render, } from "./template.js";
23
30
  export { ServiceProvider } from "./provider.js";
24
31
  export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
25
32
  export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
26
33
  export { HttpKernel } from "./http/kernel.js";
27
- export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
28
- export { validate } from "./validation.js";
34
+ export { TestClient, TestResponse, testClient } from "./testing.js";
35
+ export { HttpException, BadRequestException, UnauthorizedException, PaymentRequiredException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, LengthRequiredException, ValidationException, TooManyRequestsException, ServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, createError, STATUS_TEXT, } from "./exceptions.js";
36
+ export { validate, validateRequest, validated } from "./validation.js";
29
37
  export { Session, session, sessionMiddleware } from "./session.js";
30
38
  export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
39
+ export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
40
+ export { Transformer } from "./transformer.js";
41
+ export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
42
+ export { Vite, viteTags, viteAsset, viteReactRefresh } from "./vite.js";
@@ -14,6 +14,11 @@ export interface LoggerOptions {
14
14
  pretty?: boolean;
15
15
  /** Fields merged into every log line. */
16
16
  bindings?: Record<string, unknown>;
17
+ /**
18
+ * Field paths to redact from log output — top-level keys (`"password"`) or dot
19
+ * paths (`"req.headers.authorization"`). Matched values become `"[redacted]"`.
20
+ */
21
+ redact?: string[];
17
22
  }
18
23
  export declare class Logger {
19
24
  private options;
@@ -7,6 +7,24 @@
7
7
  * logger().error("payment failed", { orderId, error });
8
8
  */
9
9
  const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
10
+ const REDACTED = "[redacted]";
11
+ /** Return a copy of `obj` with `path` (dot-separated) redacted — clones only along the path. */
12
+ function redactPath(obj, keys) {
13
+ const [head, ...rest] = keys;
14
+ if (head === undefined || !(head in obj))
15
+ return obj;
16
+ const clone = { ...obj };
17
+ if (rest.length === 0) {
18
+ clone[head] = REDACTED;
19
+ }
20
+ else {
21
+ const child = clone[head];
22
+ if (child && typeof child === "object") {
23
+ clone[head] = redactPath(child, rest);
24
+ }
25
+ }
26
+ return clone;
27
+ }
10
28
  export class Logger {
11
29
  options;
12
30
  threshold;
@@ -18,13 +36,17 @@ export class Logger {
18
36
  if (LEVELS[level] < this.threshold)
19
37
  return;
20
38
  const time = new Date().toISOString();
39
+ let fields = { ...this.options.bindings, ...context };
40
+ for (const path of this.options.redact ?? []) {
41
+ fields = redactPath(fields, path.split("."));
42
+ }
21
43
  if (this.options.pretty) {
22
- const extra = context ? " " + JSON.stringify(context) : "";
44
+ const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
23
45
  const fn = level === "warn" ? console.warn : level === "error" ? console.error : console.log;
24
46
  fn(`[${time}] ${level.toUpperCase().padEnd(5)} ${message}${extra}`);
25
47
  }
26
48
  else {
27
- console.log(JSON.stringify({ level, time, msg: message, ...this.options.bindings, ...context }));
49
+ console.log(JSON.stringify({ level, time, msg: message, ...fields }));
28
50
  }
29
51
  }
30
52
  debug(message, context) {
@@ -165,11 +165,11 @@ export class Migrator {
165
165
  /** Names of migrations already applied. */
166
166
  async ran() {
167
167
  await this.ensure();
168
- const rows = await this.conn.select("SELECT name FROM migrations", []);
168
+ const rows = (await this.conn.select("SELECT name FROM migrations", []));
169
169
  return rows.map((r) => String(r.name));
170
170
  }
171
171
  async maxBatch() {
172
- const rows = await this.conn.select("SELECT MAX(batch) AS b FROM migrations", []);
172
+ const rows = (await this.conn.select("SELECT MAX(batch) AS b FROM migrations", []));
173
173
  return Number(rows[0]?.b ?? 0);
174
174
  }
175
175
  /** Run all pending migrations. Returns the names applied. */
@@ -196,7 +196,7 @@ export class Migrator {
196
196
  const batch = await this.maxBatch();
197
197
  if (!batch)
198
198
  return [];
199
- const rows = await this.conn.select(this.ph("SELECT name FROM migrations WHERE batch = ?"), [batch]);
199
+ const rows = (await this.conn.select(this.ph("SELECT name FROM migrations WHERE batch = ?"), [batch]));
200
200
  const schema = new SchemaBuilder(this.conn, this.dialect);
201
201
  const rolled = [];
202
202
  for (const name of rows.map((r) => String(r.name)).reverse()) {
@@ -15,7 +15,7 @@
15
15
  * user.email = "new@x.com";
16
16
  * await user.save();
17
17
  */
18
- import { type QueryBuilder, type Row } from "./database.js";
18
+ import { type QueryBuilder, type Row, type Paginated } from "./database.js";
19
19
  import { BelongsTo, BelongsToMany, HasMany, HasOne } from "./relations.js";
20
20
  import { type Casts } from "./casts.js";
21
21
  type ModelClass<T extends Model> = (new (attributes?: Row) => T) & typeof Model;
@@ -28,6 +28,10 @@ export declare class Model {
28
28
  static guarded: string[];
29
29
  /** Column -> cast type; values round-trip as real JS types. */
30
30
  static casts: Casts;
31
+ /** Auto-manage `created_at` / `updated_at` on write. Off by default. */
32
+ static timestamps: boolean;
33
+ static createdAtColumn: string;
34
+ static updatedAtColumn: string;
31
35
  [key: string]: unknown;
32
36
  constructor(attributes?: Row);
33
37
  /** A raw query builder scoped to this model's table. */
@@ -36,6 +40,8 @@ export declare class Model {
36
40
  static filterFillable(attributes: Row): Row;
37
41
  /** Cast attributes to their storable primitives for a write. */
38
42
  static toDatabase(attributes: Row): Row;
43
+ /** Stamp created_at/updated_at onto a write payload when timestamps are on. */
44
+ static stampTimestamps(data: Row, forInsert: boolean): Row;
39
45
  static all<T extends Model>(this: ModelClass<T>): Promise<T[]>;
40
46
  static find<T extends Model>(this: ModelClass<T>, id: unknown): Promise<T | null>;
41
47
  static findOrFail<T extends Model>(this: ModelClass<T>, id: unknown): Promise<T>;
@@ -43,6 +49,14 @@ export declare class Model {
43
49
  /** Fetch models matching a simple equality condition. */
44
50
  static where<T extends Model>(this: ModelClass<T>, column: string, value: unknown): Promise<T[]>;
45
51
  static create<T extends Model>(this: ModelClass<T>, attributes: Row): Promise<T>;
52
+ /** A page of models plus pagination metadata. */
53
+ static paginate<T extends Model>(this: ModelClass<T>, page?: number, perPage?: number): Promise<Paginated<T>>;
54
+ /** Find the first row matching `match`, or create one from `{ ...match, ...values }`. */
55
+ static firstOrCreate<T extends Model>(this: ModelClass<T>, match: Row, values?: Row): Promise<T>;
56
+ /** Update the first row matching `match` with `values`, or create it. */
57
+ static updateOrCreate<T extends Model>(this: ModelClass<T>, match: Row, values?: Row): Promise<T>;
58
+ /** A query scoped to every column/value in `match`. */
59
+ private static matching;
46
60
  /**
47
61
  * Eager-load relations onto an array of models with one extra query each —
48
62
  * the fix for N+1. Each name must be a relationship method on the model.
@@ -65,6 +79,10 @@ export declare class Model {
65
79
  getRelation<T = unknown>(name: string): T | undefined;
66
80
  /** Insert (no primary key) or update (has one). */
67
81
  save(): Promise<this>;
82
+ /** Mass-assign then save — `fill` + `save` in one call. */
83
+ update(attributes: Row): Promise<this>;
84
+ /** Reload this model's columns from the database. */
85
+ refresh(): Promise<this>;
68
86
  delete(): Promise<void>;
69
87
  /** Merge mass-assignable attributes into the model (cast, not saved). */
70
88
  fill(attributes: Row): this;
@@ -40,6 +40,10 @@ export class Model {
40
40
  static guarded = [];
41
41
  /** Column -> cast type; values round-trip as real JS types. */
42
42
  static casts = {};
43
+ /** Auto-manage `created_at` / `updated_at` on write. Off by default. */
44
+ static timestamps = false;
45
+ static createdAtColumn = "created_at";
46
+ static updatedAtColumn = "updated_at";
43
47
  constructor(attributes = {}) {
44
48
  // Hydration is unguarded (rows come from the database) but always cast.
45
49
  Object.assign(this, applyCasts(attributes, this.constructor.casts, castGet));
@@ -70,6 +74,17 @@ export class Model {
70
74
  static toDatabase(attributes) {
71
75
  return applyCasts(attributes, this.casts, castSet);
72
76
  }
77
+ /** Stamp created_at/updated_at onto a write payload when timestamps are on. */
78
+ static stampTimestamps(data, forInsert) {
79
+ if (!this.timestamps)
80
+ return data;
81
+ const now = new Date().toISOString();
82
+ const out = { ...data };
83
+ if (forInsert)
84
+ out[this.createdAtColumn] = now;
85
+ out[this.updatedAtColumn] = now;
86
+ return out;
87
+ }
73
88
  static async all() {
74
89
  const rows = await db(this.table).get();
75
90
  return rows.map((row) => new this(row));
@@ -95,12 +110,46 @@ export class Model {
95
110
  }
96
111
  static async create(attributes) {
97
112
  const filtered = this.filterFillable(attributes);
98
- const id = await db(this.table).insertGetId(this.toDatabase(filtered));
113
+ const write = this.stampTimestamps(this.toDatabase(filtered), true);
114
+ const id = await db(this.table).insertGetId(write);
99
115
  const model = new this(filtered);
116
+ if (this.timestamps) {
117
+ model[this.createdAtColumn] = write[this.createdAtColumn];
118
+ model[this.updatedAtColumn] = write[this.updatedAtColumn];
119
+ }
100
120
  if (id != null)
101
121
  model[this.primaryKey] = id;
102
122
  return model;
103
123
  }
124
+ /** A page of models plus pagination metadata. */
125
+ static async paginate(page = 1, perPage = 15) {
126
+ const result = await db(this.table).paginate(page, perPage);
127
+ return { ...result, data: result.data.map((row) => new this(row)) };
128
+ }
129
+ /** Find the first row matching `match`, or create one from `{ ...match, ...values }`. */
130
+ static async firstOrCreate(match, values = {}) {
131
+ const row = await this.matching(match).first();
132
+ if (row)
133
+ return new this(row);
134
+ return this.create({ ...match, ...values });
135
+ }
136
+ /** Update the first row matching `match` with `values`, or create it. */
137
+ static async updateOrCreate(match, values = {}) {
138
+ const row = await this.matching(match).first();
139
+ if (row) {
140
+ const model = new this(row);
141
+ await model.forceFill(values).save();
142
+ return model;
143
+ }
144
+ return this.create({ ...match, ...values });
145
+ }
146
+ /** A query scoped to every column/value in `match`. */
147
+ static matching(match) {
148
+ let q = db(this.table);
149
+ for (const [column, value] of Object.entries(match))
150
+ q = q.where(column, value);
151
+ return q;
152
+ }
104
153
  /**
105
154
  * Eager-load relations onto an array of models with one extra query each —
106
155
  * the fix for N+1. Each name must be a relationship method on the model.
@@ -161,10 +210,17 @@ export class Model {
161
210
  async save() {
162
211
  const ctor = this.ctor();
163
212
  const { table, primaryKey } = ctor;
164
- const data = ctor.toDatabase({ ...this });
165
- const idValue = data[primaryKey];
213
+ const idValue = this[primaryKey];
214
+ const forInsert = idValue == null;
215
+ const data = ctor.stampTimestamps(ctor.toDatabase({ ...this }), forInsert);
216
+ // Reflect the stamps back onto the instance.
217
+ if (ctor.timestamps) {
218
+ if (forInsert)
219
+ this[ctor.createdAtColumn] = data[ctor.createdAtColumn];
220
+ this[ctor.updatedAtColumn] = data[ctor.updatedAtColumn];
221
+ }
166
222
  delete data[primaryKey];
167
- if (idValue != null) {
223
+ if (!forInsert) {
168
224
  await db(table).where(primaryKey, idValue).update(data);
169
225
  }
170
226
  else {
@@ -174,6 +230,18 @@ export class Model {
174
230
  }
175
231
  return this;
176
232
  }
233
+ /** Mass-assign then save — `fill` + `save` in one call. */
234
+ async update(attributes) {
235
+ return this.fill(attributes).save();
236
+ }
237
+ /** Reload this model's columns from the database. */
238
+ async refresh() {
239
+ const ctor = this.ctor();
240
+ const row = await db(ctor.table).where(ctor.primaryKey, this[ctor.primaryKey]).first();
241
+ if (row)
242
+ Object.assign(this, applyCasts(row, ctor.casts, castGet));
243
+ return this;
244
+ }
177
245
  async delete() {
178
246
  const { table, primaryKey } = this.ctor();
179
247
  await db(table).where(primaryKey, this[primaryKey]).delete();
@@ -1,16 +1,25 @@
1
1
  /**
2
- * Service providers are the central place to configure the application.
2
+ * Service providers are the central place to configure the application — Keel's
3
+ * plugin system. A provider packages a slice of functionality and wires it in.
3
4
  *
4
5
  * register(): bind things into the container. Do NOT resolve other services
5
6
  * here — nothing is guaranteed to be registered yet.
6
7
  * boot(): called after every provider has registered. Safe to resolve
7
8
  * and wire things together here.
9
+ *
10
+ * Register with options to make a provider reusable:
11
+ *
12
+ * class RateLimitProvider extends ServiceProvider<{ max: number }> {
13
+ * boot() { this.app.make(HttpKernel).use(rateLimiter({ max: this.options.max })); }
14
+ * }
15
+ * app.register(RateLimitProvider, { max: 100 });
8
16
  */
9
17
  import type { Application } from "./application.js";
10
- export declare abstract class ServiceProvider {
18
+ export declare abstract class ServiceProvider<O = Record<string, unknown>> {
11
19
  protected app: Application;
12
- constructor(app: Application);
20
+ protected options: O;
21
+ constructor(app: Application, options?: O);
13
22
  register(): void | Promise<void>;
14
23
  boot(): void | Promise<void>;
15
24
  }
16
- export type ProviderClass = new (app: Application) => ServiceProvider;
25
+ export type ProviderClass = new (app: Application, options?: any) => ServiceProvider;
@@ -1,15 +1,25 @@
1
1
  /**
2
- * Service providers are the central place to configure the application.
2
+ * Service providers are the central place to configure the application — Keel's
3
+ * plugin system. A provider packages a slice of functionality and wires it in.
3
4
  *
4
5
  * register(): bind things into the container. Do NOT resolve other services
5
6
  * here — nothing is guaranteed to be registered yet.
6
7
  * boot(): called after every provider has registered. Safe to resolve
7
8
  * and wire things together here.
9
+ *
10
+ * Register with options to make a provider reusable:
11
+ *
12
+ * class RateLimitProvider extends ServiceProvider<{ max: number }> {
13
+ * boot() { this.app.make(HttpKernel).use(rateLimiter({ max: this.options.max })); }
14
+ * }
15
+ * app.register(RateLimitProvider, { max: 100 });
8
16
  */
9
17
  export class ServiceProvider {
10
18
  app;
11
- constructor(app) {
19
+ options;
20
+ constructor(app, options = {}) {
12
21
  this.app = app;
22
+ this.options = options;
13
23
  }
14
24
  register() { }
15
25
  boot() { }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Redis integration. Like the database and mail layers, it's built on a small
3
+ * pluggable driver (`RedisConnection`) rather than a hard dependency — so the
4
+ * core imports no client and runs on Node and the edge. Point it at Upstash
5
+ * (HTTP/`fetch`), ioredis, node-redis, or the built-in `MemoryRedis` for tests.
6
+ *
7
+ * setRedis(new MemoryRedis()); // or an Upstash/ioredis adapter
8
+ * await redis().set("views", "1");
9
+ * await redis().incr("views"); // 2
10
+ * await redis().remember("user:1", 60, () => fetchUser(1));
11
+ *
12
+ * `Redis` adds JSON helpers and a `remember` cache pattern over the raw command
13
+ * seam; `redisStore()` exposes it as a `CacheStore` so the cache can be
14
+ * Redis-backed.
15
+ */
16
+ import type { CacheStore } from "./cache.js";
17
+ export interface SetOptions {
18
+ /** Expire after N seconds. */
19
+ ex?: number;
20
+ /** Expire after N milliseconds. */
21
+ px?: number;
22
+ }
23
+ /** The bridge to your Redis client — implement it once per driver. */
24
+ export interface RedisConnection {
25
+ get(key: string): Promise<string | null>;
26
+ set(key: string, value: string, options?: SetOptions): Promise<void>;
27
+ del(...keys: string[]): Promise<number>;
28
+ exists(...keys: string[]): Promise<number>;
29
+ incrBy(key: string, amount: number): Promise<number>;
30
+ expire(key: string, seconds: number): Promise<boolean>;
31
+ ttl(key: string): Promise<number>;
32
+ keys(pattern: string): Promise<string[]>;
33
+ flushAll(): Promise<void>;
34
+ }
35
+ /** An in-memory `RedisConnection` with TTL support — the default; ideal for tests. */
36
+ export declare class MemoryRedis implements RedisConnection {
37
+ private store;
38
+ private live;
39
+ get(key: string): Promise<string | null>;
40
+ set(key: string, value: string, options?: SetOptions): Promise<void>;
41
+ del(...keys: string[]): Promise<number>;
42
+ exists(...keys: string[]): Promise<number>;
43
+ incrBy(key: string, amount: number): Promise<number>;
44
+ expire(key: string, seconds: number): Promise<boolean>;
45
+ ttl(key: string): Promise<number>;
46
+ keys(pattern: string): Promise<string[]>;
47
+ flushAll(): Promise<void>;
48
+ }
49
+ export declare class Redis {
50
+ private conn;
51
+ constructor(conn: RedisConnection);
52
+ get(key: string): Promise<string | null>;
53
+ set(key: string, value: string, options?: SetOptions): Promise<void>;
54
+ del(...keys: string[]): Promise<number>;
55
+ exists(...keys: string[]): Promise<number>;
56
+ has(key: string): Promise<boolean>;
57
+ incr(key: string): Promise<number>;
58
+ decr(key: string): Promise<number>;
59
+ incrBy(key: string, amount: number): Promise<number>;
60
+ expire(key: string, seconds: number): Promise<boolean>;
61
+ ttl(key: string): Promise<number>;
62
+ keys(pattern?: string): Promise<string[]>;
63
+ flushAll(): Promise<void>;
64
+ /** Get a JSON value (parsed), or null if the key is unset. */
65
+ getJson<T>(key: string): Promise<T | null>;
66
+ /** Set a value as JSON. */
67
+ setJson(key: string, value: unknown, options?: SetOptions): Promise<void>;
68
+ /** Return the cached JSON value, or compute it, store it for `seconds`, and return it. */
69
+ remember<T>(key: string, seconds: number, factory: () => T | Promise<T>): Promise<T>;
70
+ /** The underlying driver, for raw access. */
71
+ get connection(): RedisConnection;
72
+ }
73
+ /** Expose a `Redis` client as a `CacheStore`, so the cache can be Redis-backed. */
74
+ export declare function redisStore(client?: Redis): CacheStore;
75
+ /** Register the default Redis driver used by `redis()`. */
76
+ export declare function setRedis(conn: RedisConnection): Redis;
77
+ /** The default Redis client. */
78
+ export declare function redis(): Redis;