@shaferllc/keel 0.12.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +28 -3
  2. package/dist/core/application.js +9 -0
  3. package/dist/core/auth.d.ts +41 -0
  4. package/dist/core/auth.js +70 -0
  5. package/dist/core/cache.d.ts +41 -0
  6. package/dist/core/cache.js +81 -0
  7. package/dist/core/casts.d.ts +19 -0
  8. package/dist/core/casts.js +69 -0
  9. package/dist/core/crypto.d.ts +24 -0
  10. package/dist/core/crypto.js +97 -0
  11. package/dist/core/database.d.ts +58 -0
  12. package/dist/core/database.js +140 -0
  13. package/dist/core/debug.d.ts +12 -0
  14. package/dist/core/debug.js +55 -0
  15. package/dist/core/events.d.ts +21 -0
  16. package/dist/core/events.js +46 -0
  17. package/dist/core/exceptions.d.ts +10 -1
  18. package/dist/core/exceptions.js +10 -1
  19. package/dist/core/factory.d.ts +84 -0
  20. package/dist/core/factory.js +154 -0
  21. package/dist/core/helpers.d.ts +13 -0
  22. package/dist/core/helpers.js +24 -0
  23. package/dist/core/http/kernel.js +21 -2
  24. package/dist/core/http/router.d.ts +32 -8
  25. package/dist/core/http/router.js +74 -5
  26. package/dist/core/index.d.ts +31 -2
  27. package/dist/core/index.js +17 -1
  28. package/dist/core/logger.d.ts +29 -0
  29. package/dist/core/logger.js +49 -0
  30. package/dist/core/mail.d.ts +94 -0
  31. package/dist/core/mail.js +157 -0
  32. package/dist/core/migrations.d.ts +76 -0
  33. package/dist/core/migrations.js +211 -0
  34. package/dist/core/model.d.ts +75 -0
  35. package/dist/core/model.js +202 -0
  36. package/dist/core/queue.d.ts +73 -0
  37. package/dist/core/queue.js +88 -0
  38. package/dist/core/rate-limit.d.ts +23 -0
  39. package/dist/core/rate-limit.js +50 -0
  40. package/dist/core/relations.d.ts +90 -0
  41. package/dist/core/relations.js +229 -0
  42. package/dist/core/request.d.ts +53 -1
  43. package/dist/core/request.js +187 -0
  44. package/dist/core/session.d.ts +46 -0
  45. package/dist/core/session.js +112 -0
  46. package/dist/core/static.d.ts +27 -0
  47. package/dist/core/static.js +61 -0
  48. package/package.json +1 -1
@@ -0,0 +1,140 @@
1
+ /**
2
+ * A small, driver-agnostic query builder. It generates parameterized SQL and
3
+ * runs it through a `Connection` you provide — so it works with any driver:
4
+ * Cloudflare D1, Neon/Postgres, PlanetScale, Turso/libSQL, better-sqlite3, pg.
5
+ * The core stays edge-safe because Keel never imports a database driver.
6
+ *
7
+ * setConnection(myConnection, "postgres");
8
+ * const active = await db("users").where("active", true).orderBy("name").get();
9
+ * const user = await db("users").where("id", 1).first();
10
+ * await db("users").insert({ email });
11
+ */
12
+ let connection;
13
+ let dialect = "sqlite";
14
+ /** Register the database connection (and dialect) used by `db()`. */
15
+ export function setConnection(conn, driverDialect = "sqlite") {
16
+ connection = conn;
17
+ dialect = driverDialect;
18
+ }
19
+ function conn() {
20
+ if (!connection)
21
+ throw new Error("No database connection. Call setConnection(conn, dialect).");
22
+ return connection;
23
+ }
24
+ /** Render `?` placeholders for the active dialect (Postgres uses $1, $2, …). */
25
+ function placeholders(sql) {
26
+ if (dialect !== "postgres")
27
+ return sql;
28
+ let i = 0;
29
+ return sql.replace(/\?/g, () => `$${++i}`);
30
+ }
31
+ export class QueryBuilder {
32
+ table;
33
+ wheres = [];
34
+ orders = [];
35
+ columns = "*";
36
+ _limit;
37
+ _offset;
38
+ constructor(table) {
39
+ this.table = table;
40
+ }
41
+ select(...columns) {
42
+ this.columns = columns.length ? columns.join(", ") : "*";
43
+ return this;
44
+ }
45
+ where(column, opOrValue, value) {
46
+ const [op, val] = value === undefined ? ["=", opOrValue] : [opOrValue, value];
47
+ this.wheres.push({ boolean: "AND", sql: `${column} ${op} ?`, bindings: [val] });
48
+ return this;
49
+ }
50
+ orWhere(column, opOrValue, value) {
51
+ const [op, val] = value === undefined ? ["=", opOrValue] : [opOrValue, value];
52
+ this.wheres.push({ boolean: "OR", sql: `${column} ${op} ?`, bindings: [val] });
53
+ return this;
54
+ }
55
+ whereIn(column, values) {
56
+ const marks = values.map(() => "?").join(", ");
57
+ this.wheres.push({ boolean: "AND", sql: `${column} IN (${marks})`, bindings: values });
58
+ return this;
59
+ }
60
+ whereNull(column) {
61
+ this.wheres.push({ boolean: "AND", sql: `${column} IS NULL`, bindings: [] });
62
+ return this;
63
+ }
64
+ whereNotNull(column) {
65
+ this.wheres.push({ boolean: "AND", sql: `${column} IS NOT NULL`, bindings: [] });
66
+ return this;
67
+ }
68
+ orderBy(column, direction = "asc") {
69
+ this.orders.push(`${column} ${direction.toUpperCase()}`);
70
+ return this;
71
+ }
72
+ limit(n) {
73
+ this._limit = n;
74
+ return this;
75
+ }
76
+ offset(n) {
77
+ this._offset = n;
78
+ return this;
79
+ }
80
+ whereClause() {
81
+ if (!this.wheres.length)
82
+ return { sql: "", bindings: [] };
83
+ const sql = " WHERE " +
84
+ this.wheres.map((w, i) => (i === 0 ? "" : `${w.boolean} `) + w.sql).join(" ");
85
+ return { sql, bindings: this.wheres.flatMap((w) => w.bindings) };
86
+ }
87
+ /* ------------------------------- reads ------------------------------- */
88
+ async get() {
89
+ const where = this.whereClause();
90
+ let sql = `SELECT ${this.columns} FROM ${this.table}${where.sql}`;
91
+ if (this.orders.length)
92
+ sql += ` ORDER BY ${this.orders.join(", ")}`;
93
+ if (this._limit != null)
94
+ sql += ` LIMIT ${this._limit}`;
95
+ if (this._offset != null)
96
+ sql += ` OFFSET ${this._offset}`;
97
+ return conn().select(placeholders(sql), where.bindings);
98
+ }
99
+ async first() {
100
+ this._limit = 1;
101
+ const rows = await this.get();
102
+ return rows[0] ?? null;
103
+ }
104
+ async count() {
105
+ const where = this.whereClause();
106
+ const rows = await conn().select(placeholders(`SELECT COUNT(*) AS count FROM ${this.table}${where.sql}`), where.bindings);
107
+ return Number(rows[0]?.count ?? 0);
108
+ }
109
+ async exists() {
110
+ return (await this.count()) > 0;
111
+ }
112
+ /* ------------------------------- writes ------------------------------ */
113
+ async insert(data) {
114
+ const keys = Object.keys(data);
115
+ const sql = `INSERT INTO ${this.table} (${keys.join(", ")}) VALUES (${keys
116
+ .map(() => "?")
117
+ .join(", ")})`;
118
+ return conn().write(placeholders(sql), Object.values(data));
119
+ }
120
+ async insertGetId(data) {
121
+ return (await this.insert(data)).insertId;
122
+ }
123
+ async update(data) {
124
+ const keys = Object.keys(data);
125
+ const set = keys.map((k) => `${k} = ?`).join(", ");
126
+ const where = this.whereClause();
127
+ return conn().write(placeholders(`UPDATE ${this.table} SET ${set}${where.sql}`), [
128
+ ...Object.values(data),
129
+ ...where.bindings,
130
+ ]);
131
+ }
132
+ async delete() {
133
+ const where = this.whereClause();
134
+ return conn().write(placeholders(`DELETE FROM ${this.table}${where.sql}`), where.bindings);
135
+ }
136
+ }
137
+ /** Start a query against a table. */
138
+ export function db(table) {
139
+ return new QueryBuilder(table);
140
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Debugging helpers. `dump()` prints values to the console; `dd()` dumps and
3
+ * dies — it halts the request and renders the values in the browser. Both are
4
+ * edge-safe (plain console + a self-rendering exception).
5
+ *
6
+ * dump(user, order); // logs, keeps going
7
+ * dd(request.headers()); // renders and stops the request
8
+ */
9
+ /** Print values to the console. Returns the first value, so it can be inlined. */
10
+ export declare function dump<T>(...values: [T, ...unknown[]]): T;
11
+ /** Dump values to the browser and halt the request. */
12
+ export declare function dd(...values: unknown[]): never;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Debugging helpers. `dump()` prints values to the console; `dd()` dumps and
3
+ * dies — it halts the request and renders the values in the browser. Both are
4
+ * edge-safe (plain console + a self-rendering exception).
5
+ *
6
+ * dump(user, order); // logs, keeps going
7
+ * dd(request.headers()); // renders and stops the request
8
+ */
9
+ import { HttpException } from "./exceptions.js";
10
+ /** Safe, pretty JSON — handles circular refs, functions, and bigints. */
11
+ function stringify(value) {
12
+ const seen = new WeakSet();
13
+ return JSON.stringify(value, (_key, val) => {
14
+ if (typeof val === "object" && val !== null) {
15
+ if (seen.has(val))
16
+ return "[Circular]";
17
+ seen.add(val);
18
+ }
19
+ if (typeof val === "function")
20
+ return `[Function: ${val.name || "anonymous"}]`;
21
+ if (typeof val === "bigint")
22
+ return `${val}n`;
23
+ if (val === undefined)
24
+ return "[undefined]";
25
+ return val;
26
+ }, 2);
27
+ }
28
+ /** Print values to the console. Returns the first value, so it can be inlined. */
29
+ export function dump(...values) {
30
+ console.log("⚓ dump →", ...values);
31
+ return values[0];
32
+ }
33
+ class DumpException extends HttpException {
34
+ values;
35
+ constructor(values) {
36
+ super(200);
37
+ this.values = values;
38
+ this.name = "DumpException";
39
+ }
40
+ handle(c) {
41
+ const esc = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
42
+ const blocks = this.values
43
+ .map((v) => `<pre>${esc(stringify(v))}</pre>`)
44
+ .join("");
45
+ return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>dump</title><style>
46
+ body{margin:0;background:#0b1120;color:#e2e8f0;font-family:ui-monospace,Menlo,monospace;padding:2rem}
47
+ h1{font-size:.8rem;letter-spacing:.15em;text-transform:uppercase;color:#f87171;margin:0 0 1rem}
48
+ pre{background:#020617;border:1px solid #1e293b;border-radius:.5rem;padding:1.2rem;overflow-x:auto;font-size:.85rem;line-height:1.6;margin:0 0 1rem}
49
+ </style></head><body><h1>⚓ dump &amp; die</h1>${blocks}</body></html>`, 200);
50
+ }
51
+ }
52
+ /** Dump values to the browser and halt the request. */
53
+ export function dd(...values) {
54
+ throw new DumpException(values);
55
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * A tiny event emitter for decoupling. Listeners may be async; `emit` awaits
3
+ * them in registration order. Bound as a singleton on the application, so the
4
+ * global `emit()` / `listen()` helpers reach it from anywhere.
5
+ *
6
+ * listen("user.registered", (user) => sendWelcome(user));
7
+ * await emit("user.registered", user);
8
+ */
9
+ export type Listener<T = unknown> = (payload: T) => void | Promise<void>;
10
+ export declare class Events {
11
+ private listeners;
12
+ /** Subscribe to an event. Returns an unsubscribe function. */
13
+ on<T = unknown>(event: string, listener: Listener<T>): () => void;
14
+ /** Subscribe for a single emission. */
15
+ once<T = unknown>(event: string, listener: Listener<T>): () => void;
16
+ off<T = unknown>(event: string, listener: Listener<T>): void;
17
+ /** Emit an event, awaiting every listener in order. */
18
+ emit<T = unknown>(event: string, payload?: T): Promise<void>;
19
+ listenerCount(event: string): number;
20
+ clear(event?: string): void;
21
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * A tiny event emitter for decoupling. Listeners may be async; `emit` awaits
3
+ * them in registration order. Bound as a singleton on the application, so the
4
+ * global `emit()` / `listen()` helpers reach it from anywhere.
5
+ *
6
+ * listen("user.registered", (user) => sendWelcome(user));
7
+ * await emit("user.registered", user);
8
+ */
9
+ export class Events {
10
+ listeners = new Map();
11
+ /** Subscribe to an event. Returns an unsubscribe function. */
12
+ on(event, listener) {
13
+ const set = this.listeners.get(event) ?? new Set();
14
+ set.add(listener);
15
+ this.listeners.set(event, set);
16
+ return () => this.off(event, listener);
17
+ }
18
+ /** Subscribe for a single emission. */
19
+ once(event, listener) {
20
+ const wrapper = async (payload) => {
21
+ this.off(event, wrapper);
22
+ await listener(payload);
23
+ };
24
+ return this.on(event, wrapper);
25
+ }
26
+ off(event, listener) {
27
+ this.listeners.get(event)?.delete(listener);
28
+ }
29
+ /** Emit an event, awaiting every listener in order. */
30
+ async emit(event, payload) {
31
+ const set = this.listeners.get(event);
32
+ if (!set)
33
+ return;
34
+ for (const listener of [...set])
35
+ await listener(payload);
36
+ }
37
+ listenerCount(event) {
38
+ return this.listeners.get(event)?.size ?? 0;
39
+ }
40
+ clear(event) {
41
+ if (event)
42
+ this.listeners.delete(event);
43
+ else
44
+ this.listeners.clear();
45
+ }
46
+ }
@@ -4,10 +4,19 @@
4
4
  * with a clean body (JSON or HTML), or a readable error page in debug mode.
5
5
  */
6
6
  export declare const STATUS_TEXT: Record<number, string>;
7
- /** A semantic HTTP error carrying a status code and optional headers. */
7
+ /**
8
+ * A semantic HTTP error carrying a status code and optional headers.
9
+ *
10
+ * Subclasses may add:
11
+ * - a `code` (e.g. "E_UNAUTHORIZED") — included in the JSON error body;
12
+ * - a `handle(c)` method — renders the exception itself (self-handling);
13
+ * - a `report()` method — called for logging/reporting before rendering.
14
+ */
8
15
  export declare class HttpException extends Error {
9
16
  readonly status: number;
10
17
  readonly headers?: Record<string, string>;
18
+ /** A machine-readable error code, e.g. "E_VALIDATION". */
19
+ code?: string;
11
20
  constructor(status: number, message?: string, headers?: Record<string, string>);
12
21
  }
13
22
  export declare class NotFoundException extends HttpException {
@@ -16,10 +16,19 @@ export const STATUS_TEXT = {
16
16
  500: "Internal Server Error",
17
17
  503: "Service Unavailable",
18
18
  };
19
- /** A semantic HTTP error carrying a status code and optional headers. */
19
+ /**
20
+ * A semantic HTTP error carrying a status code and optional headers.
21
+ *
22
+ * Subclasses may add:
23
+ * - a `code` (e.g. "E_UNAUTHORIZED") — included in the JSON error body;
24
+ * - a `handle(c)` method — renders the exception itself (self-handling);
25
+ * - a `report()` method — called for logging/reporting before rendering.
26
+ */
20
27
  export class HttpException extends Error {
21
28
  status;
22
29
  headers;
30
+ /** A machine-readable error code, e.g. "E_VALIDATION". */
31
+ code;
23
32
  constructor(status, message, headers) {
24
33
  super(message ?? STATUS_TEXT[status] ?? "Error");
25
34
  this.name = "HttpException";
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Factories and seeders for populating the database in tests and demos. A
3
+ * factory describes how to build a model's attributes; a seeder orchestrates
4
+ * factories (and raw writes) into a repeatable dataset.
5
+ *
6
+ * const users = factory(User, (f) => ({
7
+ * name: f.name(),
8
+ * email: f.email(),
9
+ * }));
10
+ *
11
+ * await users.create(); // one persisted User
12
+ * await users.count(10).create(); // ten of them
13
+ * await users.make({ name: "Ada" }); // built, not persisted; with override
14
+ *
15
+ * class DatabaseSeeder extends Seeder {
16
+ * async run() {
17
+ * await factory(User, ...).count(10).create();
18
+ * }
19
+ * }
20
+ * await seed(DatabaseSeeder);
21
+ *
22
+ * `Faker` is a tiny, dependency-free generator — enough for realistic-looking
23
+ * fixtures without pulling in a faker library. It's seedable so runs can be
24
+ * made deterministic.
25
+ */
26
+ import type { Model } from "./model.js";
27
+ import type { Row } from "./database.js";
28
+ type ModelClass<T extends Model> = (new (attributes?: Row) => T) & {
29
+ create(attributes: Row): Promise<T>;
30
+ };
31
+ /** A small, seedable pseudo-random data source — no external dependency. */
32
+ export declare class Faker {
33
+ private state;
34
+ constructor(seed?: number);
35
+ /** xorshift32 — deterministic given a seed, fast, and edge-safe. */
36
+ private next;
37
+ /** An integer in [min, max]. */
38
+ number(min?: number, max?: number): number;
39
+ boolean(): boolean;
40
+ /** A random element of `items`. */
41
+ pick<T>(items: readonly T[]): T;
42
+ firstName(): string;
43
+ lastName(): string;
44
+ name(): string;
45
+ word(): string;
46
+ words(count?: number): string;
47
+ sentence(count?: number): string;
48
+ paragraph(sentences?: number): string;
49
+ /** A likely-unique, lowercased email. */
50
+ email(): string;
51
+ slug(): string;
52
+ /** An RFC-4122-shaped v4 UUID (from this generator, not crypto). */
53
+ uuid(): string;
54
+ }
55
+ /** Called per instance; `index` lets definitions vary across a batch. */
56
+ export type Definition<T extends Model> = (faker: Faker, index: number) => Row;
57
+ export declare class Factory<T extends Model> {
58
+ private model;
59
+ private definition;
60
+ private faker;
61
+ private _count;
62
+ constructor(model: ModelClass<T>, definition: Definition<T>, faker?: Faker);
63
+ /** How many models the next `make`/`create` produces. */
64
+ count(n: number): this;
65
+ /** Use a specific Faker (e.g. a seeded one) for reproducible data. */
66
+ usingFaker(faker: Faker): this;
67
+ private attributesFor;
68
+ /** Build model instance(s) without persisting them. */
69
+ make(overrides?: Row): T;
70
+ make(overrides: Row, _internal: "many"): T[];
71
+ /** Persist model instance(s) via `Model.create`. */
72
+ create(overrides?: Row): Promise<T | T[]>;
73
+ }
74
+ /** Start a factory for a model with an attribute definition. */
75
+ export declare function factory<T extends Model>(model: ModelClass<T>, definition: Definition<T>): Factory<T>;
76
+ export declare abstract class Seeder {
77
+ /** Populate the database. Override this. */
78
+ abstract run(): Promise<void>;
79
+ /** Run other seeders in sequence — compose a `DatabaseSeeder` from parts. */
80
+ protected call(seeders: Array<new () => Seeder>): Promise<void>;
81
+ }
82
+ /** Instantiate and run a seeder class. */
83
+ export declare function seed(SeederClass: new () => Seeder): Promise<void>;
84
+ export {};
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Factories and seeders for populating the database in tests and demos. A
3
+ * factory describes how to build a model's attributes; a seeder orchestrates
4
+ * factories (and raw writes) into a repeatable dataset.
5
+ *
6
+ * const users = factory(User, (f) => ({
7
+ * name: f.name(),
8
+ * email: f.email(),
9
+ * }));
10
+ *
11
+ * await users.create(); // one persisted User
12
+ * await users.count(10).create(); // ten of them
13
+ * await users.make({ name: "Ada" }); // built, not persisted; with override
14
+ *
15
+ * class DatabaseSeeder extends Seeder {
16
+ * async run() {
17
+ * await factory(User, ...).count(10).create();
18
+ * }
19
+ * }
20
+ * await seed(DatabaseSeeder);
21
+ *
22
+ * `Faker` is a tiny, dependency-free generator — enough for realistic-looking
23
+ * fixtures without pulling in a faker library. It's seedable so runs can be
24
+ * made deterministic.
25
+ */
26
+ /* --------------------------------- faker ---------------------------------- */
27
+ const FIRST_NAMES = [
28
+ "Ada", "Grace", "Alan", "Linus", "Katherine", "Dennis", "Barbara", "Edsger",
29
+ "Margaret", "Ken", "Radia", "Guido", "Anita", "Tim", "Joan", "Donald",
30
+ ];
31
+ const LAST_NAMES = [
32
+ "Lovelace", "Hopper", "Turing", "Torvalds", "Johnson", "Ritchie", "Liskov",
33
+ "Dijkstra", "Hamilton", "Thompson", "Perlman", "Rossum", "Borg", "Berners-Lee",
34
+ ];
35
+ const WORDS = [
36
+ "keel", "hull", "helm", "anchor", "harbor", "tide", "sail", "bow", "stern",
37
+ "deck", "mast", "rudder", "current", "voyage", "compass", "beacon", "port",
38
+ ];
39
+ const DOMAINS = ["example.com", "test.dev", "mail.io", "keel.app"];
40
+ /** A small, seedable pseudo-random data source — no external dependency. */
41
+ export class Faker {
42
+ state;
43
+ constructor(seed = 0x2545f491) {
44
+ this.state = seed >>> 0 || 0x2545f491;
45
+ }
46
+ /** xorshift32 — deterministic given a seed, fast, and edge-safe. */
47
+ next() {
48
+ let x = this.state;
49
+ x ^= x << 13;
50
+ x ^= x >>> 17;
51
+ x ^= x << 5;
52
+ this.state = x >>> 0;
53
+ return this.state / 0xffffffff;
54
+ }
55
+ /** An integer in [min, max]. */
56
+ number(min = 0, max = 1000) {
57
+ return min + Math.floor(this.next() * (max - min + 1));
58
+ }
59
+ boolean() {
60
+ return this.next() < 0.5;
61
+ }
62
+ /** A random element of `items`. */
63
+ pick(items) {
64
+ return items[this.number(0, items.length - 1)];
65
+ }
66
+ firstName() {
67
+ return this.pick(FIRST_NAMES);
68
+ }
69
+ lastName() {
70
+ return this.pick(LAST_NAMES);
71
+ }
72
+ name() {
73
+ return `${this.firstName()} ${this.lastName()}`;
74
+ }
75
+ word() {
76
+ return this.pick(WORDS);
77
+ }
78
+ words(count = 3) {
79
+ return Array.from({ length: count }, () => this.word()).join(" ");
80
+ }
81
+ sentence(count = 6) {
82
+ const s = this.words(count);
83
+ return `${s.charAt(0).toUpperCase()}${s.slice(1)}.`;
84
+ }
85
+ paragraph(sentences = 3) {
86
+ return Array.from({ length: sentences }, () => this.sentence()).join(" ");
87
+ }
88
+ /** A likely-unique, lowercased email. */
89
+ email() {
90
+ const handle = `${this.firstName()}.${this.lastName()}.${this.number(1, 9999)}`;
91
+ return `${handle.toLowerCase().replace(/[^a-z0-9.]/g, "")}@${this.pick(DOMAINS)}`;
92
+ }
93
+ slug() {
94
+ return `${this.word()}-${this.word()}-${this.number(1, 9999)}`;
95
+ }
96
+ /** An RFC-4122-shaped v4 UUID (from this generator, not crypto). */
97
+ uuid() {
98
+ const hex = () => this.number(0, 15).toString(16);
99
+ const block = (n) => Array.from({ length: n }, hex).join("");
100
+ return `${block(8)}-${block(4)}-4${block(3)}-${this.pick(["8", "9", "a", "b"])}${block(3)}-${block(12)}`;
101
+ }
102
+ }
103
+ export class Factory {
104
+ model;
105
+ definition;
106
+ faker;
107
+ _count = 1;
108
+ constructor(model, definition, faker = new Faker()) {
109
+ this.model = model;
110
+ this.definition = definition;
111
+ this.faker = faker;
112
+ }
113
+ /** How many models the next `make`/`create` produces. */
114
+ count(n) {
115
+ this._count = n;
116
+ return this;
117
+ }
118
+ /** Use a specific Faker (e.g. a seeded one) for reproducible data. */
119
+ usingFaker(faker) {
120
+ this.faker = faker;
121
+ return this;
122
+ }
123
+ attributesFor(index, overrides) {
124
+ return { ...this.definition(this.faker, index), ...overrides };
125
+ }
126
+ make(overrides = {}) {
127
+ const built = Array.from({ length: this._count }, (_, i) => new this.model(this.attributesFor(i, overrides)));
128
+ return this._count === 1 ? built[0] : built;
129
+ }
130
+ /** Persist model instance(s) via `Model.create`. */
131
+ async create(overrides = {}) {
132
+ const created = [];
133
+ for (let i = 0; i < this._count; i++) {
134
+ created.push(await this.model.create(this.attributesFor(i, overrides)));
135
+ }
136
+ return this._count === 1 ? created[0] : created;
137
+ }
138
+ }
139
+ /** Start a factory for a model with an attribute definition. */
140
+ export function factory(model, definition) {
141
+ return new Factory(model, definition);
142
+ }
143
+ /* -------------------------------- seeder ---------------------------------- */
144
+ export class Seeder {
145
+ /** Run other seeders in sequence — compose a `DatabaseSeeder` from parts. */
146
+ async call(seeders) {
147
+ for (const S of seeders)
148
+ await new S().run();
149
+ }
150
+ }
151
+ /** Instantiate and run a seeder class. */
152
+ export async function seed(SeederClass) {
153
+ await new SeederClass().run();
154
+ }
@@ -10,6 +10,9 @@
10
10
  import type { Application } from "./application.js";
11
11
  import type { Token, Factory } from "./container.js";
12
12
  import { type Renderable } from "./view.js";
13
+ import { Events, type Listener } from "./events.js";
14
+ import { Cache } from "./cache.js";
15
+ import { Logger } from "./logger.js";
13
16
  /** Register the active application. Called by the Application constructor. */
14
17
  export declare function setApplication(app: Application): void;
15
18
  /** The active application container. Throws if none has been created. */
@@ -29,6 +32,16 @@ export declare function instance<T>(token: Token<T>, value: T): T;
29
32
  export declare function make<T>(token: Token<T>): T;
30
33
  /** Whether a token is bound or has a cached instance. */
31
34
  export declare function bound(token: Token): boolean;
35
+ /** The application's event emitter. */
36
+ export declare function events(): Events;
37
+ /** Emit an event, awaiting every listener. */
38
+ export declare function emit<T = unknown>(event: string, payload?: T): Promise<void>;
39
+ /** Subscribe to an event; returns an unsubscribe function. */
40
+ export declare function listen<T = unknown>(event: string, listener: Listener<T>): () => void;
41
+ /** The application's cache. */
42
+ export declare function cache(): Cache;
43
+ /** The application's logger. */
44
+ export declare function logger(): Logger;
32
45
  /**
33
46
  * Render a view component through the View service, in one call:
34
47
  *
@@ -9,6 +9,9 @@
9
9
  */
10
10
  import { Config } from "./config.js";
11
11
  import { View } from "./view.js";
12
+ import { Events } from "./events.js";
13
+ import { Cache } from "./cache.js";
14
+ import { Logger } from "./logger.js";
12
15
  let current;
13
16
  /** Register the active application. Called by the Application constructor. */
14
17
  export function setApplication(app) {
@@ -50,6 +53,27 @@ export function make(token) {
50
53
  export function bound(token) {
51
54
  return app().bound(token);
52
55
  }
56
+ /* ------------------------------- events -------------------------------- */
57
+ /** The application's event emitter. */
58
+ export function events() {
59
+ return app().make(Events);
60
+ }
61
+ /** Emit an event, awaiting every listener. */
62
+ export function emit(event, payload) {
63
+ return events().emit(event, payload);
64
+ }
65
+ /** Subscribe to an event; returns an unsubscribe function. */
66
+ export function listen(event, listener) {
67
+ return events().on(event, listener);
68
+ }
69
+ /** The application's cache. */
70
+ export function cache() {
71
+ return app().make(Cache);
72
+ }
73
+ /** The application's logger. */
74
+ export function logger() {
75
+ return app().make(Logger);
76
+ }
53
77
  export function view(component, props) {
54
78
  return app().make(View).render(component(props));
55
79
  }
@@ -111,15 +111,32 @@ export class HttpKernel {
111
111
  for (const [param, rgx] of Object.entries(route.wheres)) {
112
112
  path = path.replace(new RegExp(`:${param}(\\??)`), `:${param}{${rgx}}$1`);
113
113
  }
114
- hono.on(route.methods, [path], ...route.middleware, honoHandler);
114
+ const middleware = route.middleware.map((m) => router.resolveMiddleware(m));
115
+ hono.on(route.methods, [path], ...middleware, honoHandler);
115
116
  }
116
117
  hono.notFound((c) => this.handle(new NotFoundException(`No route for ${c.req.method} ${c.req.path}`), c));
117
118
  hono.onError((err, c) => this.handle(err, c));
118
119
  return hono;
119
120
  }
120
- handle(err, c) {
121
+ async handle(err, c) {
122
+ // Reportable exceptions: give them a chance to log/report themselves.
123
+ const maybe = err;
124
+ if (typeof maybe?.report === "function") {
125
+ try {
126
+ await maybe.report();
127
+ }
128
+ catch {
129
+ /* reporting must never mask the original error */
130
+ }
131
+ }
121
132
  if (this.customErrorHandler)
122
133
  return this.customErrorHandler(err, c);
134
+ // Self-handling exceptions render themselves.
135
+ if (typeof maybe?.handle === "function") {
136
+ const rendered = await maybe.handle(c);
137
+ if (rendered instanceof Response)
138
+ return rendered;
139
+ }
123
140
  return this.renderException(err, c);
124
141
  }
125
142
  /** Default rendering: HTML for browsers, JSON otherwise; details in debug. */
@@ -140,6 +157,8 @@ export class HttpKernel {
140
157
  return c.html(this.errorPage(status, title, message, err, debug, c), code);
141
158
  }
142
159
  const body = { error: message, status };
160
+ if (isHttp && err.code)
161
+ body.code = err.code;
143
162
  if (err instanceof ValidationException)
144
163
  body.errors = err.errors;
145
164
  if (debug && !isHttp && err instanceof Error) {