@shaferllc/keel 0.35.0 → 0.58.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 +16 -2
- package/dist/core/application.d.ts +58 -2
- package/dist/core/application.js +99 -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 +1 -1
- package/dist/core/crypto.js +12 -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 +30 -6
- package/dist/core/index.js +16 -3
- 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/notification.d.ts +82 -0
- package/dist/core/notification.js +129 -0
- 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 +17 -1
package/dist/core/http/router.js
CHANGED
|
@@ -70,6 +70,11 @@ export class Route {
|
|
|
70
70
|
this.def.domain = pattern;
|
|
71
71
|
return this;
|
|
72
72
|
}
|
|
73
|
+
/** Attach arbitrary metadata to the route — read it via `request.route.config`. */
|
|
74
|
+
config(data) {
|
|
75
|
+
Object.assign(this.def.config, data);
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
73
78
|
}
|
|
74
79
|
/** A group of routes sharing a prefix, middleware, and/or name prefix. */
|
|
75
80
|
export class RouteGroup {
|
|
@@ -93,6 +98,12 @@ export class RouteGroup {
|
|
|
93
98
|
use(mw) {
|
|
94
99
|
return this.middleware(mw);
|
|
95
100
|
}
|
|
101
|
+
/** Attach metadata to every route in the group (a route's own config wins). */
|
|
102
|
+
config(data) {
|
|
103
|
+
for (const r of this.routes)
|
|
104
|
+
r.config = { ...data, ...r.config };
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
96
107
|
/** Constrain a parameter across every route in the group. */
|
|
97
108
|
where(param, matcher) {
|
|
98
109
|
for (const r of this.routes) {
|
|
@@ -317,10 +328,26 @@ export class Router {
|
|
|
317
328
|
handler,
|
|
318
329
|
middleware: [...this.group_mw],
|
|
319
330
|
wheres: {},
|
|
331
|
+
config: {},
|
|
320
332
|
};
|
|
321
333
|
this.routes.push(def);
|
|
334
|
+
for (const hook of this.routeHooks)
|
|
335
|
+
hook(def);
|
|
322
336
|
return new Route(def);
|
|
323
337
|
}
|
|
338
|
+
routeHooks = [];
|
|
339
|
+
/**
|
|
340
|
+
* Observe route registration — called with each route's definition as it's
|
|
341
|
+
* added (and replayed for routes already registered). Handy for logging or
|
|
342
|
+
* building an API map. The `def` is live, so reading it later reflects fluent
|
|
343
|
+
* config (`.name()`, `.middleware()`) applied after registration.
|
|
344
|
+
*/
|
|
345
|
+
onRoute(hook) {
|
|
346
|
+
for (const def of this.routes)
|
|
347
|
+
hook(def);
|
|
348
|
+
this.routeHooks.push(hook);
|
|
349
|
+
return this;
|
|
350
|
+
}
|
|
324
351
|
/** Register a global parameter constraint, applied to every matching route. */
|
|
325
352
|
where(param, matcher) {
|
|
326
353
|
this.globalWheres[param] = matcherSource(matcher);
|
|
@@ -344,7 +371,9 @@ export class Router {
|
|
|
344
371
|
throw new Error(`No route named [${name}].`);
|
|
345
372
|
let path = def.path;
|
|
346
373
|
for (const [k, v] of Object.entries(params)) {
|
|
347
|
-
|
|
374
|
+
// Global + word-boundary: replace every `:k` occurrence, and don't let
|
|
375
|
+
// `:id` match inside `:idx`.
|
|
376
|
+
path = path.replace(new RegExp(`:${k}\\b\\??`, "g"), encodeURIComponent(String(v)));
|
|
348
377
|
}
|
|
349
378
|
path = path.replace(/\/:[^/]+\?/g, "").replace(/:[^/]+/g, "");
|
|
350
379
|
if (options.qs && Object.keys(options.qs).length) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -2,23 +2,27 @@
|
|
|
2
2
|
export { Container } from "./container.js";
|
|
3
3
|
export type { Token, Constructor, Factory } from "./container.js";
|
|
4
4
|
export { Application } from "./application.js";
|
|
5
|
-
export type { BootOptions } from "./application.js";
|
|
5
|
+
export type { BootOptions, LifecycleHook, Configurator } from "./application.js";
|
|
6
6
|
export { Config, env } from "./config.js";
|
|
7
|
-
export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
|
|
7
|
+
export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
|
|
8
8
|
export { Logger } from "./logger.js";
|
|
9
9
|
export type { LogLevel, LoggerOptions } from "./logger.js";
|
|
10
|
+
export { requestLogger, requestLog } from "./request-logger.js";
|
|
11
|
+
export type { RequestLoggerOptions } from "./request-logger.js";
|
|
10
12
|
export { Events } from "./events.js";
|
|
11
13
|
export type { Listener } from "./events.js";
|
|
12
14
|
export { Cache, MemoryStore } from "./cache.js";
|
|
13
15
|
export type { CacheStore } from "./cache.js";
|
|
14
16
|
export { serveStatic } from "./static.js";
|
|
15
17
|
export type { StaticOptions } from "./static.js";
|
|
18
|
+
export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
|
|
19
|
+
export type { Disk, Contents } from "./storage.js";
|
|
16
20
|
export { dump, dd } from "./debug.js";
|
|
17
21
|
export { hash, encryption } from "./crypto.js";
|
|
18
22
|
export { rateLimiter } from "./rate-limit.js";
|
|
19
23
|
export type { RateLimiterOptions } from "./rate-limit.js";
|
|
20
24
|
export { db, setConnection, QueryBuilder } from "./database.js";
|
|
21
|
-
export type { Connection, WriteResult, Row, Dialect, Operator } from "./database.js";
|
|
25
|
+
export type { Connection, WriteResult, Row, Dialect, Operator, Paginated } from "./database.js";
|
|
22
26
|
export { Model } from "./model.js";
|
|
23
27
|
export type { CastType, Casts } from "./casts.js";
|
|
24
28
|
export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
|
|
@@ -28,12 +32,23 @@ export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail
|
|
|
28
32
|
export type { Message, Transport, MailerOptions, FetchTransportOptions } from "./mail.js";
|
|
29
33
|
export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
|
|
30
34
|
export type { Dispatchable, JobOptions, QueueDriver, Drainable, QueuedJob } from "./queue.js";
|
|
35
|
+
export { Scheduler, ScheduledTask, scheduler, setScheduler, schedule, cronMatches } from "./scheduler.js";
|
|
36
|
+
export { MemoryBroadcaster, broadcast, setBroadcaster, getBroadcaster, channelAuth, authorizeChannel, clearChannels, } from "./broadcasting.js";
|
|
37
|
+
export type { Broadcaster, Subscriber, ChannelAuthorizer } from "./broadcasting.js";
|
|
38
|
+
export { Redis, MemoryRedis, redis, setRedis, redisStore } from "./redis.js";
|
|
39
|
+
export type { RedisConnection, SetOptions } from "./redis.js";
|
|
40
|
+
export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
|
|
41
|
+
export type { Notifiable, MailContent, Channel } from "./notification.js";
|
|
31
42
|
export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
|
|
32
43
|
export type { Migration } from "./migrations.js";
|
|
33
44
|
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
45
|
+
export { decorateRequest, hasRequestDecorator, decorated, setRequestValue, clearRequestDecorators, } from "./decorators.js";
|
|
46
|
+
export type { RequestResolver } from "./decorators.js";
|
|
34
47
|
export type { ConfigData } from "./config.js";
|
|
35
48
|
export { View } from "./view.js";
|
|
36
49
|
export type { Renderable, ViewConfig } from "./view.js";
|
|
50
|
+
export { TemplateEngine, escapeHtml, templates, setTemplateEngine, render, } from "./template.js";
|
|
51
|
+
export type { Filter, RenderContext } from "./template.js";
|
|
37
52
|
export { ServiceProvider } from "./provider.js";
|
|
38
53
|
export type { ProviderClass } from "./provider.js";
|
|
39
54
|
export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
|
|
@@ -41,10 +56,19 @@ export type { Ctx, RouteHandler, RouteDefinition, Method, Matcher, MiddlewareRef
|
|
|
41
56
|
export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
|
|
42
57
|
export type { InertiaPage, InertiaOptions } from "./inertia.js";
|
|
43
58
|
export { HttpKernel } from "./http/kernel.js";
|
|
44
|
-
export {
|
|
45
|
-
export {
|
|
46
|
-
export
|
|
59
|
+
export { TestClient, TestResponse, testClient } from "./testing.js";
|
|
60
|
+
export { HttpException, BadRequestException, UnauthorizedException, PaymentRequiredException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, LengthRequiredException, ValidationException, TooManyRequestsException, ServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, createError, STATUS_TEXT, } from "./exceptions.js";
|
|
61
|
+
export { validate, validateRequest, validated } from "./validation.js";
|
|
62
|
+
export type { Schema, RequestSchemas } from "./validation.js";
|
|
47
63
|
export { Session, session, sessionMiddleware } from "./session.js";
|
|
48
64
|
export type { SessionOptions } from "./session.js";
|
|
49
65
|
export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
|
|
50
66
|
export type { UserProvider } from "./auth.js";
|
|
67
|
+
export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
|
|
68
|
+
export type { GateCallback, BeforeCallback } from "./authorization.js";
|
|
69
|
+
export { Transformer } from "./transformer.js";
|
|
70
|
+
export type { Attributes, DocumentOptions } from "./transformer.js";
|
|
71
|
+
export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
|
|
72
|
+
export type { ServiceSchema, Context, ActionHandler, EventHandler, ActionDef, ActionSchema, ActionHooks, EventSchema, ServiceHooks, Visibility, EventType, BeforeHook, AfterHook, ErrorHook, Transporter, BrokerOptions, BrokerMiddleware, CallOptions, EmitOptions, MCallDefs, MCallOptions, } from "./broker.js";
|
|
73
|
+
export { Vite, viteTags, viteAsset, viteReactRefresh } from "./vite.js";
|
|
74
|
+
export type { ViteOptions, Manifest, ManifestChunk, Attributes as ViteAttributes, AttrValue, } from "./vite.js";
|
package/dist/core/index.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
export { Container } from "./container.js";
|
|
3
3
|
export { Application } from "./application.js";
|
|
4
4
|
export { Config, env } from "./config.js";
|
|
5
|
-
export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
|
|
5
|
+
export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, onReady, onShutdown, terminate, } from "./helpers.js";
|
|
6
6
|
export { Logger } from "./logger.js";
|
|
7
|
+
export { requestLogger, requestLog } from "./request-logger.js";
|
|
7
8
|
export { Events } from "./events.js";
|
|
8
9
|
export { Cache, MemoryStore } from "./cache.js";
|
|
9
10
|
export { serveStatic } from "./static.js";
|
|
11
|
+
export { Storage, MemoryDisk, storage, setDisk } from "./storage.js";
|
|
10
12
|
export { dump, dd } from "./debug.js";
|
|
11
13
|
export { hash, encryption } from "./crypto.js";
|
|
12
14
|
export { rateLimiter } from "./rate-limit.js";
|
|
@@ -16,14 +18,25 @@ export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations
|
|
|
16
18
|
export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
|
|
17
19
|
export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
|
|
18
20
|
export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
|
|
21
|
+
export { Scheduler, ScheduledTask, scheduler, setScheduler, schedule, cronMatches } from "./scheduler.js";
|
|
22
|
+
export { MemoryBroadcaster, broadcast, setBroadcaster, getBroadcaster, channelAuth, authorizeChannel, clearChannels, } from "./broadcasting.js";
|
|
23
|
+
export { Redis, MemoryRedis, redis, setRedis, redisStore } from "./redis.js";
|
|
24
|
+
export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
|
|
19
25
|
export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
|
|
20
26
|
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
27
|
+
export { decorateRequest, hasRequestDecorator, decorated, setRequestValue, clearRequestDecorators, } from "./decorators.js";
|
|
21
28
|
export { View } from "./view.js";
|
|
29
|
+
export { TemplateEngine, escapeHtml, templates, setTemplateEngine, render, } from "./template.js";
|
|
22
30
|
export { ServiceProvider } from "./provider.js";
|
|
23
31
|
export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
|
|
24
32
|
export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
|
|
25
33
|
export { HttpKernel } from "./http/kernel.js";
|
|
26
|
-
export {
|
|
27
|
-
export {
|
|
34
|
+
export { TestClient, TestResponse, testClient } from "./testing.js";
|
|
35
|
+
export { HttpException, BadRequestException, UnauthorizedException, PaymentRequiredException, ForbiddenException, NotFoundException, MethodNotAllowedException, NotAcceptableException, RequestTimeoutException, ConflictException, LengthRequiredException, ValidationException, TooManyRequestsException, ServerErrorException, NotImplementedException, BadGatewayException, ServiceUnavailableException, createError, STATUS_TEXT, } from "./exceptions.js";
|
|
36
|
+
export { validate, validateRequest, validated } from "./validation.js";
|
|
28
37
|
export { Session, session, sessionMiddleware } from "./session.js";
|
|
29
38
|
export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
|
|
39
|
+
export { define, policy, gateBefore, setUserResolver, clearAuthorization, can, cannot, canFor, authorize, authorizeFor, } from "./authorization.js";
|
|
40
|
+
export { Transformer } from "./transformer.js";
|
|
41
|
+
export { Broker, Service, LocalTransporter, ServiceNotFoundError, RequestTimeoutError, broker, setBroker, } from "./broker.js";
|
|
42
|
+
export { Vite, viteTags, viteAsset, viteReactRefresh } from "./vite.js";
|
package/dist/core/logger.d.ts
CHANGED
|
@@ -14,6 +14,11 @@ export interface LoggerOptions {
|
|
|
14
14
|
pretty?: boolean;
|
|
15
15
|
/** Fields merged into every log line. */
|
|
16
16
|
bindings?: Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Field paths to redact from log output — top-level keys (`"password"`) or dot
|
|
19
|
+
* paths (`"req.headers.authorization"`). Matched values become `"[redacted]"`.
|
|
20
|
+
*/
|
|
21
|
+
redact?: string[];
|
|
17
22
|
}
|
|
18
23
|
export declare class Logger {
|
|
19
24
|
private options;
|
package/dist/core/logger.js
CHANGED
|
@@ -7,6 +7,24 @@
|
|
|
7
7
|
* logger().error("payment failed", { orderId, error });
|
|
8
8
|
*/
|
|
9
9
|
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
10
|
+
const REDACTED = "[redacted]";
|
|
11
|
+
/** Return a copy of `obj` with `path` (dot-separated) redacted — clones only along the path. */
|
|
12
|
+
function redactPath(obj, keys) {
|
|
13
|
+
const [head, ...rest] = keys;
|
|
14
|
+
if (head === undefined || !(head in obj))
|
|
15
|
+
return obj;
|
|
16
|
+
const clone = { ...obj };
|
|
17
|
+
if (rest.length === 0) {
|
|
18
|
+
clone[head] = REDACTED;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const child = clone[head];
|
|
22
|
+
if (child && typeof child === "object") {
|
|
23
|
+
clone[head] = redactPath(child, rest);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return clone;
|
|
27
|
+
}
|
|
10
28
|
export class Logger {
|
|
11
29
|
options;
|
|
12
30
|
threshold;
|
|
@@ -18,13 +36,17 @@ export class Logger {
|
|
|
18
36
|
if (LEVELS[level] < this.threshold)
|
|
19
37
|
return;
|
|
20
38
|
const time = new Date().toISOString();
|
|
39
|
+
let fields = { ...this.options.bindings, ...context };
|
|
40
|
+
for (const path of this.options.redact ?? []) {
|
|
41
|
+
fields = redactPath(fields, path.split("."));
|
|
42
|
+
}
|
|
21
43
|
if (this.options.pretty) {
|
|
22
|
-
const extra =
|
|
44
|
+
const extra = Object.keys(fields).length ? " " + JSON.stringify(fields) : "";
|
|
23
45
|
const fn = level === "warn" ? console.warn : level === "error" ? console.error : console.log;
|
|
24
46
|
fn(`[${time}] ${level.toUpperCase().padEnd(5)} ${message}${extra}`);
|
|
25
47
|
}
|
|
26
48
|
else {
|
|
27
|
-
console.log(JSON.stringify({ level, time, msg: message, ...
|
|
49
|
+
console.log(JSON.stringify({ level, time, msg: message, ...fields }));
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
52
|
debug(message, context) {
|
package/dist/core/migrations.js
CHANGED
|
@@ -165,11 +165,11 @@ export class Migrator {
|
|
|
165
165
|
/** Names of migrations already applied. */
|
|
166
166
|
async ran() {
|
|
167
167
|
await this.ensure();
|
|
168
|
-
const rows = await this.conn.select("SELECT name FROM migrations", []);
|
|
168
|
+
const rows = (await this.conn.select("SELECT name FROM migrations", []));
|
|
169
169
|
return rows.map((r) => String(r.name));
|
|
170
170
|
}
|
|
171
171
|
async maxBatch() {
|
|
172
|
-
const rows = await this.conn.select("SELECT MAX(batch) AS b FROM migrations", []);
|
|
172
|
+
const rows = (await this.conn.select("SELECT MAX(batch) AS b FROM migrations", []));
|
|
173
173
|
return Number(rows[0]?.b ?? 0);
|
|
174
174
|
}
|
|
175
175
|
/** Run all pending migrations. Returns the names applied. */
|
|
@@ -196,7 +196,7 @@ export class Migrator {
|
|
|
196
196
|
const batch = await this.maxBatch();
|
|
197
197
|
if (!batch)
|
|
198
198
|
return [];
|
|
199
|
-
const rows = await this.conn.select(this.ph("SELECT name FROM migrations WHERE batch = ?"), [batch]);
|
|
199
|
+
const rows = (await this.conn.select(this.ph("SELECT name FROM migrations WHERE batch = ?"), [batch]));
|
|
200
200
|
const schema = new SchemaBuilder(this.conn, this.dialect);
|
|
201
201
|
const rolled = [];
|
|
202
202
|
for (const name of rows.map((r) => String(r.name)).reverse()) {
|
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();
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
/** A recipient. Anything with routing info — often a `User` model. */
|
|
21
|
+
export interface Notifiable {
|
|
22
|
+
/** Return the address/id for a channel (e.g. an email). Falls back to `email`/`id`. */
|
|
23
|
+
routeNotificationFor?(channel: string): string | number | undefined;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** What a notification hands the mail channel. */
|
|
27
|
+
export interface MailContent {
|
|
28
|
+
subject: string;
|
|
29
|
+
text?: string;
|
|
30
|
+
html?: string;
|
|
31
|
+
from?: string;
|
|
32
|
+
/** Override the resolved recipient address. */
|
|
33
|
+
to?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare abstract class Notification {
|
|
36
|
+
/** Deliver from a queued job instead of inline. */
|
|
37
|
+
shouldQueue: boolean;
|
|
38
|
+
/** Channels to deliver on. Default: mail. */
|
|
39
|
+
via(_notifiable: Notifiable): string[];
|
|
40
|
+
/** Build the mail-channel content. Required if `via` includes "mail". */
|
|
41
|
+
toMail?(notifiable: Notifiable): MailContent;
|
|
42
|
+
/** Build the payload stored/serialized by the database and array channels. */
|
|
43
|
+
toArray?(notifiable: Notifiable): Record<string, unknown>;
|
|
44
|
+
}
|
|
45
|
+
/** Resolve where a notifiable receives a given channel. */
|
|
46
|
+
export declare function routeFor(notifiable: Notifiable, channel: string): string | number | undefined;
|
|
47
|
+
/** The bridge that actually delivers a notification on one channel. */
|
|
48
|
+
export interface Channel {
|
|
49
|
+
send(notifiable: Notifiable, notification: Notification): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
/** Delivers via the mailer, using the notification's `toMail`. */
|
|
52
|
+
export declare class MailChannel implements Channel {
|
|
53
|
+
send(notifiable: Notifiable, notification: Notification): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/** Persists the notification's `toArray` payload to a table via the query builder. */
|
|
56
|
+
export declare class DatabaseChannel implements Channel {
|
|
57
|
+
private table;
|
|
58
|
+
constructor(table?: string);
|
|
59
|
+
send(notifiable: Notifiable, notification: Notification): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
/** Collects deliveries in memory — for tests. */
|
|
62
|
+
export declare class ArrayChannel implements Channel {
|
|
63
|
+
readonly sent: {
|
|
64
|
+
notifiable: Notifiable;
|
|
65
|
+
notification: Notification;
|
|
66
|
+
}[];
|
|
67
|
+
send(notifiable: Notifiable, notification: Notification): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
export declare class Notifier {
|
|
70
|
+
private channels;
|
|
71
|
+
/** Register (or replace) a channel by name. */
|
|
72
|
+
channel(name: string, channel: Channel): this;
|
|
73
|
+
private deliver;
|
|
74
|
+
/** Send a notification to one or many recipients. */
|
|
75
|
+
send(notifiables: Notifiable | Notifiable[], notification: Notification): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
/** Register the default notifier used by `notify()`. */
|
|
78
|
+
export declare function setNotifier(instance: Notifier): Notifier;
|
|
79
|
+
/** The default notifier instance (register channels on it). */
|
|
80
|
+
export declare function getNotifier(): Notifier;
|
|
81
|
+
/** Send a notification to one or many notifiables via the default notifier. */
|
|
82
|
+
export declare function notify(notifiables: Notifiable | Notifiable[], notification: Notification): Promise<void>;
|
|
@@ -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
|
+
}
|