@raiyyan/brisk 0.0.0-stage → 0.2.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/AGENTS.md +535 -0
- package/LICENSE +21 -0
- package/README.md +1745 -2
- package/dist/bin/brisk.d.ts +3 -0
- package/dist/bin/brisk.d.ts.map +1 -0
- package/dist/bin/brisk.js +13 -0
- package/dist/bin/brisk.js.map +1 -0
- package/dist/src/cli/agents.d.ts +7 -0
- package/dist/src/cli/agents.d.ts.map +1 -0
- package/dist/src/cli/agents.js +55 -0
- package/dist/src/cli/agents.js.map +1 -0
- package/dist/src/cli/cli.d.ts +5 -0
- package/dist/src/cli/cli.d.ts.map +1 -0
- package/dist/src/cli/cli.js +113 -0
- package/dist/src/cli/cli.js.map +1 -0
- package/dist/src/cli/client.d.ts +12 -0
- package/dist/src/cli/client.d.ts.map +1 -0
- package/dist/src/cli/client.js +76 -0
- package/dist/src/cli/client.js.map +1 -0
- package/dist/src/cli/init.d.ts +16 -0
- package/dist/src/cli/init.d.ts.map +1 -0
- package/dist/src/cli/init.js +149 -0
- package/dist/src/cli/init.js.map +1 -0
- package/dist/src/cli/names.d.ts +14 -0
- package/dist/src/cli/names.d.ts.map +1 -0
- package/dist/src/cli/names.js +25 -0
- package/dist/src/cli/names.js.map +1 -0
- package/dist/src/cli/render.d.ts +30 -0
- package/dist/src/cli/render.d.ts.map +1 -0
- package/dist/src/cli/render.js +101 -0
- package/dist/src/cli/render.js.map +1 -0
- package/dist/src/cli/system.d.ts +35 -0
- package/dist/src/cli/system.d.ts.map +1 -0
- package/dist/src/cli/system.js +81 -0
- package/dist/src/cli/system.js.map +1 -0
- package/dist/src/client/client.d.ts +64 -0
- package/dist/src/client/client.d.ts.map +1 -0
- package/dist/src/client/client.js +170 -0
- package/dist/src/client/client.js.map +1 -0
- package/dist/src/client/index.d.ts +8 -0
- package/dist/src/client/index.d.ts.map +1 -0
- package/dist/src/client/index.js +7 -0
- package/dist/src/client/index.js.map +1 -0
- package/dist/src/config/env.d.ts +28 -0
- package/dist/src/config/env.d.ts.map +1 -0
- package/dist/src/config/env.js +35 -0
- package/dist/src/config/env.js.map +1 -0
- package/dist/src/config/logger.d.ts +32 -0
- package/dist/src/config/logger.d.ts.map +1 -0
- package/dist/src/config/logger.js +31 -0
- package/dist/src/config/logger.js.map +1 -0
- package/dist/src/di/container.d.ts +194 -0
- package/dist/src/di/container.d.ts.map +1 -0
- package/dist/src/di/container.js +225 -0
- package/dist/src/di/container.js.map +1 -0
- package/dist/src/di/index.d.ts +8 -0
- package/dist/src/di/index.d.ts.map +1 -0
- package/dist/src/di/index.js +8 -0
- package/dist/src/di/index.js.map +1 -0
- package/dist/src/errors/http-errors.d.ts +295 -0
- package/dist/src/errors/http-errors.d.ts.map +1 -0
- package/dist/src/errors/http-errors.js +311 -0
- package/dist/src/errors/http-errors.js.map +1 -0
- package/dist/src/errors/index.d.ts +8 -0
- package/dist/src/errors/index.d.ts.map +1 -0
- package/dist/src/errors/index.js +8 -0
- package/dist/src/errors/index.js.map +1 -0
- package/dist/src/http/app.d.ts +229 -0
- package/dist/src/http/app.d.ts.map +1 -0
- package/dist/src/http/app.js +45 -0
- package/dist/src/http/app.js.map +1 -0
- package/dist/src/http/context.d.ts +66 -0
- package/dist/src/http/context.d.ts.map +1 -0
- package/dist/src/http/context.js +96 -0
- package/dist/src/http/context.js.map +1 -0
- package/dist/src/http/controller.d.ts +195 -0
- package/dist/src/http/controller.d.ts.map +1 -0
- package/dist/src/http/controller.js +290 -0
- package/dist/src/http/controller.js.map +1 -0
- package/dist/src/http/error-handler.d.ts +86 -0
- package/dist/src/http/error-handler.d.ts.map +1 -0
- package/dist/src/http/error-handler.js +147 -0
- package/dist/src/http/error-handler.js.map +1 -0
- package/dist/src/http/instance.d.ts +14 -0
- package/dist/src/http/instance.d.ts.map +1 -0
- package/dist/src/http/instance.js +377 -0
- package/dist/src/http/instance.js.map +1 -0
- package/dist/src/http/middleware.d.ts +141 -0
- package/dist/src/http/middleware.d.ts.map +1 -0
- package/dist/src/http/middleware.js +160 -0
- package/dist/src/http/middleware.js.map +1 -0
- package/dist/src/http/openapi.d.ts +34 -0
- package/dist/src/http/openapi.d.ts.map +1 -0
- package/dist/src/http/openapi.js +151 -0
- package/dist/src/http/openapi.js.map +1 -0
- package/dist/src/http/path-params.d.ts +24 -0
- package/dist/src/http/path-params.d.ts.map +1 -0
- package/dist/src/http/path-params.js +2 -0
- package/dist/src/http/path-params.js.map +1 -0
- package/dist/src/http/routes.d.ts +141 -0
- package/dist/src/http/routes.d.ts.map +1 -0
- package/dist/src/http/routes.js +2 -0
- package/dist/src/http/routes.js.map +1 -0
- package/dist/src/http/server.d.ts +23 -0
- package/dist/src/http/server.d.ts.map +1 -0
- package/dist/src/http/server.js +22 -0
- package/dist/src/http/server.js.map +1 -0
- package/dist/src/http/validation.d.ts +16 -0
- package/dist/src/http/validation.d.ts.map +1 -0
- package/dist/src/http/validation.js +51 -0
- package/dist/src/http/validation.js.map +1 -0
- package/dist/src/index.d.ts +22 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +19 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/testing/index.d.ts +7 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +7 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/test-client.d.ts +67 -0
- package/dist/src/testing/test-client.d.ts.map +1 -0
- package/dist/src/testing/test-client.js +78 -0
- package/dist/src/testing/test-client.js.map +1 -0
- package/package.json +89 -3
- package/starters/minimal/.env.example +3 -0
- package/starters/minimal/.oxfmtrc.json +5 -0
- package/starters/minimal/.oxlintrc.json +14 -0
- package/starters/minimal/AGENTS.md +23 -0
- package/starters/minimal/CLAUDE.md +2 -0
- package/starters/minimal/README.md +53 -0
- package/starters/minimal/_gitignore +3 -0
- package/starters/minimal/package.json +29 -0
- package/starters/minimal/src/app.ts +14 -0
- package/starters/minimal/src/env.ts +7 -0
- package/starters/minimal/src/features/index.ts +8 -0
- package/starters/minimal/src/features/notes/notes.controller.ts +38 -0
- package/starters/minimal/src/features/notes/notes.repository.ts +36 -0
- package/starters/minimal/src/features/notes/notes.schema.ts +24 -0
- package/starters/minimal/src/features/notes/notes.service.ts +33 -0
- package/starters/minimal/src/main.ts +4 -0
- package/starters/minimal/src/schemas.ts +26 -0
- package/starters/minimal/templates/feature/src/features/index.ts.controllers.insert +1 -0
- package/starters/minimal/templates/feature/src/features/index.ts.imports.insert +1 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.controller.ts +38 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.repository.ts +36 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.schema.ts +24 -0
- package/starters/minimal/templates/feature/src/features/{{kebab}}/{{kebab}}.service.ts +33 -0
- package/starters/minimal/templates/feature/test/{{kebab}}.service.test.ts +20 -0
- package/starters/minimal/templates/feature/test/{{kebab}}.test.ts +35 -0
- package/starters/minimal/templates/middleware/src/middlewares/{{kebab}}.ts +12 -0
- package/starters/minimal/test/notes.service.test.ts +20 -0
- package/starters/minimal/test/notes.test.ts +35 -0
- package/starters/minimal/tsconfig.json +17 -0
- package/starters/minimal/vitest.config.ts +9 -0
- package/starters/sqlite-auth/.dockerignore +8 -0
- package/starters/sqlite-auth/.env.example +9 -0
- package/starters/sqlite-auth/.oxfmtrc.json +5 -0
- package/starters/sqlite-auth/.oxlintrc.json +14 -0
- package/starters/sqlite-auth/AGENTS.md +23 -0
- package/starters/sqlite-auth/CLAUDE.md +2 -0
- package/starters/sqlite-auth/Dockerfile +23 -0
- package/starters/sqlite-auth/README.md +93 -0
- package/starters/sqlite-auth/_gitignore +5 -0
- package/starters/sqlite-auth/compose.yaml +14 -0
- package/starters/sqlite-auth/package.json +35 -0
- package/starters/sqlite-auth/public/.gitkeep +0 -0
- package/starters/sqlite-auth/src/app.ts +39 -0
- package/starters/sqlite-auth/src/auth/tokens.ts +41 -0
- package/starters/sqlite-auth/src/database/clock.ts +4 -0
- package/starters/sqlite-auth/src/database/database.ts +29 -0
- package/starters/sqlite-auth/src/database/errors.ts +11 -0
- package/starters/sqlite-auth/src/database/migrations.ts +29 -0
- package/starters/sqlite-auth/src/env.ts +17 -0
- package/starters/sqlite-auth/src/features/auth/auth.controller.ts +28 -0
- package/starters/sqlite-auth/src/features/index.ts +12 -0
- package/starters/sqlite-auth/src/features/notes/notes.controller.ts +39 -0
- package/starters/sqlite-auth/src/features/notes/notes.repository.ts +47 -0
- package/starters/sqlite-auth/src/features/notes/notes.schema.ts +24 -0
- package/starters/sqlite-auth/src/features/notes/notes.service.ts +33 -0
- package/starters/sqlite-auth/src/features/users/admin-seed.ts +21 -0
- package/starters/sqlite-auth/src/features/users/users.controller.ts +42 -0
- package/starters/sqlite-auth/src/features/users/users.errors.ts +13 -0
- package/starters/sqlite-auth/src/features/users/users.repository.ts +77 -0
- package/starters/sqlite-auth/src/features/users/users.schema.ts +35 -0
- package/starters/sqlite-auth/src/features/users/users.service.ts +80 -0
- package/starters/sqlite-auth/src/main.ts +4 -0
- package/starters/sqlite-auth/src/middlewares/authenticate.ts +29 -0
- package/starters/sqlite-auth/src/schemas.ts +26 -0
- package/starters/sqlite-auth/templates/feature/src/database/migrations.ts.migrations.insert +5 -0
- package/starters/sqlite-auth/templates/feature/src/features/index.ts.controllers.insert +1 -0
- package/starters/sqlite-auth/templates/feature/src/features/index.ts.imports.insert +1 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.controller.ts +39 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.repository.ts +47 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.schema.ts +24 -0
- package/starters/sqlite-auth/templates/feature/src/features/{{kebab}}/{{kebab}}.service.ts +33 -0
- package/starters/sqlite-auth/templates/feature/test/{{kebab}}.service.test.ts +20 -0
- package/starters/sqlite-auth/templates/feature/test/{{kebab}}.test.ts +41 -0
- package/starters/sqlite-auth/templates/middleware/src/middlewares/{{kebab}}.ts +12 -0
- package/starters/sqlite-auth/test/auth.test.ts +18 -0
- package/starters/sqlite-auth/test/helpers.ts +13 -0
- package/starters/sqlite-auth/test/notes.service.test.ts +20 -0
- package/starters/sqlite-auth/test/notes.test.ts +41 -0
- package/starters/sqlite-auth/test/users.service.test.ts +25 -0
- package/starters/sqlite-auth/test/users.test.ts +164 -0
- package/starters/sqlite-auth/tsconfig.json +17 -0
- package/starters/sqlite-auth/vitest.config.ts +15 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { Express, RequestHandler } from "express";
|
|
2
|
+
import type { Logger } from "../config/logger.ts";
|
|
3
|
+
import type { Container, ProviderEntry } from "../di/container.ts";
|
|
4
|
+
import type { RequestContext } from "./context.ts";
|
|
5
|
+
import type { Controller } from "./controller.ts";
|
|
6
|
+
import type { ErrorFormatter, ErrorMapper } from "./error-handler.ts";
|
|
7
|
+
import type { Middleware } from "./middleware.ts";
|
|
8
|
+
import type { OpenApiOptions } from "./openapi.ts";
|
|
9
|
+
import type { Manifest } from "./routes.ts";
|
|
10
|
+
import type { NodeServer } from "./server.ts";
|
|
11
|
+
/** Controllers by name. The names are the typed client's top level: `{ users }` gives `api.users.get(...)`. */
|
|
12
|
+
export type ControllerMap = Readonly<Record<string, Controller>>;
|
|
13
|
+
/**
|
|
14
|
+
* Per-request hooks for metrics, tracing and error reporting. Logic that can reject a request is a middleware
|
|
15
|
+
* in `use`; startup and shutdown work belongs in a provider class (`onInit`, `onReady`, `onDrain`, `onShutdown`).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* hooks: {
|
|
20
|
+
* onResponse: (ctx, { status, durationMs }) => metrics.observe(ctx.req.route?.path, status, durationMs),
|
|
21
|
+
* onError: (error) => {
|
|
22
|
+
* if (error instanceof HttpError && error.status < 500) return;
|
|
23
|
+
* Sentry.captureException(error);
|
|
24
|
+
* },
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export interface AppHooks {
|
|
29
|
+
/**
|
|
30
|
+
* Once per request, when its connection is done with it. `aborted` is `true` when the client left before the
|
|
31
|
+
* response was fully sent; `status` is then what the server had set so far.
|
|
32
|
+
*/
|
|
33
|
+
onResponse?: (ctx: RequestContext, info: {
|
|
34
|
+
status: number;
|
|
35
|
+
durationMs: number;
|
|
36
|
+
aborted: boolean;
|
|
37
|
+
}) => void;
|
|
38
|
+
/** Every error that reaches the error handler, client errors included, before the response is sent. */
|
|
39
|
+
onError?: (error: unknown, ctx: RequestContext) => void;
|
|
40
|
+
}
|
|
41
|
+
/** How errors become responses. */
|
|
42
|
+
export interface ErrorOptions {
|
|
43
|
+
/**
|
|
44
|
+
* Turn library errors into HTTP errors in one place: `[(e) => isUniqueViolation(e) ? new ConflictError() : undefined]`.
|
|
45
|
+
* The first mapper that returns an error wins.
|
|
46
|
+
*/
|
|
47
|
+
mappers?: readonly ErrorMapper[];
|
|
48
|
+
/** Replace the error body, such as with RFC 9457 problem details. Default `{ error: { code, message, details, requestId } }`. */
|
|
49
|
+
format?: ErrorFormatter;
|
|
50
|
+
/** Send the real message of unexpected errors to clients. Development only. Default `false`. */
|
|
51
|
+
exposeInternal?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** HTTP-level settings. Every default suits an API; change them only for a reason. */
|
|
54
|
+
export interface HttpOptions {
|
|
55
|
+
/** Size limit for JSON bodies (`application/json` and `application/*+json`). Default `1mb`. */
|
|
56
|
+
bodyLimit?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Express app settings, as `app.set(name, value)` takes them, such as `{ "trust proxy": 1 }` behind one proxy
|
|
59
|
+
* you control (never blindly, or clients can spoof `req.ip` with `X-Forwarded-For`), `{ etag: false }`, or
|
|
60
|
+
* `{ "query parser": "extended" }` for nested query objects (brisk's default is flat).
|
|
61
|
+
*/
|
|
62
|
+
settings?: Readonly<Record<string, unknown>>;
|
|
63
|
+
/**
|
|
64
|
+
* Security headers on every response. `true` (default) sends strict API defaults; an object overrides single
|
|
65
|
+
* headers (`false` removes one), such as a relaxed `content-security-policy` for HTML pages.
|
|
66
|
+
*/
|
|
67
|
+
securityHeaders?: boolean | Readonly<Record<string, string | false>>;
|
|
68
|
+
/** Health check path, or `false` for none. Default `/health`. */
|
|
69
|
+
healthPath?: string | false;
|
|
70
|
+
/**
|
|
71
|
+
* How long shutdown waits for in-flight requests before destroying the remaining sockets. Default 10 000 ms.
|
|
72
|
+
* A process the app manages also exits with code 1 if shutdown, hooks included, is not done 10 s later.
|
|
73
|
+
*/
|
|
74
|
+
shutdownTimeoutMs?: number;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Options for {@link defineApp}. All are optional: an app with no controllers serves only its health check, or
|
|
78
|
+
* nothing at all when started with `app.start()`.
|
|
79
|
+
*/
|
|
80
|
+
export interface AppOptions<C extends ControllerMap = ControllerMap> {
|
|
81
|
+
/** Path every controller is mounted under, such as `/api`. Default `/`. */
|
|
82
|
+
prefix?: string;
|
|
83
|
+
/** Controllers by name: `{ users: usersController }`. */
|
|
84
|
+
controllers?: C;
|
|
85
|
+
/**
|
|
86
|
+
* Values for tokens (`provide(Clock, fakeClock)`), and classes to create at startup even when nothing injects
|
|
87
|
+
* them (a WebSocket gateway, a queue consumer) so their lifecycle hooks run.
|
|
88
|
+
*/
|
|
89
|
+
providers?: readonly ProviderEntry[];
|
|
90
|
+
/** The app's pino logger. Default: `createLogger()`, JSON at `info`. */
|
|
91
|
+
logger?: Logger;
|
|
92
|
+
/**
|
|
93
|
+
* Middlewares for every request after the health check and OpenAPI routes: routes, mounts and 404s alike.
|
|
94
|
+
* The same values a route's `use` takes. An Express middleware (`fromExpress(cors())`) runs as a plain
|
|
95
|
+
* Express layer; a `defineMiddleware` runs once per request, its `security` is listed on every OpenAPI
|
|
96
|
+
* operation, and a route reads its output with `await middleware.run(ctx)`. To type a middleware's output on
|
|
97
|
+
* `ctx`, list it in the controller's `use`; it still runs only once.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* use: [fromExpress(cors({ origin: env.WEB_ORIGIN })), fromExpress(rateLimit({ limit: 100 }))],
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
use?: readonly Middleware[];
|
|
105
|
+
/**
|
|
106
|
+
* Express handlers that are not brisk routes, by path: static files, GraphQL, tRPC, webhooks that need the
|
|
107
|
+
* raw body, a single-page app's fallback. They answer what brisk's routes did not, so a mount never shadows
|
|
108
|
+
* a route, and they get the request's context, `inject()`, `use` and the error handler. Paths are absolute
|
|
109
|
+
* (the app `prefix` does not apply), and Express strips the path before the handler sees the URL. Brisk does
|
|
110
|
+
* not parse their bodies. A Node `(req, res)` handler, such as GraphQL Yoga's, mounts as
|
|
111
|
+
* `(req, res) => yoga(req, res)`.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* mount: {
|
|
116
|
+
* "/graphql": (req, res) => yoga(req, res),
|
|
117
|
+
* "/webhooks/stripe": [express.raw({ type: "application/json" }), stripeWebhook],
|
|
118
|
+
* "/": express.static("public"),
|
|
119
|
+
* },
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
mount?: Readonly<Record<string, RequestHandler | readonly RequestHandler[]>>;
|
|
123
|
+
hooks?: AppHooks;
|
|
124
|
+
/** Serve an OpenAPI 3.1 document built from the routes' schemas, at `/openapi.json` by default. */
|
|
125
|
+
openapi?: OpenApiOptions;
|
|
126
|
+
errors?: ErrorOptions;
|
|
127
|
+
http?: HttpOptions;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Options for {@link App.create}: providers added after the app's own, typically fakes in tests. A value for a
|
|
131
|
+
* token the app already provides wins; a class is created alongside the app's.
|
|
132
|
+
*/
|
|
133
|
+
export interface CreateOptions {
|
|
134
|
+
providers?: readonly ProviderEntry[];
|
|
135
|
+
}
|
|
136
|
+
/** Options for {@link App.start}. */
|
|
137
|
+
export interface StartOptions extends CreateOptions {
|
|
138
|
+
/**
|
|
139
|
+
* Let the app own the process (default `true`): SIGINT and SIGTERM shut it down gracefully and exit 0; an
|
|
140
|
+
* unhandled rejection or uncaught exception is logged, shuts it down and exits 1.
|
|
141
|
+
*/
|
|
142
|
+
manageProcess?: boolean;
|
|
143
|
+
}
|
|
144
|
+
/** Options for {@link App.listen}. */
|
|
145
|
+
export interface ListenOptions extends StartOptions {
|
|
146
|
+
/** Interface to bind, such as `127.0.0.1` for local only. Default: all interfaces. */
|
|
147
|
+
host?: string;
|
|
148
|
+
/** Your own server, such as `https.createServer(tls)`; brisk attaches its request handler. */
|
|
149
|
+
server?: NodeServer;
|
|
150
|
+
}
|
|
151
|
+
/** Types only: the app's controllers, read by `ApiOf` and `createClient`. */
|
|
152
|
+
interface Typed<C> {
|
|
153
|
+
readonly "~controllers"?: C;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* An app as {@link defineApp} declares it. Nothing runs until `listen`, `start` or `create`; each call makes a
|
|
157
|
+
* fresh, independent instance, so tests can run as many as they like.
|
|
158
|
+
*/
|
|
159
|
+
export interface App<C extends ControllerMap = ControllerMap> extends Typed<C> {
|
|
160
|
+
/**
|
|
161
|
+
* Creates an instance and serves HTTP: init hooks, listen, ready hooks. Resolves with the running instance.
|
|
162
|
+
* When startup fails (an `onInit` throws, the port is taken), what already started is shut down first.
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* ```ts
|
|
166
|
+
* await app.listen(env.PORT);
|
|
167
|
+
* await app.listen(443, { server: https.createServer(tls) });
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
listen(port: number, options?: ListenOptions): Promise<AppInstance<C>>;
|
|
171
|
+
/**
|
|
172
|
+
* Creates an instance and starts it without HTTP: for workers and schedulers that share the app's providers.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* // worker.ts
|
|
177
|
+
* await defineApp({ providers: [EmailConsumer] }).start();
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
start(options?: StartOptions): Promise<AppInstance<C>>;
|
|
181
|
+
/**
|
|
182
|
+
* Creates an instance that is not started, for platforms that bring their own server, such as serverless:
|
|
183
|
+
* `const instance = app.create(); await instance.init();` then hand `instance.express` to the adapter.
|
|
184
|
+
*/
|
|
185
|
+
create(options?: CreateOptions): AppInstance<C>;
|
|
186
|
+
/** The routes by name, as data: what `brisk client` writes for a frontend. */
|
|
187
|
+
readonly manifest: Manifest;
|
|
188
|
+
/** The OpenAPI 3.1 document for the routes: what `brisk openapi` writes. */
|
|
189
|
+
openapi(): Record<string, unknown>;
|
|
190
|
+
}
|
|
191
|
+
/** One instance of an app: its Express app, server and container, and its shutdown. */
|
|
192
|
+
export interface AppInstance<C extends ControllerMap = ControllerMap> extends Typed<C> {
|
|
193
|
+
readonly express: Express;
|
|
194
|
+
/** The listening server, or `undefined` when the instance did not listen. */
|
|
195
|
+
readonly server: NodeServer | undefined;
|
|
196
|
+
readonly container: Container;
|
|
197
|
+
readonly logger: Logger;
|
|
198
|
+
readonly manifest: Manifest;
|
|
199
|
+
openapi(): Record<string, unknown>;
|
|
200
|
+
/** Runs the `onInit` hooks once; `listen` and `start` call it. Call it yourself after `create()`. */
|
|
201
|
+
init(): Promise<void>;
|
|
202
|
+
/** Graceful shutdown: drain, close the server, run `onShutdown`. Safe to call more than once. */
|
|
203
|
+
shutdown(): Promise<void>;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Declares an application: its controllers, providers and settings. Nothing runs at import time; `listen`,
|
|
207
|
+
* `start` and `create` each build a fresh instance. `typeof app` is the API's type for the typed client.
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```ts
|
|
211
|
+
* // src/app.ts
|
|
212
|
+
* export const app = defineApp({
|
|
213
|
+
* prefix: "/api",
|
|
214
|
+
* controllers: { users: usersController },
|
|
215
|
+
* logger: createLogger({ level: env.LOG_LEVEL }),
|
|
216
|
+
* openapi: { title: "Shop API", version: "1.0.0" },
|
|
217
|
+
* });
|
|
218
|
+
* export type Api = typeof app;
|
|
219
|
+
*
|
|
220
|
+
* // src/main.ts
|
|
221
|
+
* await app.listen(env.PORT);
|
|
222
|
+
*
|
|
223
|
+
* // test/users.test.ts
|
|
224
|
+
* const api = await createTestClient(app, { providers: [provide(Clock, () => fixedDate)] });
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
export declare function defineApp<const C extends ControllerMap = {}>(options?: AppOptions<C>): App<C>;
|
|
228
|
+
export {};
|
|
229
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/http/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createInstance } from "./instance.js";
|
|
2
|
+
/**
|
|
3
|
+
* Declares an application: its controllers, providers and settings. Nothing runs at import time; `listen`,
|
|
4
|
+
* `start` and `create` each build a fresh instance. `typeof app` is the API's type for the typed client.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* // src/app.ts
|
|
9
|
+
* export const app = defineApp({
|
|
10
|
+
* prefix: "/api",
|
|
11
|
+
* controllers: { users: usersController },
|
|
12
|
+
* logger: createLogger({ level: env.LOG_LEVEL }),
|
|
13
|
+
* openapi: { title: "Shop API", version: "1.0.0" },
|
|
14
|
+
* });
|
|
15
|
+
* export type Api = typeof app;
|
|
16
|
+
*
|
|
17
|
+
* // src/main.ts
|
|
18
|
+
* await app.listen(env.PORT);
|
|
19
|
+
*
|
|
20
|
+
* // test/users.test.ts
|
|
21
|
+
* const api = await createTestClient(app, { providers: [provide(Clock, () => fixedDate)] });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function defineApp(options = {}) {
|
|
25
|
+
let probe;
|
|
26
|
+
const inspect = () => (probe ??= createInstance(options));
|
|
27
|
+
return {
|
|
28
|
+
async listen(port, { providers, ...listenOptions } = {}) {
|
|
29
|
+
const instance = createInstance(options, providers);
|
|
30
|
+
await instance.listen(port, listenOptions);
|
|
31
|
+
return instance;
|
|
32
|
+
},
|
|
33
|
+
async start({ providers, ...startOptions } = {}) {
|
|
34
|
+
const instance = createInstance(options, providers);
|
|
35
|
+
await instance.start(startOptions);
|
|
36
|
+
return instance;
|
|
37
|
+
},
|
|
38
|
+
create: ({ providers } = {}) => createInstance(options, providers),
|
|
39
|
+
get manifest() {
|
|
40
|
+
return inspect().manifest;
|
|
41
|
+
},
|
|
42
|
+
openapi: () => inspect().openapi(),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../../src/http/app.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,cAAc,EAAe,MAAM,eAAe,CAAC;AA8M5D;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,SAAS,CAAqC,OAAO,GAAkB,EAAE;IACvF,IAAI,KAA4B,CAAC;IACjC,MAAM,OAAO,GAAG,GAAc,EAAE,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IACrE,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,GAAG,aAAa,EAAE,GAAG,EAAE;YACrD,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YACpD,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;YAC3C,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,GAAG,YAAY,EAAE,GAAG,EAAE;YAC7C,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YACpD,MAAM,QAAQ,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YACnC,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,EAAE,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,SAAS,CAAC;QAClE,IAAI,QAAQ;YACV,OAAO,OAAO,EAAE,CAAC,QAAQ,CAAC;QAC5B,CAAC;QACD,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE;KACnC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import type { Logger } from "../config/logger.ts";
|
|
3
|
+
/**
|
|
4
|
+
* What every request carries from the start. A handler's `ctx` adds each middleware's output and the
|
|
5
|
+
* validated input to it.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* route.get("/me", ({ req, requestId, log }) => {
|
|
10
|
+
* log.info({ ip: req.ip }, "profile viewed");
|
|
11
|
+
* return { requestId };
|
|
12
|
+
* });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export interface RequestContext {
|
|
16
|
+
/** The Express request. */
|
|
17
|
+
readonly req: Request;
|
|
18
|
+
/** The Express response. Once you write to it (a file, a stream), brisk sends nothing itself. */
|
|
19
|
+
readonly res: Response;
|
|
20
|
+
/** The caller's `x-request-id` when it is short and plain, otherwise a new UUID. Sent back in the response. */
|
|
21
|
+
readonly requestId: string;
|
|
22
|
+
/** The request's logger: every line carries `requestId`. */
|
|
23
|
+
readonly log: Logger;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A handler's context: the request's fields and the same lazy logger, plus middleware output and validated input.
|
|
27
|
+
* Every field is assigned in the constructor, so all handler contexts share one hidden class. Middleware outputs
|
|
28
|
+
* are written straight onto it; a middleware may return its own `log` (such as `log.child({ userId })`), which
|
|
29
|
+
* the setter accepts.
|
|
30
|
+
*/
|
|
31
|
+
export declare class HandlerContext implements RequestContext {
|
|
32
|
+
#private;
|
|
33
|
+
readonly req: Request;
|
|
34
|
+
readonly res: Response;
|
|
35
|
+
readonly requestId: string;
|
|
36
|
+
params: unknown;
|
|
37
|
+
query: unknown;
|
|
38
|
+
body: unknown;
|
|
39
|
+
headers: unknown;
|
|
40
|
+
constructor(base: RequestContext);
|
|
41
|
+
get log(): Logger;
|
|
42
|
+
set log(log: Logger);
|
|
43
|
+
}
|
|
44
|
+
export declare function createRequestContext(req: Request, res: Response, requestId: string, logger: Logger): RequestContext;
|
|
45
|
+
/** The memo of middleware results for the request behind any context: the base one, a handler's, or a copy. */
|
|
46
|
+
export declare function requestMemo(ctx: RequestContext): Map<object, unknown>;
|
|
47
|
+
/**
|
|
48
|
+
* Returns a request's context, for code that only has the Express `req`, such as an Express handler in the
|
|
49
|
+
* app's `mount`.
|
|
50
|
+
*
|
|
51
|
+
* @throws When the request was not handled by an app made with `defineApp`.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* defineApp({
|
|
56
|
+
* mount: {
|
|
57
|
+
* "/events": (req, res) => {
|
|
58
|
+
* getContext(req).log.info("stream opened");
|
|
59
|
+
* res.type("text/event-stream").write("data: hello\n\n");
|
|
60
|
+
* },
|
|
61
|
+
* },
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export declare function getContext(req: Request): RequestContext;
|
|
66
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../../src/http/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAElD;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,iGAAiG;IACjG,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB,+GAA+G;IAC/G,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4DAA4D;IAC5D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAiCD;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,cAAc;;IACnD,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IAIjB,YAAY,IAAI,EAAE,cAAc,EAS/B;IAED,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,IAAI,GAAG,CAAC,GAAG,EAAE,MAAM,EAElB;CACF;AAED,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,cAAc,CAIhB;AAED,+GAA+G;AAC/G,wBAAgB,WAAW,CAAC,GAAG,EAAE,cAAc,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAErE;AAQD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,cAAc,CAEvD"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const CONTEXT = Symbol("brisk.context");
|
|
2
|
+
/**
|
|
3
|
+
* Fast to create: plain fields, with the request logger and the middleware memo created lazily, so a request
|
|
4
|
+
* that never logs or runs a middleware allocates neither.
|
|
5
|
+
*/
|
|
6
|
+
class Context {
|
|
7
|
+
req;
|
|
8
|
+
res;
|
|
9
|
+
requestId;
|
|
10
|
+
#logger;
|
|
11
|
+
#log;
|
|
12
|
+
#memo;
|
|
13
|
+
constructor(req, res, requestId, logger) {
|
|
14
|
+
this.req = req;
|
|
15
|
+
this.res = res;
|
|
16
|
+
this.requestId = requestId;
|
|
17
|
+
this.#logger = logger;
|
|
18
|
+
}
|
|
19
|
+
get log() {
|
|
20
|
+
return (this.#log ??= this.#logger.child({ requestId: this.requestId }));
|
|
21
|
+
}
|
|
22
|
+
/** Per-request results of middlewares, released with the request: no WeakMap entries for the GC to trace. */
|
|
23
|
+
static memo(ctx) {
|
|
24
|
+
return (ctx.#memo ??= new Map());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A handler's context: the request's fields and the same lazy logger, plus middleware output and validated input.
|
|
29
|
+
* Every field is assigned in the constructor, so all handler contexts share one hidden class. Middleware outputs
|
|
30
|
+
* are written straight onto it; a middleware may return its own `log` (such as `log.child({ userId })`), which
|
|
31
|
+
* the setter accepts.
|
|
32
|
+
*/
|
|
33
|
+
export class HandlerContext {
|
|
34
|
+
req;
|
|
35
|
+
res;
|
|
36
|
+
requestId;
|
|
37
|
+
params;
|
|
38
|
+
query;
|
|
39
|
+
body;
|
|
40
|
+
headers;
|
|
41
|
+
#base;
|
|
42
|
+
#log;
|
|
43
|
+
constructor(base) {
|
|
44
|
+
this.req = base.req;
|
|
45
|
+
this.res = base.res;
|
|
46
|
+
this.requestId = base.requestId;
|
|
47
|
+
this.params = undefined;
|
|
48
|
+
this.query = undefined;
|
|
49
|
+
this.body = undefined;
|
|
50
|
+
this.headers = undefined;
|
|
51
|
+
this.#base = base;
|
|
52
|
+
}
|
|
53
|
+
get log() {
|
|
54
|
+
return this.#log ?? this.#base.log;
|
|
55
|
+
}
|
|
56
|
+
set log(log) {
|
|
57
|
+
this.#log = log;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function createRequestContext(req, res, requestId, logger) {
|
|
61
|
+
const ctx = new Context(req, res, requestId, logger);
|
|
62
|
+
Reflect.set(req, CONTEXT, ctx);
|
|
63
|
+
return ctx;
|
|
64
|
+
}
|
|
65
|
+
/** The memo of middleware results for the request behind any context: the base one, a handler's, or a copy. */
|
|
66
|
+
export function requestMemo(ctx) {
|
|
67
|
+
return Context.memo(ctx instanceof Context ? ctx : contextOf(ctx.req));
|
|
68
|
+
}
|
|
69
|
+
function contextOf(req) {
|
|
70
|
+
const ctx = Reflect.get(req, CONTEXT);
|
|
71
|
+
if (ctx instanceof Context)
|
|
72
|
+
return ctx;
|
|
73
|
+
throw new Error("Request context missing: the request was not handled by an app made with defineApp()");
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Returns a request's context, for code that only has the Express `req`, such as an Express handler in the
|
|
77
|
+
* app's `mount`.
|
|
78
|
+
*
|
|
79
|
+
* @throws When the request was not handled by an app made with `defineApp`.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```ts
|
|
83
|
+
* defineApp({
|
|
84
|
+
* mount: {
|
|
85
|
+
* "/events": (req, res) => {
|
|
86
|
+
* getContext(req).log.info("stream opened");
|
|
87
|
+
* res.type("text/event-stream").write("data: hello\n\n");
|
|
88
|
+
* },
|
|
89
|
+
* },
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function getContext(req) {
|
|
94
|
+
return contextOf(req);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../../src/http/context.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { Router, type Request, type RequestHandler } from "express";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { Container, ProviderEntry } from "../di/container.ts";
|
|
4
|
+
import { type RequestContext } from "./context.ts";
|
|
5
|
+
import { type Middleware, type MiddlewareContext, type SecuritySchemes } from "./middleware.ts";
|
|
6
|
+
import type { PathParams } from "./path-params.ts";
|
|
7
|
+
import type { ControllerTypes, HttpMethod, Manifest, Route, RouteInput, RouteOutput, Routes } from "./routes.ts";
|
|
8
|
+
type Schema = z.ZodType;
|
|
9
|
+
type OptionalSchema = Schema | undefined;
|
|
10
|
+
type Middlewares = readonly Middleware[];
|
|
11
|
+
/** A route's success status: 2xx, or 3xx for redirects. Failures are thrown as errors, not returned. */
|
|
12
|
+
export type SuccessStatus = 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 307 | 308;
|
|
13
|
+
type Output<S, Fallback> = S extends Schema ? z.output<S> : Fallback;
|
|
14
|
+
/** A handler's `ctx`: the request basics, every middleware's output, and the validated input. */
|
|
15
|
+
export type RouteContext<Path extends string, P, Q, B, H, U extends Middlewares> = RequestContext & MiddlewareContext<U> & {
|
|
16
|
+
params: Output<P, PathParams<Path>>;
|
|
17
|
+
query: Output<Q, Record<string, unknown>>;
|
|
18
|
+
body: Output<B, unknown>;
|
|
19
|
+
headers: Output<H, Request["headers"]>;
|
|
20
|
+
};
|
|
21
|
+
/** With a `response` schema, a handler returns the schema's input; fields it does not list are stripped. */
|
|
22
|
+
type Reply<R> = R extends Schema ? z.input<R> : unknown;
|
|
23
|
+
type Handler<Path extends string, P, Q, B, H, U extends Middlewares, Returned> = (ctx: RouteContext<Path, P, Q, B, H, U>) => Returned | Promise<Returned>;
|
|
24
|
+
/**
|
|
25
|
+
* A route's schemas and settings. Each schema is a Zod schema, and its validated value replaces the raw one on
|
|
26
|
+
* the handler's `ctx`.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* route.patch("/:id", {
|
|
31
|
+
* params: z.object({ id: z.uuid() }),
|
|
32
|
+
* body: UpdateUserBody,
|
|
33
|
+
* response: UserResponse,
|
|
34
|
+
* use: [requireRole("admin")],
|
|
35
|
+
* }, ({ params, body }) => users.update(params.id, body));
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export interface RouteOptions<P, Q, B, H, R, U extends Middlewares> {
|
|
39
|
+
params?: P;
|
|
40
|
+
query?: Q;
|
|
41
|
+
body?: B;
|
|
42
|
+
headers?: H;
|
|
43
|
+
/** The response's shape. The result is parsed through it, so fields it does not list never leak. */
|
|
44
|
+
response?: R;
|
|
45
|
+
/** Middlewares for this route only. They run after the controller's and the group's. */
|
|
46
|
+
use?: U;
|
|
47
|
+
/**
|
|
48
|
+
* The success status, which wins over `ctx.res.status()`. Default: 201 for POST, 200 otherwise, and 204 when
|
|
49
|
+
* the handler returns nothing.
|
|
50
|
+
*/
|
|
51
|
+
status?: SuccessStatus;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Declares a route, as `(path, handler)` or `(path, options, handler)`, and returns it. Return it from the
|
|
55
|
+
* controller's setup under a name, and the typed client calls it by that name.
|
|
56
|
+
*
|
|
57
|
+
* Path params such as `:id` are typed on `ctx.params`. With a `response` schema, the handler must return the
|
|
58
|
+
* schema's input, and the client receives its output.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* get: route.get("/:id", { response: UserResponse }, ({ params }) => users.get(params.id)),
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export interface RouteMethod<Prefix extends string, MU extends Middlewares> {
|
|
66
|
+
<const Path extends string, Returned>(path: Path, handler: Handler<`${Prefix}${Path}`, undefined, undefined, undefined, undefined, MU, Returned>): Route<RouteInput<`${Prefix}${Path}`, undefined, undefined, undefined, undefined>, RouteOutput<undefined, Returned>>;
|
|
67
|
+
<const Path extends string, const U extends Middlewares = [], P extends OptionalSchema = undefined, Q extends OptionalSchema = undefined, B extends OptionalSchema = undefined, H extends OptionalSchema = undefined, R extends OptionalSchema = undefined, Returned extends Reply<R> = Reply<R>>(path: Path, options: RouteOptions<P, Q, B, H, R, U>, handler: Handler<`${Prefix}${Path}`, P, Q, B, H, [...MU, ...U], Returned>): Route<RouteInput<`${Prefix}${Path}`, P, Q, B, H>, RouteOutput<R, Returned>>;
|
|
68
|
+
}
|
|
69
|
+
/** Options for {@link RouteBuilder.group}. */
|
|
70
|
+
export interface GroupOptions<U extends Middlewares> {
|
|
71
|
+
/** Middlewares for every route in the group. */
|
|
72
|
+
use?: U;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Nests routes under a sub-path, with extra middlewares when given: `(path, setup)` or `(path, options, setup)`.
|
|
76
|
+
* Returns what `setup` returns, so the group's routes nest under the group's name in the client.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```ts
|
|
80
|
+
* admin: route.group("/admin", { use: [requireRole("admin")] }, (admin) => ({
|
|
81
|
+
* remove: admin.delete("/:id", ({ params }) => users.remove(params.id)),
|
|
82
|
+
* })),
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export interface GroupMethod<Prefix extends string, MU extends Middlewares> {
|
|
86
|
+
<const Path extends string, const R extends Routes | void>(path: Path, setup: (route: RouteBuilder<`${Prefix}${Path}`, MU>) => R): R;
|
|
87
|
+
<const Path extends string, const U extends Middlewares, const R extends Routes | void>(path: Path, options: GroupOptions<U>, setup: (route: RouteBuilder<`${Prefix}${Path}`, [...MU, ...U]>) => R): R;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The `route` object a controller's setup receives. Each method declares one route and returns it.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* defineController({ prefix: "/projects", use: [authenticate] }, (route) => ({
|
|
95
|
+
* list: route.get("/", () => projects.list()),
|
|
96
|
+
* members: route.group("/:projectId/members", { use: [requireRole("owner")] }, (members) => ({
|
|
97
|
+
* add: members.post("/", { body: AddMember }, ({ params, body }) => projects.addMember(params.projectId, body)),
|
|
98
|
+
* })),
|
|
99
|
+
* }));
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export interface RouteBuilder<Prefix extends string, MU extends Middlewares> {
|
|
103
|
+
get: RouteMethod<Prefix, MU>;
|
|
104
|
+
post: RouteMethod<Prefix, MU>;
|
|
105
|
+
put: RouteMethod<Prefix, MU>;
|
|
106
|
+
patch: RouteMethod<Prefix, MU>;
|
|
107
|
+
delete: RouteMethod<Prefix, MU>;
|
|
108
|
+
/** Nests routes under a sub-path, such as an admin-only section with its own middlewares. */
|
|
109
|
+
group: GroupMethod<Prefix, MU>;
|
|
110
|
+
}
|
|
111
|
+
/** Options for {@link defineController}. */
|
|
112
|
+
export interface ControllerOptions<Prefix extends string, U extends Middlewares> {
|
|
113
|
+
/** The controller's path, under the app's `prefix`. */
|
|
114
|
+
prefix: Prefix;
|
|
115
|
+
/** Middlewares for every route; their outputs are typed on each handler's `ctx`. */
|
|
116
|
+
use?: U;
|
|
117
|
+
/**
|
|
118
|
+
* Providers this feature brings, such as the WebSocket gateway or cache invalidator behind its routes:
|
|
119
|
+
* created at startup with the app, so removing the controller removes them. A value the app or a test
|
|
120
|
+
* provides for the same token wins.
|
|
121
|
+
*/
|
|
122
|
+
providers?: readonly ProviderEntry[];
|
|
123
|
+
}
|
|
124
|
+
/** A registered route, as listed in the startup log. */
|
|
125
|
+
export interface RouteInfo {
|
|
126
|
+
method: HttpMethod;
|
|
127
|
+
path: string;
|
|
128
|
+
}
|
|
129
|
+
/** A registered route with what documentation needs: its schemas, success status and name, when it has one. */
|
|
130
|
+
export interface Endpoint extends RouteInfo {
|
|
131
|
+
/** Dotted route name within its controller, such as `admin.remove`; `undefined` for unnamed routes. */
|
|
132
|
+
name: string | undefined;
|
|
133
|
+
schemas: Readonly<Partial<Record<"params" | "query" | "body" | "headers" | "response", z.ZodType>>>;
|
|
134
|
+
status: number | undefined;
|
|
135
|
+
/** The security schemes its middlewares enforce, merged; `undefined` for a public route. */
|
|
136
|
+
security: SecuritySchemes | undefined;
|
|
137
|
+
}
|
|
138
|
+
/** Where the app mounts a controller. */
|
|
139
|
+
export interface MountTarget {
|
|
140
|
+
container: Container;
|
|
141
|
+
/** The app's one router; routes register full paths on it. */
|
|
142
|
+
router: Router;
|
|
143
|
+
/** The app prefix joined to the controller's. */
|
|
144
|
+
basePath: string;
|
|
145
|
+
/** The app's JSON body parser, which runs on brisk's own routes only. */
|
|
146
|
+
parseBody: RequestHandler;
|
|
147
|
+
}
|
|
148
|
+
/** What mounting a controller produced: every route it registered, and its named routes when it returned them. */
|
|
149
|
+
export interface MountedController {
|
|
150
|
+
endpoints: Endpoint[];
|
|
151
|
+
manifest: Manifest | undefined;
|
|
152
|
+
}
|
|
153
|
+
/** A controller, as {@link defineController} returns it. Apps mount it; you only pass it to `defineApp`. */
|
|
154
|
+
export interface Controller<R extends Routes | void = Routes | void> extends ControllerTypes<R> {
|
|
155
|
+
readonly prefix: string;
|
|
156
|
+
/** Providers the controller brings: created with the app, before the app's own providers. */
|
|
157
|
+
readonly providers: readonly ProviderEntry[];
|
|
158
|
+
/** Builds the controller inside the app's container and registers its routes. Called by the app. */
|
|
159
|
+
mount(target: MountTarget): MountedController;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Declares a feature's HTTP routes. `setup` runs once per app instance, inside its container, so it can
|
|
163
|
+
* `inject()` services. It declares routes and returns them by name; the names form the typed client, so
|
|
164
|
+
* `usersController` mounted as `users` gives `api.users.get(...)`.
|
|
165
|
+
*
|
|
166
|
+
* A handler's return value is the response: nothing sends 204, a `Readable` is streamed, a `Buffer` is sent
|
|
167
|
+
* as bytes, and anything else is JSON, parsed through `response` when given. Throw an error class to fail;
|
|
168
|
+
* never call `next()`.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* export const usersController = defineController({ prefix: "/users", use: [authenticate] }, (route) => {
|
|
173
|
+
* const users = inject(UsersService);
|
|
174
|
+
* return {
|
|
175
|
+
* me: route.get("/me", ({ user }) => user),
|
|
176
|
+
* get: route.get("/:id", { params: UserIdParams, response: UserResponse }, ({ params }) => users.get(params.id)),
|
|
177
|
+
* create: route.post("/", { body: CreateUserBody, response: UserResponse }, ({ body }) => users.create(body)),
|
|
178
|
+
* };
|
|
179
|
+
* });
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
export declare function defineController<const Prefix extends string, const U extends Middlewares = [], const R extends Routes | void = void>(options: ControllerOptions<Prefix, U>, setup: (route: RouteBuilder<Prefix, U>) => R): Controller<R>;
|
|
183
|
+
/**
|
|
184
|
+
* Answers requests no controller handled on a known path: `OPTIONS` gets 204, other methods 405, both with an
|
|
185
|
+
* `Allow` header that lists what every controller accepts there.
|
|
186
|
+
*
|
|
187
|
+
* The app mounts it twice. Before the controllers, for `OPTIONS` only, because an Express router would
|
|
188
|
+
* otherwise answer `OPTIONS` itself with just its own methods. After them, where only unmatched requests
|
|
189
|
+
* arrive. A request that matches a route never runs it.
|
|
190
|
+
*/
|
|
191
|
+
export declare function methodGuard(routes: readonly RouteInfo[]): Router;
|
|
192
|
+
/** Joins URL segments into one normalized path: `("/api", "/users/", ":id")` gives `/api/users/:id`. */
|
|
193
|
+
export declare function joinPath(...parts: readonly string[]): string;
|
|
194
|
+
export {};
|
|
195
|
+
//# sourceMappingURL=controller.d.ts.map
|