@shaferllc/keel 0.12.0 → 0.36.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 +30 -3
- package/dist/core/application.js +9 -0
- package/dist/core/auth.d.ts +41 -0
- package/dist/core/auth.js +70 -0
- package/dist/core/cache.d.ts +41 -0
- package/dist/core/cache.js +81 -0
- package/dist/core/casts.d.ts +19 -0
- package/dist/core/casts.js +69 -0
- package/dist/core/crypto.d.ts +24 -0
- package/dist/core/crypto.js +97 -0
- package/dist/core/database.d.ts +58 -0
- package/dist/core/database.js +140 -0
- package/dist/core/debug.d.ts +12 -0
- package/dist/core/debug.js +55 -0
- package/dist/core/events.d.ts +21 -0
- package/dist/core/events.js +46 -0
- package/dist/core/exceptions.d.ts +10 -1
- package/dist/core/exceptions.js +10 -1
- package/dist/core/factory.d.ts +84 -0
- package/dist/core/factory.js +154 -0
- package/dist/core/helpers.d.ts +13 -0
- package/dist/core/helpers.js +24 -0
- package/dist/core/http/kernel.js +21 -2
- package/dist/core/http/router.d.ts +32 -8
- package/dist/core/http/router.js +74 -5
- package/dist/core/index.d.ts +33 -2
- package/dist/core/index.js +18 -1
- package/dist/core/logger.d.ts +29 -0
- package/dist/core/logger.js +49 -0
- package/dist/core/mail.d.ts +94 -0
- package/dist/core/mail.js +157 -0
- package/dist/core/migrations.d.ts +76 -0
- package/dist/core/migrations.js +211 -0
- package/dist/core/model.d.ts +75 -0
- package/dist/core/model.js +202 -0
- package/dist/core/notification.d.ts +82 -0
- package/dist/core/notification.js +129 -0
- package/dist/core/queue.d.ts +73 -0
- package/dist/core/queue.js +88 -0
- package/dist/core/rate-limit.d.ts +23 -0
- package/dist/core/rate-limit.js +50 -0
- package/dist/core/relations.d.ts +90 -0
- package/dist/core/relations.js +229 -0
- package/dist/core/request.d.ts +53 -1
- package/dist/core/request.js +187 -0
- package/dist/core/session.d.ts +46 -0
- package/dist/core/session.js +112 -0
- package/dist/core/static.d.ts +27 -0
- package/dist/core/static.js +61 -0
- package/package.json +2 -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 & 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
|
-
/**
|
|
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 {
|
package/dist/core/exceptions.js
CHANGED
|
@@ -16,10 +16,19 @@ export const STATUS_TEXT = {
|
|
|
16
16
|
500: "Internal Server Error",
|
|
17
17
|
503: "Service Unavailable",
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
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
|
+
}
|
package/dist/core/helpers.d.ts
CHANGED
|
@@ -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
|
*
|
package/dist/core/helpers.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/http/kernel.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|