@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.
- package/README.md +14 -2
- package/dist/core/application.d.ts +58 -2
- package/dist/core/application.js +99 -3
- package/dist/core/auth.d.ts +21 -2
- package/dist/core/auth.js +38 -3
- package/dist/core/authorization.d.ts +52 -0
- package/dist/core/authorization.js +97 -0
- package/dist/core/broadcasting.d.ts +49 -0
- package/dist/core/broadcasting.js +84 -0
- package/dist/core/broker.d.ts +398 -0
- package/dist/core/broker.js +602 -0
- package/dist/core/crypto.d.ts +51 -1
- package/dist/core/crypto.js +96 -3
- package/dist/core/database.d.ts +26 -1
- package/dist/core/database.js +65 -2
- package/dist/core/decorators.d.ts +39 -0
- package/dist/core/decorators.js +72 -0
- package/dist/core/exceptions.d.ts +77 -6
- package/dist/core/exceptions.js +168 -10
- package/dist/core/helpers.d.ts +6 -0
- package/dist/core/helpers.js +12 -0
- package/dist/core/http/kernel.js +14 -2
- package/dist/core/http/router.d.ts +14 -0
- package/dist/core/http/router.js +30 -1
- package/dist/core/index.d.ts +31 -8
- package/dist/core/index.js +17 -5
- package/dist/core/logger.d.ts +5 -0
- package/dist/core/logger.js +24 -2
- package/dist/core/migrations.js +3 -3
- package/dist/core/model.d.ts +19 -1
- package/dist/core/model.js +72 -4
- package/dist/core/provider.d.ts +13 -4
- package/dist/core/provider.js +12 -2
- package/dist/core/redis.d.ts +78 -0
- package/dist/core/redis.js +176 -0
- package/dist/core/request-logger.d.ts +26 -0
- package/dist/core/request-logger.js +48 -0
- package/dist/core/request.d.ts +17 -1
- package/dist/core/request.js +27 -1
- package/dist/core/scheduler.d.ts +60 -0
- package/dist/core/scheduler.js +166 -0
- package/dist/core/session.js +17 -2
- package/dist/core/storage.d.ts +57 -0
- package/dist/core/storage.js +98 -0
- package/dist/core/template.d.ts +50 -0
- package/dist/core/template.js +753 -0
- package/dist/core/testing.d.ts +54 -0
- package/dist/core/testing.js +141 -0
- package/dist/core/transformer.d.ts +89 -0
- package/dist/core/transformer.js +152 -0
- package/dist/core/validation.d.ts +20 -0
- package/dist/core/validation.js +52 -1
- package/dist/core/vite.d.ts +117 -0
- package/dist/core/vite.js +258 -0
- package/dist/vite/index.d.ts +40 -0
- package/dist/vite/index.js +146 -0
- package/package.json +16 -1
package/dist/core/model.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/model.js
CHANGED
|
@@ -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
|
|
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
|
|
165
|
-
const
|
|
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 (
|
|
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();
|
package/dist/core/provider.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/core/provider.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/core/request.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/request.js
CHANGED
|
@@ -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
|
},
|