@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.
Files changed (50) hide show
  1. package/README.md +30 -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 +33 -2
  27. package/dist/core/index.js +18 -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/notification.d.ts +82 -0
  37. package/dist/core/notification.js +129 -0
  38. package/dist/core/queue.d.ts +73 -0
  39. package/dist/core/queue.js +88 -0
  40. package/dist/core/rate-limit.d.ts +23 -0
  41. package/dist/core/rate-limit.js +50 -0
  42. package/dist/core/relations.d.ts +90 -0
  43. package/dist/core/relations.js +229 -0
  44. package/dist/core/request.d.ts +53 -1
  45. package/dist/core/request.js +187 -0
  46. package/dist/core/session.d.ts +46 -0
  47. package/dist/core/session.js +112 -0
  48. package/dist/core/static.d.ts +27 -0
  49. package/dist/core/static.js +61 -0
  50. package/package.json +2 -1
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Notifications — send a message to a recipient over one or more channels
3
+ * (mail, database, or your own), inline or through the queue. This is where the
4
+ * mail and queue layers compose: a notification declares *what* to say and
5
+ * *which channels* carry it, and each channel decides *how*.
6
+ *
7
+ * class InvoicePaid extends Notification {
8
+ * constructor(private amount: number) { super(); }
9
+ * via() { return ["mail", "database"]; }
10
+ * toMail() { return { subject: "Payment received", text: `Thanks for $${this.amount}.` }; }
11
+ * toArray() { return { amount: this.amount }; }
12
+ * }
13
+ *
14
+ * await notify(user, new InvoicePaid(4200)); // user.email routes the mail
15
+ *
16
+ * Set `shouldQueue = true` on a notification to deliver it from a queued job
17
+ * instead of inline. Channels are pluggable, so `array` (for tests) or a custom
18
+ * push-to-provider channel slot in the same way transports and drivers do.
19
+ */
20
+ import { db } from "./database.js";
21
+ import { getMailer } from "./mail.js";
22
+ import { dispatch } from "./queue.js";
23
+ export class Notification {
24
+ /** Deliver from a queued job instead of inline. */
25
+ shouldQueue = false;
26
+ /** Channels to deliver on. Default: mail. */
27
+ via(_notifiable) {
28
+ return ["mail"];
29
+ }
30
+ }
31
+ /** Resolve where a notifiable receives a given channel. */
32
+ export function routeFor(notifiable, channel) {
33
+ if (typeof notifiable.routeNotificationFor === "function") {
34
+ const route = notifiable.routeNotificationFor(channel);
35
+ if (route !== undefined && route !== null)
36
+ return route;
37
+ }
38
+ if (channel === "mail")
39
+ return notifiable.email;
40
+ return notifiable.id;
41
+ }
42
+ /* -------------------------------- channels -------------------------------- */
43
+ /** Delivers via the mailer, using the notification's `toMail`. */
44
+ export class MailChannel {
45
+ async send(notifiable, notification) {
46
+ if (!notification.toMail) {
47
+ throw new Error(`${notification.constructor.name} has no toMail() for the mail channel.`);
48
+ }
49
+ const content = notification.toMail(notifiable);
50
+ const to = content.to ?? routeFor(notifiable, "mail");
51
+ if (!to) {
52
+ throw new Error("Notification: no mail route (set `email` or `routeNotificationFor`).");
53
+ }
54
+ const message = getMailer().message().to(String(to)).subject(content.subject);
55
+ if (content.from)
56
+ message.from(content.from);
57
+ if (content.text)
58
+ message.text(content.text);
59
+ if (content.html)
60
+ message.html(content.html);
61
+ await message.send();
62
+ }
63
+ }
64
+ /** Persists the notification's `toArray` payload to a table via the query builder. */
65
+ export class DatabaseChannel {
66
+ table;
67
+ constructor(table = "notifications") {
68
+ this.table = table;
69
+ }
70
+ async send(notifiable, notification) {
71
+ const data = notification.toArray ? notification.toArray(notifiable) : {};
72
+ await db(this.table).insert({
73
+ type: notification.constructor.name,
74
+ notifiable_id: routeFor(notifiable, "database") ?? null,
75
+ data: JSON.stringify(data),
76
+ });
77
+ }
78
+ }
79
+ /** Collects deliveries in memory — for tests. */
80
+ export class ArrayChannel {
81
+ sent = [];
82
+ async send(notifiable, notification) {
83
+ this.sent.push({ notifiable, notification });
84
+ }
85
+ }
86
+ /* -------------------------------- notifier -------------------------------- */
87
+ export class Notifier {
88
+ channels = new Map([["mail", new MailChannel()]]);
89
+ /** Register (or replace) a channel by name. */
90
+ channel(name, channel) {
91
+ this.channels.set(name, channel);
92
+ return this;
93
+ }
94
+ async deliver(notifiable, notification) {
95
+ for (const name of notification.via(notifiable)) {
96
+ const channel = this.channels.get(name);
97
+ if (!channel)
98
+ throw new Error(`No notification channel "${name}" registered.`);
99
+ await channel.send(notifiable, notification);
100
+ }
101
+ }
102
+ /** Send a notification to one or many recipients. */
103
+ async send(notifiables, notification) {
104
+ const recipients = Array.isArray(notifiables) ? notifiables : [notifiables];
105
+ const run = async () => {
106
+ for (const recipient of recipients)
107
+ await this.deliver(recipient, notification);
108
+ };
109
+ if (notification.shouldQueue)
110
+ await dispatch(run);
111
+ else
112
+ await run();
113
+ }
114
+ }
115
+ /* --------------------------------- global --------------------------------- */
116
+ let notifier = new Notifier();
117
+ /** Register the default notifier used by `notify()`. */
118
+ export function setNotifier(instance) {
119
+ notifier = instance;
120
+ return notifier;
121
+ }
122
+ /** The default notifier instance (register channels on it). */
123
+ export function getNotifier() {
124
+ return notifier;
125
+ }
126
+ /** Send a notification to one or many notifiables via the default notifier. */
127
+ export function notify(notifiables, notification) {
128
+ return notifier.send(notifiables, notification);
129
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * A small queue for background work. Dispatch a `Job` (or a plain function) and
3
+ * a pluggable `QueueDriver` decides when it runs — immediately (the default),
4
+ * held in memory for a worker to drain, or handed to a real broker. The API
5
+ * mirrors the database and mail layers (`setQueue` / `dispatch` are to queues
6
+ * what `setConnection` / `db()` are to the database). The core imports no broker,
7
+ * so it stays edge-safe.
8
+ *
9
+ * class SendWelcome extends Job {
10
+ * constructor(private userId: number) { super(); }
11
+ * async handle() { await mail().to(...).send(); }
12
+ * }
13
+ *
14
+ * await dispatch(new SendWelcome(user.id)); // runs now with the sync driver
15
+ *
16
+ * // Defer instead, and drain with a worker:
17
+ * setQueue(new MemoryDriver());
18
+ * await dispatch(new SendWelcome(user.id)); // queued
19
+ * await work(); // runs everything pending
20
+ */
21
+ /** A unit of background work. Subclass and implement `handle`. */
22
+ export declare abstract class Job {
23
+ abstract handle(): void | Promise<void>;
24
+ }
25
+ /** Anything dispatchable: a `Job` instance or a plain function. */
26
+ export type Dispatchable = Job | (() => void | Promise<void>);
27
+ export interface JobOptions {
28
+ /** Seconds to wait before the job becomes available (drivers may honor it). */
29
+ delay?: number;
30
+ /** Named queue/lane to place the job on. */
31
+ queue?: string;
32
+ }
33
+ /** The bridge to your queue backend. */
34
+ export interface QueueDriver {
35
+ push(job: Dispatchable, options: JobOptions): Promise<void>;
36
+ }
37
+ /** A driver that holds jobs locally and can run them on demand. */
38
+ export interface Drainable {
39
+ readonly size: number;
40
+ work(): Promise<number>;
41
+ }
42
+ /** Runs jobs the moment they're dispatched — the default; ideal for dev/tests. */
43
+ export declare class SyncDriver implements QueueDriver {
44
+ push(job: Dispatchable, _options: JobOptions): Promise<void>;
45
+ }
46
+ export interface QueuedJob {
47
+ job: Dispatchable;
48
+ options: JobOptions;
49
+ }
50
+ /** Holds jobs in memory; `work()` drains them. Assert on `.jobs` in tests. */
51
+ export declare class MemoryDriver implements QueueDriver, Drainable {
52
+ readonly jobs: QueuedJob[];
53
+ push(job: Dispatchable, options: JobOptions): Promise<void>;
54
+ get size(): number;
55
+ /** Run every pending job in FIFO order; returns how many ran. */
56
+ work(): Promise<number>;
57
+ }
58
+ export declare class Queue {
59
+ readonly driver: QueueDriver;
60
+ constructor(driver: QueueDriver);
61
+ /** Place a job on the queue (the driver decides when it runs). */
62
+ dispatch(job: Dispatchable, options?: JobOptions): Promise<void>;
63
+ /** Drain the driver if it holds jobs locally; returns how many ran. */
64
+ work(): Promise<number>;
65
+ }
66
+ /** Register the default queue driver used by `dispatch()`. */
67
+ export declare function setQueue(driver: QueueDriver): Queue;
68
+ /** The default queue instance. */
69
+ export declare function getQueue(): Queue;
70
+ /** Dispatch a job (or function) onto the default queue. */
71
+ export declare function dispatch(job: Dispatchable, options?: JobOptions): Promise<void>;
72
+ /** Drain the default queue's pending jobs (no-op for immediate drivers). */
73
+ export declare function work(): Promise<number>;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * A small queue for background work. Dispatch a `Job` (or a plain function) and
3
+ * a pluggable `QueueDriver` decides when it runs — immediately (the default),
4
+ * held in memory for a worker to drain, or handed to a real broker. The API
5
+ * mirrors the database and mail layers (`setQueue` / `dispatch` are to queues
6
+ * what `setConnection` / `db()` are to the database). The core imports no broker,
7
+ * so it stays edge-safe.
8
+ *
9
+ * class SendWelcome extends Job {
10
+ * constructor(private userId: number) { super(); }
11
+ * async handle() { await mail().to(...).send(); }
12
+ * }
13
+ *
14
+ * await dispatch(new SendWelcome(user.id)); // runs now with the sync driver
15
+ *
16
+ * // Defer instead, and drain with a worker:
17
+ * setQueue(new MemoryDriver());
18
+ * await dispatch(new SendWelcome(user.id)); // queued
19
+ * await work(); // runs everything pending
20
+ */
21
+ /** A unit of background work. Subclass and implement `handle`. */
22
+ export class Job {
23
+ }
24
+ function run(job) {
25
+ return Promise.resolve(job instanceof Job ? job.handle() : job());
26
+ }
27
+ /* -------------------------------- drivers --------------------------------- */
28
+ /** Runs jobs the moment they're dispatched — the default; ideal for dev/tests. */
29
+ export class SyncDriver {
30
+ async push(job, _options) {
31
+ await run(job);
32
+ }
33
+ }
34
+ /** Holds jobs in memory; `work()` drains them. Assert on `.jobs` in tests. */
35
+ export class MemoryDriver {
36
+ jobs = [];
37
+ async push(job, options) {
38
+ this.jobs.push({ job, options });
39
+ }
40
+ get size() {
41
+ return this.jobs.length;
42
+ }
43
+ /** Run every pending job in FIFO order; returns how many ran. */
44
+ async work() {
45
+ let count = 0;
46
+ while (this.jobs.length) {
47
+ const next = this.jobs.shift();
48
+ await run(next.job);
49
+ count++;
50
+ }
51
+ return count;
52
+ }
53
+ }
54
+ /* --------------------------------- queue ---------------------------------- */
55
+ export class Queue {
56
+ driver;
57
+ constructor(driver) {
58
+ this.driver = driver;
59
+ }
60
+ /** Place a job on the queue (the driver decides when it runs). */
61
+ async dispatch(job, options = {}) {
62
+ await this.driver.push(job, options);
63
+ }
64
+ /** Drain the driver if it holds jobs locally; returns how many ran. */
65
+ async work() {
66
+ const drainable = this.driver;
67
+ return typeof drainable.work === "function" ? drainable.work() : 0;
68
+ }
69
+ }
70
+ /* --------------------------------- global --------------------------------- */
71
+ let queue = new Queue(new SyncDriver());
72
+ /** Register the default queue driver used by `dispatch()`. */
73
+ export function setQueue(driver) {
74
+ queue = new Queue(driver);
75
+ return queue;
76
+ }
77
+ /** The default queue instance. */
78
+ export function getQueue() {
79
+ return queue;
80
+ }
81
+ /** Dispatch a job (or function) onto the default queue. */
82
+ export function dispatch(job, options) {
83
+ return queue.dispatch(job, options);
84
+ }
85
+ /** Drain the default queue's pending jobs (no-op for immediate drivers). */
86
+ export function work() {
87
+ return queue.work();
88
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A fixed-window rate-limiting middleware. Limits requests per key (the client
3
+ * IP by default) within a time window, and sets the standard `X-RateLimit-*`
4
+ * and `Retry-After` headers.
5
+ *
6
+ * this.use(rateLimiter({ max: 60, window: 60 })); // 60 req / minute
7
+ * router.post("/login", handler).use(rateLimiter({ max: 5, window: 60 }));
8
+ *
9
+ * The default store is in-memory (per process / per isolate). For distributed
10
+ * limiting, pass a store backed by Redis/KV.
11
+ */
12
+ import type { Context, MiddlewareHandler } from "hono";
13
+ export interface RateLimiterOptions {
14
+ /** Max requests allowed per window. Default: 60. */
15
+ max?: number;
16
+ /** Window length in seconds. Default: 60. */
17
+ window?: number;
18
+ /** Derive the bucket key from the request. Default: client IP. */
19
+ key?: (c: Context) => string;
20
+ /** Response message on 429. */
21
+ message?: string;
22
+ }
23
+ export declare function rateLimiter(options?: RateLimiterOptions): MiddlewareHandler;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * A fixed-window rate-limiting middleware. Limits requests per key (the client
3
+ * IP by default) within a time window, and sets the standard `X-RateLimit-*`
4
+ * and `Retry-After` headers.
5
+ *
6
+ * this.use(rateLimiter({ max: 60, window: 60 })); // 60 req / minute
7
+ * router.post("/login", handler).use(rateLimiter({ max: 5, window: 60 }));
8
+ *
9
+ * The default store is in-memory (per process / per isolate). For distributed
10
+ * limiting, pass a store backed by Redis/KV.
11
+ */
12
+ function defaultKey(c) {
13
+ return (c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
14
+ c.req.header("x-real-ip") ??
15
+ "global");
16
+ }
17
+ export function rateLimiter(options = {}) {
18
+ const max = options.max ?? 60;
19
+ const windowMs = (options.window ?? 60) * 1000;
20
+ const keyFor = options.key ?? defaultKey;
21
+ const buckets = new Map();
22
+ return async (c, next) => {
23
+ const now = Date.now();
24
+ const key = keyFor(c);
25
+ // Occasionally prune expired buckets to bound memory.
26
+ if (buckets.size > 10_000) {
27
+ for (const [k, b] of buckets)
28
+ if (b.reset <= now)
29
+ buckets.delete(k);
30
+ }
31
+ let bucket = buckets.get(key);
32
+ if (!bucket || bucket.reset <= now) {
33
+ bucket = { count: 0, reset: now + windowMs };
34
+ buckets.set(key, bucket);
35
+ }
36
+ bucket.count++;
37
+ const remaining = Math.max(0, max - bucket.count);
38
+ if (bucket.count > max) {
39
+ return c.json({ error: options.message ?? "Too Many Requests", status: 429 }, 429, {
40
+ "X-RateLimit-Limit": String(max),
41
+ "X-RateLimit-Remaining": "0",
42
+ "Retry-After": String(Math.ceil((bucket.reset - now) / 1000)),
43
+ });
44
+ }
45
+ await next();
46
+ // Set headers on the final response (survives any handler type).
47
+ c.header("X-RateLimit-Limit", String(max));
48
+ c.header("X-RateLimit-Remaining", String(remaining));
49
+ };
50
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Model relationships over the query builder. Define a relationship as a method
3
+ * on your model that returns one of these, then read it lazily (`await`) or
4
+ * eager-load many at once with `Model.load()` to avoid N+1 queries.
5
+ *
6
+ * class User extends Model {
7
+ * static table = "users";
8
+ * posts() { return this.hasMany(Post); } // user_id on posts
9
+ * profile() { return this.hasOne(Profile); }
10
+ * roles() { return this.belongsToMany(Role); } // role_user pivot
11
+ * }
12
+ * class Post extends Model {
13
+ * static table = "posts";
14
+ * author() { return this.belongsTo(User); } // user_id on posts
15
+ * }
16
+ *
17
+ * const posts = await user.posts(); // relations are awaitable
18
+ * const author = await post.author();
19
+ *
20
+ * const users = await User.all();
21
+ * await User.load(users, "posts"); // one extra query, not N
22
+ * users[0].getRelation("posts");
23
+ *
24
+ * Every relation is edge-safe: it only uses the driver-agnostic query builder
25
+ * (belongsToMany runs two `whereIn` reads instead of a JOIN).
26
+ */
27
+ import { type QueryBuilder, type Row } from "./database.js";
28
+ import type { Model } from "./model.js";
29
+ type ModelClass<T extends Model> = (new (attributes?: Row) => T) & typeof Model;
30
+ /** Base class: a relationship is awaitable (resolves to its loaded result). */
31
+ export declare abstract class Relation<TRelated extends Model, TResult> implements PromiseLike<TResult> {
32
+ protected parent: Model;
33
+ protected related: ModelClass<TRelated>;
34
+ constructor(parent: Model, related: ModelClass<TRelated>);
35
+ /** The query builder with the relationship constraint applied. */
36
+ abstract query(): QueryBuilder;
37
+ /** Load the relationship for this single parent. */
38
+ abstract get(): Promise<TResult>;
39
+ /** Batch-load this relationship onto many parents, then assign each result. */
40
+ abstract eager(models: Model[], name: string): Promise<void>;
41
+ then<R1 = TResult, R2 = never>(onFulfilled?: ((value: TResult) => R1 | PromiseLike<R1>) | null, onRejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null): PromiseLike<R1 | R2>;
42
+ protected hydrate(rows: Row[]): TRelated[];
43
+ }
44
+ export declare class HasMany<T extends Model> extends Relation<T, T[]> {
45
+ private foreignKey;
46
+ private localKey;
47
+ constructor(parent: Model, related: ModelClass<T>, foreignKey: string, localKey: string);
48
+ private localValue;
49
+ query(): QueryBuilder;
50
+ get(): Promise<T[]>;
51
+ eager(models: Model[], name: string): Promise<void>;
52
+ }
53
+ export declare class HasOne<T extends Model> extends Relation<T, T | null> {
54
+ private foreignKey;
55
+ private localKey;
56
+ constructor(parent: Model, related: ModelClass<T>, foreignKey: string, localKey: string);
57
+ private localValue;
58
+ query(): QueryBuilder;
59
+ get(): Promise<T | null>;
60
+ eager(models: Model[], name: string): Promise<void>;
61
+ }
62
+ export declare class BelongsTo<T extends Model> extends Relation<T, T | null> {
63
+ private foreignKey;
64
+ private ownerKey;
65
+ constructor(parent: Model, related: ModelClass<T>, foreignKey: string, ownerKey: string);
66
+ private foreignValue;
67
+ query(): QueryBuilder;
68
+ get(): Promise<T | null>;
69
+ eager(models: Model[], name: string): Promise<void>;
70
+ }
71
+ export declare class BelongsToMany<T extends Model> extends Relation<T, T[]> {
72
+ private pivotTable;
73
+ private foreignPivotKey;
74
+ private relatedPivotKey;
75
+ private parentKey;
76
+ private relatedKey;
77
+ constructor(parent: Model, related: ModelClass<T>, pivotTable: string, foreignPivotKey: string, relatedPivotKey: string, parentKey: string, relatedKey: string);
78
+ private parentValue;
79
+ /** The query against the related table, once pivot rows are known. */
80
+ query(): QueryBuilder;
81
+ get(): Promise<T[]>;
82
+ eager(models: Model[], name: string): Promise<void>;
83
+ /** Attach a related row by linking it through the pivot table. */
84
+ attach(id: unknown, extra?: Row): Promise<void>;
85
+ /** Detach one related row (or all, when no id is given). */
86
+ detach(id?: unknown): Promise<void>;
87
+ /** Make the pivot contain exactly `ids` (detach the rest, attach the new). */
88
+ sync(ids: unknown[]): Promise<void>;
89
+ }
90
+ export {};
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Model relationships over the query builder. Define a relationship as a method
3
+ * on your model that returns one of these, then read it lazily (`await`) or
4
+ * eager-load many at once with `Model.load()` to avoid N+1 queries.
5
+ *
6
+ * class User extends Model {
7
+ * static table = "users";
8
+ * posts() { return this.hasMany(Post); } // user_id on posts
9
+ * profile() { return this.hasOne(Profile); }
10
+ * roles() { return this.belongsToMany(Role); } // role_user pivot
11
+ * }
12
+ * class Post extends Model {
13
+ * static table = "posts";
14
+ * author() { return this.belongsTo(User); } // user_id on posts
15
+ * }
16
+ *
17
+ * const posts = await user.posts(); // relations are awaitable
18
+ * const author = await post.author();
19
+ *
20
+ * const users = await User.all();
21
+ * await User.load(users, "posts"); // one extra query, not N
22
+ * users[0].getRelation("posts");
23
+ *
24
+ * Every relation is edge-safe: it only uses the driver-agnostic query builder
25
+ * (belongsToMany runs two `whereIn` reads instead of a JOIN).
26
+ */
27
+ import { db } from "./database.js";
28
+ function unique(values) {
29
+ return [...new Set(values)];
30
+ }
31
+ /** Base class: a relationship is awaitable (resolves to its loaded result). */
32
+ export class Relation {
33
+ parent;
34
+ related;
35
+ constructor(parent, related) {
36
+ this.parent = parent;
37
+ this.related = related;
38
+ }
39
+ then(onFulfilled, onRejected) {
40
+ return this.get().then(onFulfilled, onRejected);
41
+ }
42
+ hydrate(rows) {
43
+ return rows.map((row) => new this.related(row));
44
+ }
45
+ }
46
+ /* --------------------------------- has-many -------------------------------- */
47
+ export class HasMany extends Relation {
48
+ foreignKey;
49
+ localKey;
50
+ constructor(parent, related, foreignKey, localKey) {
51
+ super(parent, related);
52
+ this.foreignKey = foreignKey;
53
+ this.localKey = localKey;
54
+ }
55
+ localValue() {
56
+ return this.parent[this.localKey];
57
+ }
58
+ query() {
59
+ return db(this.related.table).where(this.foreignKey, this.localValue());
60
+ }
61
+ async get() {
62
+ return this.hydrate(await this.query().get());
63
+ }
64
+ async eager(models, name) {
65
+ const keys = unique(models.map((m) => m[this.localKey]).filter((v) => v != null));
66
+ const rows = keys.length
67
+ ? await db(this.related.table).whereIn(this.foreignKey, keys).get()
68
+ : [];
69
+ const grouped = new Map();
70
+ for (const row of rows) {
71
+ const bucket = grouped.get(row[this.foreignKey]) ?? [];
72
+ bucket.push(new this.related(row));
73
+ grouped.set(row[this.foreignKey], bucket);
74
+ }
75
+ for (const m of models) {
76
+ m.setRelation(name, grouped.get(m[this.localKey]) ?? []);
77
+ }
78
+ }
79
+ }
80
+ /* --------------------------------- has-one --------------------------------- */
81
+ export class HasOne extends Relation {
82
+ foreignKey;
83
+ localKey;
84
+ constructor(parent, related, foreignKey, localKey) {
85
+ super(parent, related);
86
+ this.foreignKey = foreignKey;
87
+ this.localKey = localKey;
88
+ }
89
+ localValue() {
90
+ return this.parent[this.localKey];
91
+ }
92
+ query() {
93
+ return db(this.related.table).where(this.foreignKey, this.localValue());
94
+ }
95
+ async get() {
96
+ const row = await this.query().first();
97
+ return row ? new this.related(row) : null;
98
+ }
99
+ async eager(models, name) {
100
+ const keys = unique(models.map((m) => m[this.localKey]).filter((v) => v != null));
101
+ const rows = keys.length
102
+ ? await db(this.related.table).whereIn(this.foreignKey, keys).get()
103
+ : [];
104
+ const byKey = new Map();
105
+ for (const row of rows) {
106
+ if (!byKey.has(row[this.foreignKey]))
107
+ byKey.set(row[this.foreignKey], new this.related(row));
108
+ }
109
+ for (const m of models) {
110
+ m.setRelation(name, byKey.get(m[this.localKey]) ?? null);
111
+ }
112
+ }
113
+ }
114
+ /* -------------------------------- belongs-to ------------------------------- */
115
+ export class BelongsTo extends Relation {
116
+ foreignKey;
117
+ ownerKey;
118
+ constructor(parent, related, foreignKey, ownerKey) {
119
+ super(parent, related);
120
+ this.foreignKey = foreignKey;
121
+ this.ownerKey = ownerKey;
122
+ }
123
+ foreignValue() {
124
+ return this.parent[this.foreignKey];
125
+ }
126
+ query() {
127
+ return db(this.related.table).where(this.ownerKey, this.foreignValue());
128
+ }
129
+ async get() {
130
+ if (this.foreignValue() == null)
131
+ return null;
132
+ const row = await this.query().first();
133
+ return row ? new this.related(row) : null;
134
+ }
135
+ async eager(models, name) {
136
+ const keys = unique(models.map((m) => m[this.foreignKey]).filter((v) => v != null));
137
+ const rows = keys.length
138
+ ? await db(this.related.table).whereIn(this.ownerKey, keys).get()
139
+ : [];
140
+ const byKey = new Map();
141
+ for (const row of rows)
142
+ byKey.set(row[this.ownerKey], new this.related(row));
143
+ for (const m of models) {
144
+ m.setRelation(name, byKey.get(m[this.foreignKey]) ?? null);
145
+ }
146
+ }
147
+ }
148
+ /* ----------------------------- belongs-to-many ----------------------------- */
149
+ export class BelongsToMany extends Relation {
150
+ pivotTable;
151
+ foreignPivotKey;
152
+ relatedPivotKey;
153
+ parentKey;
154
+ relatedKey;
155
+ constructor(parent, related, pivotTable, foreignPivotKey, relatedPivotKey, parentKey, relatedKey) {
156
+ super(parent, related);
157
+ this.pivotTable = pivotTable;
158
+ this.foreignPivotKey = foreignPivotKey;
159
+ this.relatedPivotKey = relatedPivotKey;
160
+ this.parentKey = parentKey;
161
+ this.relatedKey = relatedKey;
162
+ }
163
+ parentValue() {
164
+ return this.parent[this.parentKey];
165
+ }
166
+ /** The query against the related table, once pivot rows are known. */
167
+ query() {
168
+ return db(this.related.table);
169
+ }
170
+ async get() {
171
+ if (this.parentValue() == null)
172
+ return [];
173
+ const pivots = await db(this.pivotTable)
174
+ .where(this.foreignPivotKey, this.parentValue())
175
+ .get();
176
+ const relatedIds = unique(pivots.map((p) => p[this.relatedPivotKey]));
177
+ if (!relatedIds.length)
178
+ return [];
179
+ const rows = await db(this.related.table).whereIn(this.relatedKey, relatedIds).get();
180
+ return this.hydrate(rows);
181
+ }
182
+ async eager(models, name) {
183
+ const parentIds = unique(models.map((m) => m[this.parentKey]).filter((v) => v != null));
184
+ if (!parentIds.length) {
185
+ for (const m of models)
186
+ m.setRelation(name, []);
187
+ return;
188
+ }
189
+ const pivots = await db(this.pivotTable).whereIn(this.foreignPivotKey, parentIds).get();
190
+ const relatedIds = unique(pivots.map((p) => p[this.relatedPivotKey]));
191
+ const rows = relatedIds.length
192
+ ? await db(this.related.table).whereIn(this.relatedKey, relatedIds).get()
193
+ : [];
194
+ const relatedById = new Map(rows.map((row) => [row[this.relatedKey], row]));
195
+ const grouped = new Map();
196
+ for (const pivot of pivots) {
197
+ const row = relatedById.get(pivot[this.relatedPivotKey]);
198
+ if (!row)
199
+ continue;
200
+ const bucket = grouped.get(pivot[this.foreignPivotKey]) ?? [];
201
+ bucket.push(new this.related(row));
202
+ grouped.set(pivot[this.foreignPivotKey], bucket);
203
+ }
204
+ for (const m of models) {
205
+ m.setRelation(name, grouped.get(m[this.parentKey]) ?? []);
206
+ }
207
+ }
208
+ /** Attach a related row by linking it through the pivot table. */
209
+ async attach(id, extra = {}) {
210
+ await db(this.pivotTable).insert({
211
+ [this.foreignPivotKey]: this.parentValue(),
212
+ [this.relatedPivotKey]: id,
213
+ ...extra,
214
+ });
215
+ }
216
+ /** Detach one related row (or all, when no id is given). */
217
+ async detach(id) {
218
+ let q = db(this.pivotTable).where(this.foreignPivotKey, this.parentValue());
219
+ if (id !== undefined)
220
+ q = q.where(this.relatedPivotKey, id);
221
+ await q.delete();
222
+ }
223
+ /** Make the pivot contain exactly `ids` (detach the rest, attach the new). */
224
+ async sync(ids) {
225
+ await this.detach();
226
+ for (const id of ids)
227
+ await this.attach(id);
228
+ }
229
+ }