@shaferllc/keel 0.36.0 → 0.59.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 (57) 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/auth.d.ts +21 -2
  5. package/dist/core/auth.js +38 -3
  6. package/dist/core/authorization.d.ts +52 -0
  7. package/dist/core/authorization.js +97 -0
  8. package/dist/core/broadcasting.d.ts +49 -0
  9. package/dist/core/broadcasting.js +84 -0
  10. package/dist/core/broker.d.ts +398 -0
  11. package/dist/core/broker.js +602 -0
  12. package/dist/core/crypto.d.ts +51 -1
  13. package/dist/core/crypto.js +96 -3
  14. package/dist/core/database.d.ts +26 -1
  15. package/dist/core/database.js +65 -2
  16. package/dist/core/decorators.d.ts +39 -0
  17. package/dist/core/decorators.js +72 -0
  18. package/dist/core/exceptions.d.ts +77 -6
  19. package/dist/core/exceptions.js +168 -10
  20. package/dist/core/helpers.d.ts +6 -0
  21. package/dist/core/helpers.js +12 -0
  22. package/dist/core/http/kernel.js +14 -2
  23. package/dist/core/http/router.d.ts +14 -0
  24. package/dist/core/http/router.js +30 -1
  25. package/dist/core/index.d.ts +31 -8
  26. package/dist/core/index.js +17 -5
  27. package/dist/core/logger.d.ts +5 -0
  28. package/dist/core/logger.js +24 -2
  29. package/dist/core/migrations.js +3 -3
  30. package/dist/core/model.d.ts +19 -1
  31. package/dist/core/model.js +72 -4
  32. package/dist/core/provider.d.ts +13 -4
  33. package/dist/core/provider.js +12 -2
  34. package/dist/core/redis.d.ts +78 -0
  35. package/dist/core/redis.js +176 -0
  36. package/dist/core/request-logger.d.ts +26 -0
  37. package/dist/core/request-logger.js +48 -0
  38. package/dist/core/request.d.ts +17 -1
  39. package/dist/core/request.js +27 -1
  40. package/dist/core/scheduler.d.ts +60 -0
  41. package/dist/core/scheduler.js +166 -0
  42. package/dist/core/session.js +17 -2
  43. package/dist/core/storage.d.ts +57 -0
  44. package/dist/core/storage.js +98 -0
  45. package/dist/core/template.d.ts +50 -0
  46. package/dist/core/template.js +753 -0
  47. package/dist/core/testing.d.ts +54 -0
  48. package/dist/core/testing.js +141 -0
  49. package/dist/core/transformer.d.ts +89 -0
  50. package/dist/core/transformer.js +152 -0
  51. package/dist/core/validation.d.ts +20 -0
  52. package/dist/core/validation.js +52 -1
  53. package/dist/core/vite.d.ts +117 -0
  54. package/dist/core/vite.js +258 -0
  55. package/dist/vite/index.d.ts +40 -0
  56. package/dist/vite/index.js +146 -0
  57. package/package.json +16 -1
@@ -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;
@@ -0,0 +1,176 @@
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
+ /* ------------------------------ memory driver ----------------------------- */
17
+ /** An in-memory `RedisConnection` with TTL support — the default; ideal for tests. */
18
+ export class MemoryRedis {
19
+ store = new Map();
20
+ live(key) {
21
+ const entry = this.store.get(key);
22
+ if (!entry)
23
+ return undefined;
24
+ if (entry.expires && entry.expires <= Date.now()) {
25
+ this.store.delete(key);
26
+ return undefined;
27
+ }
28
+ return entry;
29
+ }
30
+ async get(key) {
31
+ return this.live(key)?.value ?? null;
32
+ }
33
+ async set(key, value, options) {
34
+ const ms = options?.px ?? (options?.ex != null ? options.ex * 1000 : 0);
35
+ this.store.set(key, { value, expires: ms ? Date.now() + ms : 0 });
36
+ }
37
+ async del(...keys) {
38
+ let n = 0;
39
+ for (const key of keys)
40
+ if (this.store.delete(key))
41
+ n++;
42
+ return n;
43
+ }
44
+ async exists(...keys) {
45
+ let n = 0;
46
+ for (const key of keys)
47
+ if (this.live(key))
48
+ n++;
49
+ return n;
50
+ }
51
+ async incrBy(key, amount) {
52
+ const current = Number(this.live(key)?.value ?? 0);
53
+ const next = current + amount;
54
+ const expires = this.store.get(key)?.expires ?? 0;
55
+ this.store.set(key, { value: String(next), expires });
56
+ return next;
57
+ }
58
+ async expire(key, seconds) {
59
+ const entry = this.live(key);
60
+ if (!entry)
61
+ return false;
62
+ entry.expires = Date.now() + seconds * 1000;
63
+ return true;
64
+ }
65
+ async ttl(key) {
66
+ const entry = this.live(key);
67
+ if (!entry)
68
+ return -2; // no such key
69
+ if (!entry.expires)
70
+ return -1; // no expiry
71
+ return Math.ceil((entry.expires - Date.now()) / 1000);
72
+ }
73
+ async keys(pattern) {
74
+ // Glob-ish: translate `*` and `?` to a regex.
75
+ const rx = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
76
+ const out = [];
77
+ for (const key of this.store.keys())
78
+ if (this.live(key) && rx.test(key))
79
+ out.push(key);
80
+ return out;
81
+ }
82
+ async flushAll() {
83
+ this.store.clear();
84
+ }
85
+ }
86
+ /* -------------------------------- the client ------------------------------ */
87
+ export class Redis {
88
+ conn;
89
+ constructor(conn) {
90
+ this.conn = conn;
91
+ }
92
+ get(key) {
93
+ return this.conn.get(key);
94
+ }
95
+ set(key, value, options) {
96
+ return this.conn.set(key, value, options);
97
+ }
98
+ del(...keys) {
99
+ return this.conn.del(...keys);
100
+ }
101
+ exists(...keys) {
102
+ return this.conn.exists(...keys);
103
+ }
104
+ async has(key) {
105
+ return (await this.conn.exists(key)) > 0;
106
+ }
107
+ incr(key) {
108
+ return this.conn.incrBy(key, 1);
109
+ }
110
+ decr(key) {
111
+ return this.conn.incrBy(key, -1);
112
+ }
113
+ incrBy(key, amount) {
114
+ return this.conn.incrBy(key, amount);
115
+ }
116
+ expire(key, seconds) {
117
+ return this.conn.expire(key, seconds);
118
+ }
119
+ ttl(key) {
120
+ return this.conn.ttl(key);
121
+ }
122
+ keys(pattern = "*") {
123
+ return this.conn.keys(pattern);
124
+ }
125
+ flushAll() {
126
+ return this.conn.flushAll();
127
+ }
128
+ /** Get a JSON value (parsed), or null if the key is unset. */
129
+ async getJson(key) {
130
+ const raw = await this.conn.get(key);
131
+ return raw == null ? null : JSON.parse(raw);
132
+ }
133
+ /** Set a value as JSON. */
134
+ setJson(key, value, options) {
135
+ return this.conn.set(key, JSON.stringify(value), options);
136
+ }
137
+ /** Return the cached JSON value, or compute it, store it for `seconds`, and return it. */
138
+ async remember(key, seconds, factory) {
139
+ const hit = await this.getJson(key);
140
+ if (hit !== null)
141
+ return hit;
142
+ const fresh = await factory();
143
+ await this.setJson(key, fresh, { ex: seconds });
144
+ return fresh;
145
+ }
146
+ /** The underlying driver, for raw access. */
147
+ get connection() {
148
+ return this.conn;
149
+ }
150
+ }
151
+ /* ----------------------------- cache adapter ------------------------------ */
152
+ /** Expose a `Redis` client as a `CacheStore`, so the cache can be Redis-backed. */
153
+ export function redisStore(client = redis()) {
154
+ return {
155
+ // The cache treats `undefined` as "miss"; Redis has no such value, so map it.
156
+ get: async (key) => (await client.getJson(key)) ?? undefined,
157
+ set: async (key, value, ttlMs) => {
158
+ await client.setJson(key, value, ttlMs ? { px: ttlMs } : undefined);
159
+ },
160
+ delete: async (key) => {
161
+ await client.del(key);
162
+ },
163
+ clear: () => client.flushAll(),
164
+ };
165
+ }
166
+ /* -------------------------------- global ---------------------------------- */
167
+ let client = new Redis(new MemoryRedis());
168
+ /** Register the default Redis driver used by `redis()`. */
169
+ export function setRedis(conn) {
170
+ client = new Redis(conn);
171
+ return client;
172
+ }
173
+ /** The default Redis client. */
174
+ export function redis() {
175
+ return client;
176
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Per-request logging. `requestLogger()` middleware binds a child logger to each
3
+ * request with a generated `reqId`, so every log line within that request
4
+ * correlates — Fastify's `request.log`, built on Keel's `Logger.child()`. It can
5
+ * also log the request start and completion (method, path, status, duration).
6
+ *
7
+ * kernel.use(requestLogger()); // in app/Http/Kernel.ts
8
+ * requestLog().info("charging card"); // anywhere in the request → carries reqId
9
+ */
10
+ import type { Context, MiddlewareHandler } from "hono";
11
+ import type { Logger } from "./logger.js";
12
+ export interface RequestLoggerOptions {
13
+ /** Generate the request id. Default: `crypto.randomUUID()`. */
14
+ genReqId?: (c: Context) => string;
15
+ /** Reuse an incoming id from this header if present (e.g. `"x-request-id"`). */
16
+ idHeader?: string;
17
+ /** Log request start and completion lines. Default: true. */
18
+ logRequests?: boolean;
19
+ }
20
+ /** Middleware: attach a per-request child logger (with `reqId`) and log the request. */
21
+ export declare function requestLogger(options?: RequestLoggerOptions): MiddlewareHandler;
22
+ /**
23
+ * The current request's child logger (carrying its `reqId`), or the base logger
24
+ * when called outside a request or without `requestLogger()` installed.
25
+ */
26
+ export declare function requestLog(): Logger;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Per-request logging. `requestLogger()` middleware binds a child logger to each
3
+ * request with a generated `reqId`, so every log line within that request
4
+ * correlates — Fastify's `request.log`, built on Keel's `Logger.child()`. It can
5
+ * also log the request start and completion (method, path, status, duration).
6
+ *
7
+ * kernel.use(requestLogger()); // in app/Http/Kernel.ts
8
+ * requestLog().info("charging card"); // anywhere in the request → carries reqId
9
+ */
10
+ import { ctx } from "./request.js";
11
+ import { logger } from "./helpers.js";
12
+ // The child logger for the current request, keyed by the context object.
13
+ const store = new WeakMap();
14
+ /** Middleware: attach a per-request child logger (with `reqId`) and log the request. */
15
+ export function requestLogger(options = {}) {
16
+ const { genReqId, idHeader, logRequests = true } = options;
17
+ return async (c, next) => {
18
+ const incoming = idHeader ? c.req.header(idHeader) : undefined;
19
+ const reqId = incoming ?? (genReqId ? genReqId(c) : crypto.randomUUID());
20
+ const log = logger().child({ reqId });
21
+ store.set(c, log);
22
+ const start = performance.now();
23
+ if (logRequests)
24
+ log.info("request", { method: c.req.method, path: c.req.path });
25
+ await next();
26
+ if (logRequests) {
27
+ log.info("request completed", {
28
+ status: c.res.status,
29
+ ms: Number((performance.now() - start).toFixed(1)),
30
+ });
31
+ }
32
+ };
33
+ }
34
+ /**
35
+ * The current request's child logger (carrying its `reqId`), or the base logger
36
+ * when called outside a request or without `requestLogger()` installed.
37
+ */
38
+ export function requestLog() {
39
+ try {
40
+ const log = store.get(ctx());
41
+ if (log)
42
+ return log;
43
+ }
44
+ catch {
45
+ // not in a request context
46
+ }
47
+ return logger();
48
+ }
@@ -39,6 +39,12 @@ interface ResponseHelper {
39
39
  status(code: number): ResponseHelper;
40
40
  /** Set a response header (chainable). */
41
41
  header(name: string, value: string): ResponseHelper;
42
+ /** Set several response headers at once (chainable). */
43
+ headers(map: Record<string, string>): ResponseHelper;
44
+ /** Read a response header set so far (e.g. in middleware after `next()`). */
45
+ getHeader(name: string): string | null;
46
+ /** Whether a response header has been set. */
47
+ hasHeader(name: string): boolean;
42
48
  /** Set the Content-Type (chainable). */
43
49
  type(mime: string): ResponseHelper;
44
50
  /** Append a value to a (possibly multi-value) header (chainable). */
@@ -74,13 +80,23 @@ export declare const request: {
74
80
  param(name?: string): string | Record<string, string>;
75
81
  query(name?: string): string | undefined | Record<string, string>;
76
82
  json<T = unknown>(): Promise<T>;
83
+ /**
84
+ * The raw request body as text — for content types `json()`/`all()` don't
85
+ * handle (XML, CSV, a custom format). Parse it yourself from here.
86
+ */
87
+ text(): Promise<string>;
88
+ /** The raw request body as an ArrayBuffer (binary payloads). */
89
+ arrayBuffer(): Promise<ArrayBuffer>;
90
+ /** The raw request body as a Blob. */
91
+ blob(): Promise<Blob>;
77
92
  /** The raw web Request. */
78
93
  readonly raw: Request;
79
- /** The matched route: `{ name, pattern, methods }`. */
94
+ /** The matched route: `{ name, pattern, methods, config }`. */
80
95
  readonly route: {
81
96
  name?: string;
82
97
  pattern: string;
83
98
  methods: import("./index.js").Method[];
99
+ config: Record<string, unknown>;
84
100
  } | undefined;
85
101
  /** Whether the matched route has the given name. */
86
102
  routeIs(name: string): boolean;
@@ -128,6 +128,17 @@ export const response = {
128
128
  ctx().header(name, value);
129
129
  return response;
130
130
  },
131
+ headers(map) {
132
+ for (const [name, value] of Object.entries(map))
133
+ ctx().header(name, value);
134
+ return response;
135
+ },
136
+ getHeader(name) {
137
+ return ctx().res.headers.get(name);
138
+ },
139
+ hasHeader(name) {
140
+ return ctx().res.headers.has(name);
141
+ },
131
142
  type(mime) {
132
143
  ctx().header("content-type", mime);
133
144
  return response;
@@ -196,11 +207,26 @@ export const request = {
196
207
  json() {
197
208
  return ctx().req.json();
198
209
  },
210
+ /**
211
+ * The raw request body as text — for content types `json()`/`all()` don't
212
+ * handle (XML, CSV, a custom format). Parse it yourself from here.
213
+ */
214
+ text() {
215
+ return ctx().req.text();
216
+ },
217
+ /** The raw request body as an ArrayBuffer (binary payloads). */
218
+ arrayBuffer() {
219
+ return ctx().req.arrayBuffer();
220
+ },
221
+ /** The raw request body as a Blob. */
222
+ blob() {
223
+ return ctx().req.blob();
224
+ },
199
225
  /** The raw web Request. */
200
226
  get raw() {
201
227
  return ctx().req.raw;
202
228
  },
203
- /** The matched route: `{ name, pattern, methods }`. */
229
+ /** The matched route: `{ name, pattern, methods, config }`. */
204
230
  get route() {
205
231
  return ctx().get("route");
206
232
  },