@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
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Context as HonoContext, MiddlewareHandler } from "hono";
|
|
9
9
|
import type { Container, Constructor } from "../container.js";
|
|
10
|
+
export interface UrlOptions {
|
|
11
|
+
qs?: Record<string, string | number>;
|
|
12
|
+
}
|
|
13
|
+
export interface SignedUrlOptions extends UrlOptions {
|
|
14
|
+
/** Expiry in seconds from now. */
|
|
15
|
+
expiresIn?: number;
|
|
16
|
+
}
|
|
10
17
|
/** The request context handed to every route handler and middleware. */
|
|
11
18
|
export type Ctx = HonoContext;
|
|
12
19
|
/** A route-parameter constraint: a regex, a source string, or `{ match }`. */
|
|
@@ -34,13 +41,15 @@ export type ControllerRef = Constructor | LazyController;
|
|
|
34
41
|
export type ControllerAction = [ControllerRef] | [ControllerRef, string];
|
|
35
42
|
/** A function, a controller action, or a ready-made Response. */
|
|
36
43
|
export type RouteHandler = HandlerFn | ControllerAction | Response;
|
|
44
|
+
/** A middleware handler, or the name of one registered with `router.named()`. */
|
|
45
|
+
export type MiddlewareRef = MiddlewareHandler | string;
|
|
37
46
|
export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD";
|
|
38
47
|
export interface RouteDefinition {
|
|
39
48
|
methods: Method[];
|
|
40
49
|
path: string;
|
|
41
50
|
handler: RouteHandler;
|
|
42
51
|
name?: string;
|
|
43
|
-
middleware:
|
|
52
|
+
middleware: MiddlewareRef[];
|
|
44
53
|
wheres: Record<string, string>;
|
|
45
54
|
/** Host pattern this route is bound to, e.g. ":tenant.example.com". */
|
|
46
55
|
domain?: string;
|
|
@@ -54,9 +63,9 @@ export declare class Route {
|
|
|
54
63
|
/** Alias for name(). */
|
|
55
64
|
as(name: string): this;
|
|
56
65
|
/** Attach middleware that runs only for this route (after group middleware). */
|
|
57
|
-
middleware(mw:
|
|
66
|
+
middleware(mw: MiddlewareRef | MiddlewareRef[]): this;
|
|
58
67
|
/** Alias for middleware(), matching AdonisJS. */
|
|
59
|
-
use(mw:
|
|
68
|
+
use(mw: MiddlewareRef | MiddlewareRef[]): this;
|
|
60
69
|
/** Constrain a route parameter with a regex, source string, or matcher. */
|
|
61
70
|
where(param: string, matcher: Matcher): this;
|
|
62
71
|
/** Bind this route to a host pattern (supports `:subdomain` segments). */
|
|
@@ -67,9 +76,9 @@ export declare class RouteGroup {
|
|
|
67
76
|
private routes;
|
|
68
77
|
constructor(routes: RouteDefinition[]);
|
|
69
78
|
prefix(prefix: string): this;
|
|
70
|
-
middleware(mw:
|
|
79
|
+
middleware(mw: MiddlewareRef | MiddlewareRef[]): this;
|
|
71
80
|
/** Alias for middleware(), matching AdonisJS. */
|
|
72
|
-
use(mw:
|
|
81
|
+
use(mw: MiddlewareRef | MiddlewareRef[]): this;
|
|
73
82
|
/** Constrain a parameter across every route in the group. */
|
|
74
83
|
where(param: string, matcher: Matcher): this;
|
|
75
84
|
as(namePrefix: string): this;
|
|
@@ -91,7 +100,7 @@ export declare class RouteResource {
|
|
|
91
100
|
/** Rename a route parameter, e.g. `.params({ posts: "post" })`. */
|
|
92
101
|
params(map: Record<string, string>): this;
|
|
93
102
|
/** Attach middleware to specific actions (or "*" for all). */
|
|
94
|
-
use(actions: string[] | "*", mw:
|
|
103
|
+
use(actions: string[] | "*", mw: MiddlewareRef | MiddlewareRef[]): this;
|
|
95
104
|
}
|
|
96
105
|
/** Fluent matcher for `on(path)` convenience routes. */
|
|
97
106
|
declare class RouteMatcher {
|
|
@@ -118,6 +127,7 @@ export declare class Router {
|
|
|
118
127
|
private group_prefix;
|
|
119
128
|
private group_mw;
|
|
120
129
|
private globalWheres;
|
|
130
|
+
private namedMiddleware;
|
|
121
131
|
/** Built-in parameter matchers: `router.matchers.number()`. */
|
|
122
132
|
readonly matchers: {
|
|
123
133
|
number: () => RegExp;
|
|
@@ -126,6 +136,13 @@ export declare class Router {
|
|
|
126
136
|
alpha: () => RegExp;
|
|
127
137
|
};
|
|
128
138
|
constructor(container: Container);
|
|
139
|
+
/**
|
|
140
|
+
* Register named middleware, referenceable by name in `.middleware()` /
|
|
141
|
+
* `.use()`: `router.named({ auth, admin })` then `route.use("auth")`.
|
|
142
|
+
*/
|
|
143
|
+
named(map: Record<string, MiddlewareHandler>): this;
|
|
144
|
+
/** Resolve a middleware reference (name or function) to a handler. */
|
|
145
|
+
resolveMiddleware(ref: MiddlewareRef): MiddlewareHandler;
|
|
129
146
|
get(path: string, handler: RouteHandler): Route;
|
|
130
147
|
post(path: string, handler: RouteHandler): Route;
|
|
131
148
|
put(path: string, handler: RouteHandler): Route;
|
|
@@ -152,8 +169,15 @@ export declare class Router {
|
|
|
152
169
|
where(param: string, matcher: Matcher): this;
|
|
153
170
|
/** All registered routes (excluding those trimmed to zero methods). */
|
|
154
171
|
all(): RouteDefinition[];
|
|
155
|
-
/** Generate a URL for a named route, substituting `:params
|
|
156
|
-
url(name: string, params?: Record<string, string | number
|
|
172
|
+
/** Generate a URL for a named route, substituting `:params` and query string. */
|
|
173
|
+
url(name: string, params?: Record<string, string | number>, options?: UrlOptions): string;
|
|
174
|
+
/**
|
|
175
|
+
* A tamper-proof URL for a named route, signed with `config('app.key')`.
|
|
176
|
+
* Verify the incoming request with `router.hasValidSignature()`.
|
|
177
|
+
*/
|
|
178
|
+
signedUrl(name: string, params?: Record<string, string | number>, options?: SignedUrlOptions): Promise<string>;
|
|
179
|
+
/** Whether the current request carries a valid (unexpired) signature. */
|
|
180
|
+
hasValidSignature(): Promise<boolean>;
|
|
157
181
|
/** Turn a route handler into an executable function, resolving controllers. */
|
|
158
182
|
resolve(handler: RouteHandler): HandlerFn;
|
|
159
183
|
}
|
package/dist/core/http/router.js
CHANGED
|
@@ -5,9 +5,22 @@
|
|
|
5
5
|
* Fluent, AdonisJS-inspired API: named routes, per-route and group middleware,
|
|
6
6
|
* prefixes, param constraints, resource routes, and URL generation.
|
|
7
7
|
*/
|
|
8
|
-
import { view } from "../helpers.js";
|
|
9
|
-
import { redirect as makeRedirect } from "../request.js";
|
|
8
|
+
import { view, config } from "../helpers.js";
|
|
9
|
+
import { redirect as makeRedirect, request } from "../request.js";
|
|
10
10
|
import { inertia } from "../inertia.js";
|
|
11
|
+
/** HMAC-SHA256 hex signature (Web Crypto — works on Node and the edge). */
|
|
12
|
+
async function hmac(key, data) {
|
|
13
|
+
const enc = new TextEncoder();
|
|
14
|
+
const cryptoKey = await crypto.subtle.importKey("raw", enc.encode(key), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
|
|
15
|
+
const sig = await crypto.subtle.sign("HMAC", cryptoKey, enc.encode(data));
|
|
16
|
+
return [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
17
|
+
}
|
|
18
|
+
function appKey() {
|
|
19
|
+
const key = config("app.key", "");
|
|
20
|
+
if (!key)
|
|
21
|
+
throw new Error("Signed URLs require config('app.key'). Set APP_KEY.");
|
|
22
|
+
return key;
|
|
23
|
+
}
|
|
11
24
|
function matcherSource(m) {
|
|
12
25
|
if (typeof m === "string")
|
|
13
26
|
return m;
|
|
@@ -206,11 +219,30 @@ export class Router {
|
|
|
206
219
|
group_prefix = "";
|
|
207
220
|
group_mw = [];
|
|
208
221
|
globalWheres = {};
|
|
222
|
+
namedMiddleware = {};
|
|
209
223
|
/** Built-in parameter matchers: `router.matchers.number()`. */
|
|
210
224
|
matchers = matchers;
|
|
211
225
|
constructor(container) {
|
|
212
226
|
this.container = container;
|
|
213
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Register named middleware, referenceable by name in `.middleware()` /
|
|
230
|
+
* `.use()`: `router.named({ auth, admin })` then `route.use("auth")`.
|
|
231
|
+
*/
|
|
232
|
+
named(map) {
|
|
233
|
+
Object.assign(this.namedMiddleware, map);
|
|
234
|
+
return this;
|
|
235
|
+
}
|
|
236
|
+
/** Resolve a middleware reference (name or function) to a handler. */
|
|
237
|
+
resolveMiddleware(ref) {
|
|
238
|
+
if (typeof ref !== "string")
|
|
239
|
+
return ref;
|
|
240
|
+
const mw = this.namedMiddleware[ref];
|
|
241
|
+
if (!mw) {
|
|
242
|
+
throw new Error(`No named middleware [${ref}]. Register it with router.named({ ${ref}: … }).`);
|
|
243
|
+
}
|
|
244
|
+
return mw;
|
|
245
|
+
}
|
|
214
246
|
get(path, handler) {
|
|
215
247
|
return this.add(["GET"], path, handler);
|
|
216
248
|
}
|
|
@@ -305,8 +337,8 @@ export class Router {
|
|
|
305
337
|
}
|
|
306
338
|
return this.routes.filter((r) => r.methods.length > 0);
|
|
307
339
|
}
|
|
308
|
-
/** Generate a URL for a named route, substituting `:params
|
|
309
|
-
url(name, params = {}) {
|
|
340
|
+
/** Generate a URL for a named route, substituting `:params` and query string. */
|
|
341
|
+
url(name, params = {}, options = {}) {
|
|
310
342
|
const def = this.routes.find((r) => r.name === name);
|
|
311
343
|
if (!def)
|
|
312
344
|
throw new Error(`No route named [${name}].`);
|
|
@@ -314,7 +346,44 @@ export class Router {
|
|
|
314
346
|
for (const [k, v] of Object.entries(params)) {
|
|
315
347
|
path = path.replace(new RegExp(`:${k}\\??`), encodeURIComponent(String(v)));
|
|
316
348
|
}
|
|
317
|
-
|
|
349
|
+
path = path.replace(/\/:[^/]+\?/g, "").replace(/:[^/]+/g, "");
|
|
350
|
+
if (options.qs && Object.keys(options.qs).length) {
|
|
351
|
+
const qs = new URLSearchParams(Object.fromEntries(Object.entries(options.qs).map(([k, v]) => [k, String(v)])));
|
|
352
|
+
return `${path}?${qs}`;
|
|
353
|
+
}
|
|
354
|
+
return path;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* A tamper-proof URL for a named route, signed with `config('app.key')`.
|
|
358
|
+
* Verify the incoming request with `router.hasValidSignature()`.
|
|
359
|
+
*/
|
|
360
|
+
async signedUrl(name, params = {}, options = {}) {
|
|
361
|
+
const qs = new URLSearchParams();
|
|
362
|
+
for (const [k, v] of Object.entries(options.qs ?? {}))
|
|
363
|
+
qs.set(k, String(v));
|
|
364
|
+
if (options.expiresIn) {
|
|
365
|
+
qs.set("expires", String(Math.floor(Date.now() / 1000) + options.expiresIn));
|
|
366
|
+
}
|
|
367
|
+
let url = this.url(name, params);
|
|
368
|
+
const query = qs.toString();
|
|
369
|
+
if (query)
|
|
370
|
+
url += `?${query}`;
|
|
371
|
+
const signature = await hmac(appKey(), url);
|
|
372
|
+
return `${url}${query ? "&" : "?"}signature=${signature}`;
|
|
373
|
+
}
|
|
374
|
+
/** Whether the current request carries a valid (unexpired) signature. */
|
|
375
|
+
async hasValidSignature() {
|
|
376
|
+
const url = new URL(request.raw.url);
|
|
377
|
+
const signature = url.searchParams.get("signature");
|
|
378
|
+
if (!signature)
|
|
379
|
+
return false;
|
|
380
|
+
url.searchParams.delete("signature");
|
|
381
|
+
const expires = url.searchParams.get("expires");
|
|
382
|
+
if (expires && Number(expires) < Math.floor(Date.now() / 1000))
|
|
383
|
+
return false;
|
|
384
|
+
const base = url.pathname + (url.search || "");
|
|
385
|
+
const expected = await hmac(appKey(), base);
|
|
386
|
+
return signature.length === expected.length && signature === expected;
|
|
318
387
|
}
|
|
319
388
|
/** Turn a route handler into an executable function, resolving controllers. */
|
|
320
389
|
resolve(handler) {
|
package/dist/core/index.d.ts
CHANGED
|
@@ -4,7 +4,34 @@ export type { Token, Constructor, Factory } from "./container.js";
|
|
|
4
4
|
export { Application } from "./application.js";
|
|
5
5
|
export type { BootOptions } from "./application.js";
|
|
6
6
|
export { Config, env } from "./config.js";
|
|
7
|
-
export { app, config, view, bind, singleton, instance, make, bound, } from "./helpers.js";
|
|
7
|
+
export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
|
|
8
|
+
export { Logger } from "./logger.js";
|
|
9
|
+
export type { LogLevel, LoggerOptions } from "./logger.js";
|
|
10
|
+
export { Events } from "./events.js";
|
|
11
|
+
export type { Listener } from "./events.js";
|
|
12
|
+
export { Cache, MemoryStore } from "./cache.js";
|
|
13
|
+
export type { CacheStore } from "./cache.js";
|
|
14
|
+
export { serveStatic } from "./static.js";
|
|
15
|
+
export type { StaticOptions } from "./static.js";
|
|
16
|
+
export { dump, dd } from "./debug.js";
|
|
17
|
+
export { hash, encryption } from "./crypto.js";
|
|
18
|
+
export { rateLimiter } from "./rate-limit.js";
|
|
19
|
+
export type { RateLimiterOptions } from "./rate-limit.js";
|
|
20
|
+
export { db, setConnection, QueryBuilder } from "./database.js";
|
|
21
|
+
export type { Connection, WriteResult, Row, Dialect, Operator } from "./database.js";
|
|
22
|
+
export { Model } from "./model.js";
|
|
23
|
+
export type { CastType, Casts } from "./casts.js";
|
|
24
|
+
export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
|
|
25
|
+
export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
|
|
26
|
+
export type { Definition } from "./factory.js";
|
|
27
|
+
export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
|
|
28
|
+
export type { Message, Transport, MailerOptions, FetchTransportOptions } from "./mail.js";
|
|
29
|
+
export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
|
|
30
|
+
export type { Dispatchable, JobOptions, QueueDriver, Drainable, QueuedJob } from "./queue.js";
|
|
31
|
+
export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
|
|
32
|
+
export type { Notifiable, MailContent, Channel } from "./notification.js";
|
|
33
|
+
export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
|
|
34
|
+
export type { Migration } from "./migrations.js";
|
|
8
35
|
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
9
36
|
export type { ConfigData } from "./config.js";
|
|
10
37
|
export { View } from "./view.js";
|
|
@@ -12,10 +39,14 @@ export type { Renderable, ViewConfig } from "./view.js";
|
|
|
12
39
|
export { ServiceProvider } from "./provider.js";
|
|
13
40
|
export type { ProviderClass } from "./provider.js";
|
|
14
41
|
export { Router, Route, RouteGroup, RouteResource, matchers } from "./http/router.js";
|
|
15
|
-
export type { Ctx, RouteHandler, RouteDefinition, Method, Matcher } from "./http/router.js";
|
|
42
|
+
export type { Ctx, RouteHandler, RouteDefinition, Method, Matcher, MiddlewareRef, UrlOptions, SignedUrlOptions, } from "./http/router.js";
|
|
16
43
|
export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
|
|
17
44
|
export type { InertiaPage, InertiaOptions } from "./inertia.js";
|
|
18
45
|
export { HttpKernel } from "./http/kernel.js";
|
|
19
46
|
export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
|
|
20
47
|
export { validate } from "./validation.js";
|
|
21
48
|
export type { Schema } from "./validation.js";
|
|
49
|
+
export { Session, session, sessionMiddleware } from "./session.js";
|
|
50
|
+
export type { SessionOptions } from "./session.js";
|
|
51
|
+
export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
|
|
52
|
+
export type { UserProvider } from "./auth.js";
|
package/dist/core/index.js
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
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, } from "./helpers.js";
|
|
5
|
+
export { app, config, view, bind, singleton, instance, make, bound, events, emit, listen, cache, logger, } from "./helpers.js";
|
|
6
|
+
export { Logger } from "./logger.js";
|
|
7
|
+
export { Events } from "./events.js";
|
|
8
|
+
export { Cache, MemoryStore } from "./cache.js";
|
|
9
|
+
export { serveStatic } from "./static.js";
|
|
10
|
+
export { dump, dd } from "./debug.js";
|
|
11
|
+
export { hash, encryption } from "./crypto.js";
|
|
12
|
+
export { rateLimiter } from "./rate-limit.js";
|
|
13
|
+
export { db, setConnection, QueryBuilder } from "./database.js";
|
|
14
|
+
export { Model } from "./model.js";
|
|
15
|
+
export { Relation, HasOne, HasMany, BelongsTo, BelongsToMany } from "./relations.js";
|
|
16
|
+
export { Faker, Factory as ModelFactory, factory, Seeder, seed } from "./factory.js";
|
|
17
|
+
export { Mailer, PendingMail, ArrayTransport, LogTransport, fetchTransport, mail, setMailer, getMailer, } from "./mail.js";
|
|
18
|
+
export { Job, Queue, SyncDriver, MemoryDriver, dispatch, work, setQueue, getQueue, } from "./queue.js";
|
|
19
|
+
export { Notification, Notifier, MailChannel, DatabaseChannel, ArrayChannel, routeFor, notify, setNotifier, getNotifier, } from "./notification.js";
|
|
20
|
+
export { SchemaBuilder, Migrator, TableBuilder, Column } from "./migrations.js";
|
|
6
21
|
export { ctx, json, text, html, redirect, param, query, header, body, request, response, } from "./request.js";
|
|
7
22
|
export { View } from "./view.js";
|
|
8
23
|
export { ServiceProvider } from "./provider.js";
|
|
@@ -11,3 +26,5 @@ export { Inertia, inertia, inertiaPageAttr } from "./inertia.js";
|
|
|
11
26
|
export { HttpKernel } from "./http/kernel.js";
|
|
12
27
|
export { HttpException, NotFoundException, UnauthorizedException, ForbiddenException, ValidationException, STATUS_TEXT, } from "./exceptions.js";
|
|
13
28
|
export { validate } from "./validation.js";
|
|
29
|
+
export { Session, session, sessionMiddleware } from "./session.js";
|
|
30
|
+
export { Auth, auth, authGuard, setUserProvider } from "./auth.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small leveled logger. Structured JSON by default (one line per event, ready
|
|
3
|
+
* for log aggregators); pretty single-line output in debug. Bound as a
|
|
4
|
+
* singleton on the application — reach it with the global `logger()` helper.
|
|
5
|
+
*
|
|
6
|
+
* logger().info("user registered", { userId: user.id });
|
|
7
|
+
* logger().error("payment failed", { orderId, error });
|
|
8
|
+
*/
|
|
9
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
10
|
+
export interface LoggerOptions {
|
|
11
|
+
/** Minimum level to emit. Default: "info". */
|
|
12
|
+
level?: LogLevel;
|
|
13
|
+
/** Pretty single-line output instead of JSON. Default: false. */
|
|
14
|
+
pretty?: boolean;
|
|
15
|
+
/** Fields merged into every log line. */
|
|
16
|
+
bindings?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
export declare class Logger {
|
|
19
|
+
private options;
|
|
20
|
+
private threshold;
|
|
21
|
+
constructor(options?: LoggerOptions);
|
|
22
|
+
private write;
|
|
23
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
24
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
25
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
26
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
27
|
+
/** A child logger with additional bound fields (e.g. a request id). */
|
|
28
|
+
child(bindings: Record<string, unknown>): Logger;
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small leveled logger. Structured JSON by default (one line per event, ready
|
|
3
|
+
* for log aggregators); pretty single-line output in debug. Bound as a
|
|
4
|
+
* singleton on the application — reach it with the global `logger()` helper.
|
|
5
|
+
*
|
|
6
|
+
* logger().info("user registered", { userId: user.id });
|
|
7
|
+
* logger().error("payment failed", { orderId, error });
|
|
8
|
+
*/
|
|
9
|
+
const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
10
|
+
export class Logger {
|
|
11
|
+
options;
|
|
12
|
+
threshold;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
this.threshold = LEVELS[options.level ?? "info"];
|
|
16
|
+
}
|
|
17
|
+
write(level, message, context) {
|
|
18
|
+
if (LEVELS[level] < this.threshold)
|
|
19
|
+
return;
|
|
20
|
+
const time = new Date().toISOString();
|
|
21
|
+
if (this.options.pretty) {
|
|
22
|
+
const extra = context ? " " + JSON.stringify(context) : "";
|
|
23
|
+
const fn = level === "warn" ? console.warn : level === "error" ? console.error : console.log;
|
|
24
|
+
fn(`[${time}] ${level.toUpperCase().padEnd(5)} ${message}${extra}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(JSON.stringify({ level, time, msg: message, ...this.options.bindings, ...context }));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
debug(message, context) {
|
|
31
|
+
this.write("debug", message, context);
|
|
32
|
+
}
|
|
33
|
+
info(message, context) {
|
|
34
|
+
this.write("info", message, context);
|
|
35
|
+
}
|
|
36
|
+
warn(message, context) {
|
|
37
|
+
this.write("warn", message, context);
|
|
38
|
+
}
|
|
39
|
+
error(message, context) {
|
|
40
|
+
this.write("error", message, context);
|
|
41
|
+
}
|
|
42
|
+
/** A child logger with additional bound fields (e.g. a request id). */
|
|
43
|
+
child(bindings) {
|
|
44
|
+
return new Logger({
|
|
45
|
+
...this.options,
|
|
46
|
+
bindings: { ...this.options.bindings, ...bindings },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, edge-safe mailer. You compose a message with a fluent builder and
|
|
3
|
+
* send it through a pluggable `Transport` — the same shape as the database
|
|
4
|
+
* layer (`setMailer` / `mail()` mirror `setConnection` / `db()`). The core
|
|
5
|
+
* imports no SDK: the built-in transports use `fetch`, `console`, or memory, so
|
|
6
|
+
* it runs on Node and the edge.
|
|
7
|
+
*
|
|
8
|
+
* setMailer(fetchTransport({ url, headers }), { from: "hi@app.com" });
|
|
9
|
+
*
|
|
10
|
+
* await mail()
|
|
11
|
+
* .to("ada@example.com")
|
|
12
|
+
* .subject("Welcome")
|
|
13
|
+
* .html("<h1>Hi</h1>")
|
|
14
|
+
* .send();
|
|
15
|
+
*
|
|
16
|
+
* In tests, register an `ArrayTransport` and assert on `transport.sent`.
|
|
17
|
+
*/
|
|
18
|
+
/** A normalized, ready-to-send message. */
|
|
19
|
+
export interface Message {
|
|
20
|
+
to: string[];
|
|
21
|
+
from?: string;
|
|
22
|
+
cc?: string[];
|
|
23
|
+
bcc?: string[];
|
|
24
|
+
replyTo?: string;
|
|
25
|
+
subject: string;
|
|
26
|
+
text?: string;
|
|
27
|
+
html?: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
}
|
|
30
|
+
/** The bridge to your email provider. */
|
|
31
|
+
export interface Transport {
|
|
32
|
+
send(message: Message): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
export interface MailerOptions {
|
|
35
|
+
/** Default `from` address for messages that don't set one. */
|
|
36
|
+
from?: string;
|
|
37
|
+
}
|
|
38
|
+
/** A fluent, immutable-ish builder — chain setters, then `send()`. */
|
|
39
|
+
export declare class PendingMail {
|
|
40
|
+
private mailer;
|
|
41
|
+
private message;
|
|
42
|
+
constructor(mailer: Mailer);
|
|
43
|
+
to(...addresses: string[]): this;
|
|
44
|
+
from(address: string): this;
|
|
45
|
+
cc(...addresses: string[]): this;
|
|
46
|
+
bcc(...addresses: string[]): this;
|
|
47
|
+
replyTo(address: string): this;
|
|
48
|
+
subject(subject: string): this;
|
|
49
|
+
text(text: string): this;
|
|
50
|
+
html(html: string): this;
|
|
51
|
+
header(name: string, value: string): this;
|
|
52
|
+
/** Seed several fields at once (merges into what's been chained). */
|
|
53
|
+
fill(partial: Partial<{
|
|
54
|
+
to: string | string[];
|
|
55
|
+
cc: string | string[];
|
|
56
|
+
bcc: string | string[];
|
|
57
|
+
} & Omit<Message, "to" | "cc" | "bcc">>): this;
|
|
58
|
+
/** Freeze and hand the message to the mailer. */
|
|
59
|
+
send(): Promise<Message>;
|
|
60
|
+
}
|
|
61
|
+
export declare class Mailer {
|
|
62
|
+
private transport;
|
|
63
|
+
private options;
|
|
64
|
+
constructor(transport: Transport, options?: MailerOptions);
|
|
65
|
+
/** Start composing a message. */
|
|
66
|
+
message(): PendingMail;
|
|
67
|
+
/** Validate, apply defaults, and dispatch through the transport. */
|
|
68
|
+
send(message: Message): Promise<Message>;
|
|
69
|
+
}
|
|
70
|
+
/** Collects sent messages in memory — the default, and ideal for tests. */
|
|
71
|
+
export declare class ArrayTransport implements Transport {
|
|
72
|
+
readonly sent: Message[];
|
|
73
|
+
send(message: Message): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
/** Logs each message instead of delivering it — handy in local development. */
|
|
76
|
+
export declare class LogTransport implements Transport {
|
|
77
|
+
send(message: Message): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
export interface FetchTransportOptions {
|
|
80
|
+
/** Provider endpoint to POST to. */
|
|
81
|
+
url: string;
|
|
82
|
+
/** Extra headers (e.g. `Authorization`). `Content-Type: application/json` is added. */
|
|
83
|
+
headers?: Record<string, string>;
|
|
84
|
+
/** Map a message to the provider's request body. Defaults to the message itself. */
|
|
85
|
+
body?: (message: Message) => unknown;
|
|
86
|
+
}
|
|
87
|
+
/** A generic HTTP transport for provider APIs (Resend, Postmark, …), via `fetch`. */
|
|
88
|
+
export declare function fetchTransport(options: FetchTransportOptions): Transport;
|
|
89
|
+
/** Register the default mailer used by `mail()`. */
|
|
90
|
+
export declare function setMailer(transport: Transport, options?: MailerOptions): Mailer;
|
|
91
|
+
/** The default mailer instance. */
|
|
92
|
+
export declare function getMailer(): Mailer;
|
|
93
|
+
/** Start a message on the default mailer: `mail().to(…).subject(…).send()`. */
|
|
94
|
+
export declare function mail(): PendingMail;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small, edge-safe mailer. You compose a message with a fluent builder and
|
|
3
|
+
* send it through a pluggable `Transport` — the same shape as the database
|
|
4
|
+
* layer (`setMailer` / `mail()` mirror `setConnection` / `db()`). The core
|
|
5
|
+
* imports no SDK: the built-in transports use `fetch`, `console`, or memory, so
|
|
6
|
+
* it runs on Node and the edge.
|
|
7
|
+
*
|
|
8
|
+
* setMailer(fetchTransport({ url, headers }), { from: "hi@app.com" });
|
|
9
|
+
*
|
|
10
|
+
* await mail()
|
|
11
|
+
* .to("ada@example.com")
|
|
12
|
+
* .subject("Welcome")
|
|
13
|
+
* .html("<h1>Hi</h1>")
|
|
14
|
+
* .send();
|
|
15
|
+
*
|
|
16
|
+
* In tests, register an `ArrayTransport` and assert on `transport.sent`.
|
|
17
|
+
*/
|
|
18
|
+
import { logger } from "./helpers.js";
|
|
19
|
+
function list(value) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
return [];
|
|
22
|
+
return Array.isArray(value) ? value : [value];
|
|
23
|
+
}
|
|
24
|
+
/** A fluent, immutable-ish builder — chain setters, then `send()`. */
|
|
25
|
+
export class PendingMail {
|
|
26
|
+
mailer;
|
|
27
|
+
message = { to: [], subject: "" };
|
|
28
|
+
constructor(mailer) {
|
|
29
|
+
this.mailer = mailer;
|
|
30
|
+
}
|
|
31
|
+
to(...addresses) {
|
|
32
|
+
this.message.to.push(...addresses);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
from(address) {
|
|
36
|
+
this.message.from = address;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
cc(...addresses) {
|
|
40
|
+
(this.message.cc ??= []).push(...addresses);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
bcc(...addresses) {
|
|
44
|
+
(this.message.bcc ??= []).push(...addresses);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
replyTo(address) {
|
|
48
|
+
this.message.replyTo = address;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
subject(subject) {
|
|
52
|
+
this.message.subject = subject;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
text(text) {
|
|
56
|
+
this.message.text = text;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
html(html) {
|
|
60
|
+
this.message.html = html;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
header(name, value) {
|
|
64
|
+
(this.message.headers ??= {})[name] = value;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
/** Seed several fields at once (merges into what's been chained). */
|
|
68
|
+
fill(partial) {
|
|
69
|
+
const { to, cc, bcc, ...rest } = partial;
|
|
70
|
+
if (to)
|
|
71
|
+
this.message.to.push(...list(to));
|
|
72
|
+
if (cc)
|
|
73
|
+
(this.message.cc ??= []).push(...list(cc));
|
|
74
|
+
if (bcc)
|
|
75
|
+
(this.message.bcc ??= []).push(...list(bcc));
|
|
76
|
+
Object.assign(this.message, rest);
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/** Freeze and hand the message to the mailer. */
|
|
80
|
+
async send() {
|
|
81
|
+
return this.mailer.send(this.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export class Mailer {
|
|
85
|
+
transport;
|
|
86
|
+
options;
|
|
87
|
+
constructor(transport, options = {}) {
|
|
88
|
+
this.transport = transport;
|
|
89
|
+
this.options = options;
|
|
90
|
+
}
|
|
91
|
+
/** Start composing a message. */
|
|
92
|
+
message() {
|
|
93
|
+
return new PendingMail(this);
|
|
94
|
+
}
|
|
95
|
+
/** Validate, apply defaults, and dispatch through the transport. */
|
|
96
|
+
async send(message) {
|
|
97
|
+
const final = { ...message, from: message.from ?? this.options.from };
|
|
98
|
+
if (!final.to.length)
|
|
99
|
+
throw new Error("Mail: at least one recipient (to) is required.");
|
|
100
|
+
if (!final.subject)
|
|
101
|
+
throw new Error("Mail: a subject is required.");
|
|
102
|
+
if (!final.text && !final.html)
|
|
103
|
+
throw new Error("Mail: a text or html body is required.");
|
|
104
|
+
if (!final.from)
|
|
105
|
+
throw new Error("Mail: a from address is required (set one or a default).");
|
|
106
|
+
await this.transport.send(final);
|
|
107
|
+
return final;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/* ------------------------------- transports ------------------------------- */
|
|
111
|
+
/** Collects sent messages in memory — the default, and ideal for tests. */
|
|
112
|
+
export class ArrayTransport {
|
|
113
|
+
sent = [];
|
|
114
|
+
async send(message) {
|
|
115
|
+
this.sent.push(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/** Logs each message instead of delivering it — handy in local development. */
|
|
119
|
+
export class LogTransport {
|
|
120
|
+
async send(message) {
|
|
121
|
+
logger().info("mail sent", {
|
|
122
|
+
to: message.to,
|
|
123
|
+
from: message.from,
|
|
124
|
+
subject: message.subject,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** A generic HTTP transport for provider APIs (Resend, Postmark, …), via `fetch`. */
|
|
129
|
+
export function fetchTransport(options) {
|
|
130
|
+
return {
|
|
131
|
+
async send(message) {
|
|
132
|
+
const res = await fetch(options.url, {
|
|
133
|
+
method: "POST",
|
|
134
|
+
headers: { "Content-Type": "application/json", ...options.headers },
|
|
135
|
+
body: JSON.stringify(options.body ? options.body(message) : message),
|
|
136
|
+
});
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
throw new Error(`Mail: transport responded ${res.status} ${res.statusText}`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/* --------------------------------- global --------------------------------- */
|
|
144
|
+
let mailer = new Mailer(new ArrayTransport());
|
|
145
|
+
/** Register the default mailer used by `mail()`. */
|
|
146
|
+
export function setMailer(transport, options = {}) {
|
|
147
|
+
mailer = new Mailer(transport, options);
|
|
148
|
+
return mailer;
|
|
149
|
+
}
|
|
150
|
+
/** The default mailer instance. */
|
|
151
|
+
export function getMailer() {
|
|
152
|
+
return mailer;
|
|
153
|
+
}
|
|
154
|
+
/** Start a message on the default mailer: `mail().to(…).subject(…).send()`. */
|
|
155
|
+
export function mail() {
|
|
156
|
+
return mailer.message();
|
|
157
|
+
}
|