@znight/http 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +2333 -2
- package/dist/adapter-CGSceVEN.d.mts +876 -0
- package/dist/compiler.d.mts +156 -0
- package/dist/compiler.mjs +2180 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/express.d.mts +39 -0
- package/dist/express.mjs +135 -0
- package/dist/express.mjs.map +1 -0
- package/dist/fastify.d.mts +33 -0
- package/dist/fastify.mjs +191 -0
- package/dist/fastify.mjs.map +1 -0
- package/dist/hono.d.mts +49 -0
- package/dist/hono.mjs +319 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/index.d.mts +1254 -0
- package/dist/index.mjs +5618 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-BUnkdSsa.mjs +183 -0
- package/dist/node-BUnkdSsa.mjs.map +1 -0
- package/dist/node-io-BY1ZDLRQ.mjs +1566 -0
- package/dist/node-io-BY1ZDLRQ.mjs.map +1 -0
- package/dist/node.d.mts +30 -0
- package/dist/node.mjs +1 -0
- package/dist/path-JI87C4k8.d.mts +109 -0
- package/dist/path.d.mts +2 -0
- package/dist/path.mjs +245 -0
- package/dist/path.mjs.map +1 -0
- package/dist/size-D8rsQcV0.mjs +184 -0
- package/dist/size-D8rsQcV0.mjs.map +1 -0
- package/dist/utils-DCbu3a4w.mjs +27 -0
- package/dist/utils-DCbu3a4w.mjs.map +1 -0
- package/package.json +142 -4
- package/src/adapter.ts +240 -0
- package/src/adapters/express.ts +201 -0
- package/src/adapters/fastify.ts +280 -0
- package/src/adapters/hono.ts +488 -0
- package/src/adapters/node-io.ts +368 -0
- package/src/adapters/node.ts +135 -0
- package/src/adapters/shared.ts +508 -0
- package/src/body.ts +135 -0
- package/src/coerce.ts +195 -0
- package/src/compiler/analyze.ts +1513 -0
- package/src/compiler/ast.ts +90 -0
- package/src/compiler/coercion.ts +267 -0
- package/src/compiler/generate.ts +374 -0
- package/src/compiler/index.ts +76 -0
- package/src/compiler/model.ts +260 -0
- package/src/compiler/route-decorators.ts +405 -0
- package/src/compiler/validate.ts +288 -0
- package/src/context.ts +533 -0
- package/src/cookie-settings.ts +68 -0
- package/src/cookie-signer.ts +106 -0
- package/src/cookies.ts +726 -0
- package/src/cors.ts +349 -0
- package/src/decisions.ts +331 -0
- package/src/decorators.ts +486 -0
- package/src/event-channel.ts +197 -0
- package/src/exceptions.ts +257 -0
- package/src/files.ts +429 -0
- package/src/headers.ts +39 -0
- package/src/index.ts +185 -0
- package/src/inject-sse.ts +309 -0
- package/src/interfaces.ts +101 -0
- package/src/metadata.ts +162 -0
- package/src/mime.ts +157 -0
- package/src/multipart.ts +597 -0
- package/src/option-error.ts +31 -0
- package/src/path.ts +288 -0
- package/src/pipeline.ts +918 -0
- package/src/plugin-routes.ts +188 -0
- package/src/plugin.ts +1432 -0
- package/src/proxy.ts +236 -0
- package/src/query.ts +30 -0
- package/src/response.ts +240 -0
- package/src/router.ts +119 -0
- package/src/security-headers.ts +161 -0
- package/src/serialize.ts +264 -0
- package/src/size.ts +66 -0
- package/src/sse-parse.ts +194 -0
- package/src/sse.ts +650 -0
- package/src/standard-schema.ts +81 -0
- package/src/status.ts +78 -0
- package/src/stream.ts +200 -0
- package/src/tokens.ts +21 -0
- package/src/uploads.ts +176 -0
- package/src/utils.ts +31 -0
- package/src/validate.ts +65 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin routes (RFC 0002 H1): routes another plugin asks `HttpPlugin` to serve (documentation,
|
|
3
|
+
* health or metrics endpoints), registered on every adapter in the same pipeline as the compiled
|
|
4
|
+
* controllers.
|
|
5
|
+
*/
|
|
6
|
+
import type { Container, Plugin } from '@znight/core'
|
|
7
|
+
import type { HttpContext } from './context.ts'
|
|
8
|
+
import type { RouteMetaEntry } from './decorators.ts'
|
|
9
|
+
import type { HttpMethod } from './metadata.ts'
|
|
10
|
+
import type { GuardRef } from './plugin.ts'
|
|
11
|
+
|
|
12
|
+
import { ErrorCode, ZnightError } from '@znight/core'
|
|
13
|
+
|
|
14
|
+
import { isGroupOf } from './decorators.ts'
|
|
15
|
+
import { joinPaths, normalizePath, parseRoutePath, type RoutePath, RoutePathError } from './path.ts'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A route served by `HttpPlugin` on behalf of another plugin. It runs through the same pipeline as
|
|
19
|
+
* controller routes: `http:request` hook, request scope, global guards, then its own `guards`, the
|
|
20
|
+
* handler, the exception mappers; global interceptors do not wrap it. The handler result is
|
|
21
|
+
* serialised like a controller result (`HttpResponse`, objects as JSON, strings, streams...).
|
|
22
|
+
*/
|
|
23
|
+
export interface PluginRoute {
|
|
24
|
+
/** `GET` routes also answer `HEAD`; `ALL` registers every method without its own route. */
|
|
25
|
+
readonly method: HttpMethod
|
|
26
|
+
/** Path relative to the `HttpPlugin` prefix, with the controller grammar (`/openapi.json`, `/docs/_assets/*`). */
|
|
27
|
+
readonly path: string
|
|
28
|
+
/** Handler name, shown in `HttpPlugin.routes`, logs and errors (`document`). */
|
|
29
|
+
readonly name: string
|
|
30
|
+
readonly handler: (ctx: HttpContext) => unknown
|
|
31
|
+
/**
|
|
32
|
+
* Route metadata (`[Public.meta()]`): read by guards (`Public.get(ctx)`) and by the boot check of
|
|
33
|
+
* required route decorator groups. One entry per group.
|
|
34
|
+
*/
|
|
35
|
+
readonly meta?: readonly RouteMetaEntry[]
|
|
36
|
+
/** Guards of the route (provider classes or instances), run after the global guards. */
|
|
37
|
+
readonly guards?: readonly GuardRef[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** What {@link PluginHttpRoutes.httpRoutes} receives. */
|
|
41
|
+
export interface PluginHttpRoutesContext {
|
|
42
|
+
/** The `HttpPlugin` prefix (`''` or `/api`), already applied to the paths of the routes. */
|
|
43
|
+
readonly prefix: string
|
|
44
|
+
/** The application container (initialised). */
|
|
45
|
+
readonly container: Container
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Implemented by a plugin that serves HTTP routes. `HttpPlugin` calls `httpRoutes()` once in its
|
|
50
|
+
* `onBootstrap`, after `container.init()` and whatever the plugin order, then registers the routes
|
|
51
|
+
* on its adapter. A path that breaks the grammar, or that collides with another route, is ZNIGHT007
|
|
52
|
+
* naming the plugin.
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* export class StatusPlugin implements Plugin, PluginHttpRoutes {
|
|
56
|
+
* readonly name = 'status'
|
|
57
|
+
* httpRoutes(): readonly PluginRoute[] {
|
|
58
|
+
* return [{ method: 'GET', path: '/status', name: 'status', handler: () => ({ up: true }), meta: [Public.meta()] }]
|
|
59
|
+
* }
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export interface PluginHttpRoutes {
|
|
64
|
+
httpRoutes(context: PluginHttpRoutesContext): readonly PluginRoute[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A validated plugin route (internal). */
|
|
68
|
+
export interface CollectedPluginRoute {
|
|
69
|
+
readonly plugin: string
|
|
70
|
+
/** The plugin instance (the "controller" of the route). */
|
|
71
|
+
readonly instance: object
|
|
72
|
+
readonly method: HttpMethod
|
|
73
|
+
/** Full path, prefix included. */
|
|
74
|
+
readonly path: RoutePath
|
|
75
|
+
/** Normalised path without the prefix. */
|
|
76
|
+
readonly relativePath: string
|
|
77
|
+
readonly name: string
|
|
78
|
+
readonly handler: (ctx: HttpContext) => unknown
|
|
79
|
+
readonly meta: readonly NormalizedMetaEntry[]
|
|
80
|
+
readonly guards: readonly unknown[]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A plugin route metadata entry with its group (internal). */
|
|
84
|
+
export interface NormalizedMetaEntry {
|
|
85
|
+
readonly key: string
|
|
86
|
+
readonly value: unknown
|
|
87
|
+
readonly group: string
|
|
88
|
+
readonly required: boolean
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const METHODS: ReadonlySet<string> = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'ALL'])
|
|
92
|
+
|
|
93
|
+
/** Calls `httpRoutes()` on every other plugin implementing it, in plugin order, and validates the routes. */
|
|
94
|
+
export function collectPluginRoutes(
|
|
95
|
+
plugins: readonly Plugin[],
|
|
96
|
+
self: Plugin,
|
|
97
|
+
context: PluginHttpRoutesContext,
|
|
98
|
+
): CollectedPluginRoute[] {
|
|
99
|
+
const collected: CollectedPluginRoute[] = []
|
|
100
|
+
for (const plugin of plugins) {
|
|
101
|
+
if (plugin === self) continue
|
|
102
|
+
const provider = plugin as Plugin & Partial<PluginHttpRoutes>
|
|
103
|
+
if (typeof provider.httpRoutes !== 'function') continue
|
|
104
|
+
const name = String(plugin.name)
|
|
105
|
+
const routes: unknown = provider.httpRoutes(context)
|
|
106
|
+
if (!Array.isArray(routes)) {
|
|
107
|
+
throw invalid(name, `httpRoutes() must return an array of routes (received ${describe(routes)})`)
|
|
108
|
+
}
|
|
109
|
+
for (const route of routes as unknown[]) collected.push(validateRoute(plugin, name, route, context.prefix))
|
|
110
|
+
}
|
|
111
|
+
return collected
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function validateRoute(instance: object, plugin: string, value: unknown, prefix: string): CollectedPluginRoute {
|
|
115
|
+
if (value === null || typeof value !== 'object') throw invalid(plugin, `invalid route ${describe(value)}`)
|
|
116
|
+
const route = value as Partial<PluginRoute>
|
|
117
|
+
const label = `${String(route.method)} ${String(route.path)}`
|
|
118
|
+
if (typeof route.name !== 'string' || route.name === '') {
|
|
119
|
+
throw invalid(plugin, `the route ${label} needs a name (handler name shown in HttpPlugin.routes)`)
|
|
120
|
+
}
|
|
121
|
+
const where = `${label} (${route.name})`
|
|
122
|
+
if (typeof route.method !== 'string' || !METHODS.has(route.method)) {
|
|
123
|
+
throw invalid(plugin, `unknown method ${describe(route.method)} of the route ${where}`)
|
|
124
|
+
}
|
|
125
|
+
if (typeof route.handler !== 'function') throw invalid(plugin, `the route ${where} has no handler function`)
|
|
126
|
+
if (typeof route.path !== 'string') throw invalid(plugin, `the route ${where} has no path`)
|
|
127
|
+
let path: RoutePath
|
|
128
|
+
try {
|
|
129
|
+
path = parseRoutePath(joinPaths(prefix, route.path))
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (!(error instanceof RoutePathError)) throw error
|
|
132
|
+
throw invalid(plugin, `invalid path of the route ${where}: ${error.issue.message}`, error)
|
|
133
|
+
}
|
|
134
|
+
const meta = route.meta ?? []
|
|
135
|
+
if (!Array.isArray(meta)) throw invalid(plugin, `the meta of the route ${where} must be an array of entries`)
|
|
136
|
+
const entries = (meta as unknown[]).map(entry => metaEntry(plugin, where, entry))
|
|
137
|
+
const groups = new Map<string, string>()
|
|
138
|
+
for (const entry of entries) {
|
|
139
|
+
const other = groups.get(entry.group)
|
|
140
|
+
if (other !== undefined) {
|
|
141
|
+
throw invalid(
|
|
142
|
+
plugin,
|
|
143
|
+
`the route ${where} has two entries of the "${entry.group}" group (${other}, ${entry.key}): keep one`,
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
groups.set(entry.group, entry.key)
|
|
147
|
+
}
|
|
148
|
+
const guards = route.guards ?? []
|
|
149
|
+
if (!Array.isArray(guards)) throw invalid(plugin, `the guards of the route ${where} must be an array`)
|
|
150
|
+
return {
|
|
151
|
+
plugin,
|
|
152
|
+
instance,
|
|
153
|
+
method: route.method as HttpMethod,
|
|
154
|
+
path,
|
|
155
|
+
relativePath: normalizePath(route.path),
|
|
156
|
+
name: route.name,
|
|
157
|
+
handler: route.handler,
|
|
158
|
+
meta: entries,
|
|
159
|
+
guards,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function metaEntry(plugin: string, where: string, value: unknown): NormalizedMetaEntry {
|
|
164
|
+
const entry = (value ?? {}) as Partial<RouteMetaEntry>
|
|
165
|
+
if (value === null || typeof value !== 'object' || typeof entry.key !== 'string' || entry.key === '') {
|
|
166
|
+
throw invalid(plugin, `invalid meta entry ${describe(value)} of the route ${where}: expected Decorator.meta(value)`)
|
|
167
|
+
}
|
|
168
|
+
const group = entry.group ?? entry.key
|
|
169
|
+
const required = entry.required ?? false
|
|
170
|
+
if (!isGroupOf(group, entry.key) || typeof required !== 'boolean') {
|
|
171
|
+
throw invalid(plugin, `invalid group of the meta entry "${entry.key}" of the route ${where}`)
|
|
172
|
+
}
|
|
173
|
+
return { key: entry.key, value: entry.value, group, required }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function invalid(plugin: string, reason: string, cause?: unknown): ZnightError {
|
|
177
|
+
return new ZnightError(ErrorCode.InvalidRoute, `HttpPlugin: plugin "${plugin}": ${reason}`, {
|
|
178
|
+
...(cause === undefined ? {} : { cause }),
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function describe(value: unknown): string {
|
|
183
|
+
try {
|
|
184
|
+
return typeof value === 'function' ? 'a function' : (JSON.stringify(value) ?? String(value))
|
|
185
|
+
} catch {
|
|
186
|
+
return typeof value
|
|
187
|
+
}
|
|
188
|
+
}
|