@znight/http 0.0.0-stage → 0.1.1
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,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `generate` of the `@znight/http` compiler extension: the `metadata.extensions.http` section
|
|
3
|
+
* (README, "Compiler contract"). Controllers are sorted by token, routes keep their source order and
|
|
4
|
+
* every route gets an `invoke(c, ctx)` arrow extracting, coercing and validating its arguments.
|
|
5
|
+
*/
|
|
6
|
+
import type { ClassRef, GenerateContext, GenInput, GenNode, ValueRef } from '@znight/compiler'
|
|
7
|
+
import type {
|
|
8
|
+
CoercionPlan,
|
|
9
|
+
HttpControllerData,
|
|
10
|
+
HttpParamData,
|
|
11
|
+
HttpRouteData,
|
|
12
|
+
MetaEntry,
|
|
13
|
+
ParamCoercion,
|
|
14
|
+
SseRouteData,
|
|
15
|
+
UploadData,
|
|
16
|
+
} from './model.ts'
|
|
17
|
+
|
|
18
|
+
import { compareStrings } from '../path.ts'
|
|
19
|
+
import { HTTP_PACKAGE } from './model.ts'
|
|
20
|
+
|
|
21
|
+
type Context = GenerateContext<HttpControllerData>
|
|
22
|
+
|
|
23
|
+
/** `uploads` helper by kind and presence (`@UploadedFile` / `@UploadedFiles`, required or not). */
|
|
24
|
+
const UPLOAD_HELPERS = {
|
|
25
|
+
file: { required: 'file', optional: 'optionalFile' },
|
|
26
|
+
files: { required: 'files', optional: 'optionalFiles' },
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
/** Imports of `@znight/http` helpers, created on first use (the generated module has no unused import). */
|
|
30
|
+
interface Helpers {
|
|
31
|
+
coerce(): GenNode
|
|
32
|
+
validate(): GenNode
|
|
33
|
+
uploads(): GenNode
|
|
34
|
+
context(): GenNode
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Generates `metadata.extensions.http` (`undefined` when the application has no controller). */
|
|
38
|
+
export function generateHttp(ctx: Context): GenNode | undefined {
|
|
39
|
+
if (ctx.stored.length === 0) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token))
|
|
43
|
+
const helpers = createHelpers(ctx)
|
|
44
|
+
const entries: [string, GenInput][] = [
|
|
45
|
+
['controllers', ctx.arr(controllers.map(controller => controllerNode(ctx, helpers, controller)))],
|
|
46
|
+
]
|
|
47
|
+
const groups = groupsNode(ctx, controllers)
|
|
48
|
+
if (groups !== undefined) {
|
|
49
|
+
entries.push(['groups', groups])
|
|
50
|
+
}
|
|
51
|
+
return ctx.obj(entries)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* `groups`: the route decorator groups the runtime needs (required, holding several keys, or named
|
|
56
|
+
* differently from their key), sorted, with sorted keys; `undefined` when there is none, so that
|
|
57
|
+
* applications without groups keep a byte-identical output.
|
|
58
|
+
*/
|
|
59
|
+
function groupsNode(ctx: Context, controllers: readonly HttpControllerData[]): GenNode | undefined {
|
|
60
|
+
const groups = new Map<string, { readonly keys: Set<string>; required: boolean }>()
|
|
61
|
+
const visit = (entries: readonly MetaEntry[]): void => {
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const group = groups.get(entry.group)
|
|
64
|
+
if (group === undefined) {
|
|
65
|
+
groups.set(entry.group, { keys: new Set([entry.key]), required: entry.required })
|
|
66
|
+
} else {
|
|
67
|
+
group.keys.add(entry.key)
|
|
68
|
+
group.required ||= entry.required
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const controller of controllers) {
|
|
73
|
+
visit(controller.meta)
|
|
74
|
+
for (const route of controller.routes) {
|
|
75
|
+
visit(route.meta)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const needed = [...groups]
|
|
79
|
+
.filter(([name, group]) => group.required || group.keys.size > 1 || !group.keys.has(name))
|
|
80
|
+
.sort(([a], [b]) => compareStrings(a, b))
|
|
81
|
+
if (needed.length === 0) {
|
|
82
|
+
return undefined
|
|
83
|
+
}
|
|
84
|
+
return ctx.obj(
|
|
85
|
+
needed.map(
|
|
86
|
+
([name, group]) =>
|
|
87
|
+
[
|
|
88
|
+
name,
|
|
89
|
+
ctx.obj([
|
|
90
|
+
['keys', ctx.lit([...group.keys].sort(compareStrings))],
|
|
91
|
+
['required', group.required],
|
|
92
|
+
]),
|
|
93
|
+
] as const,
|
|
94
|
+
),
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createHelpers(ctx: Context): Helpers {
|
|
99
|
+
let coerce: GenNode | undefined
|
|
100
|
+
let validate: GenNode | undefined
|
|
101
|
+
let uploads: GenNode | undefined
|
|
102
|
+
let context: GenNode | undefined
|
|
103
|
+
return {
|
|
104
|
+
coerce: () => {
|
|
105
|
+
coerce ??= ctx.importFrom(HTTP_PACKAGE, 'coerce')
|
|
106
|
+
return coerce
|
|
107
|
+
},
|
|
108
|
+
validate: () => {
|
|
109
|
+
validate ??= ctx.importFrom(HTTP_PACKAGE, 'validate')
|
|
110
|
+
return validate
|
|
111
|
+
},
|
|
112
|
+
uploads: () => {
|
|
113
|
+
uploads ??= ctx.importFrom(HTTP_PACKAGE, 'uploads')
|
|
114
|
+
return uploads
|
|
115
|
+
},
|
|
116
|
+
context: () => {
|
|
117
|
+
context ??= ctx.importFrom(HTTP_PACKAGE, 'HttpContext', { type: true })
|
|
118
|
+
return context
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function controllerNode(ctx: Context, helpers: Helpers, controller: HttpControllerData): GenNode {
|
|
124
|
+
return ctx.obj([
|
|
125
|
+
['token', controller.token],
|
|
126
|
+
['basePath', controller.basePath],
|
|
127
|
+
['guards', tokens(ctx, controller.guards)],
|
|
128
|
+
['interceptors', tokens(ctx, controller.interceptors)],
|
|
129
|
+
['meta', metaNode(ctx, controller.meta)],
|
|
130
|
+
['routes', ctx.arr(controller.routes.map(route => routeNode(ctx, helpers, controller, route)))],
|
|
131
|
+
])
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function routeNode(ctx: Context, helpers: Helpers, controller: HttpControllerData, route: HttpRouteData): GenNode {
|
|
135
|
+
const entries: [string, GenInput][] = [
|
|
136
|
+
['method', route.method],
|
|
137
|
+
['path', route.path],
|
|
138
|
+
['fullPath', route.fullPath],
|
|
139
|
+
['handler', route.handler],
|
|
140
|
+
]
|
|
141
|
+
if (route.status !== undefined) {
|
|
142
|
+
entries.push(['status', route.status])
|
|
143
|
+
}
|
|
144
|
+
if (route.headers.length > 0) {
|
|
145
|
+
entries.push(['headers', ctx.obj(route.headers.map(([name, value]) => [name, value] as const))])
|
|
146
|
+
}
|
|
147
|
+
entries.push(
|
|
148
|
+
['meta', metaNode(ctx, route.meta)],
|
|
149
|
+
['guards', tokens(ctx, route.guards)],
|
|
150
|
+
['interceptors', tokens(ctx, route.interceptors)],
|
|
151
|
+
['params', ctx.arr(route.params.map(param => paramNode(ctx, param)))],
|
|
152
|
+
)
|
|
153
|
+
if (route.bodyLimit !== undefined) {
|
|
154
|
+
entries.push(['bodyLimit', route.bodyLimit])
|
|
155
|
+
}
|
|
156
|
+
const uploads = route.params.flatMap(param => (param.upload === undefined ? [] : [param.upload]))
|
|
157
|
+
if (uploads.length > 0) {
|
|
158
|
+
entries.push(['uploads', ctx.arr(uploads.map(upload => uploadNode(ctx, upload)))])
|
|
159
|
+
}
|
|
160
|
+
if (route.rawResponse) {
|
|
161
|
+
entries.push(['rawResponse', true])
|
|
162
|
+
}
|
|
163
|
+
if (route.sse !== undefined) {
|
|
164
|
+
entries.push(['sse', sseNode(ctx, route.sse)])
|
|
165
|
+
}
|
|
166
|
+
entries.push(['invoke', invokeNode(ctx, helpers, controller, route)])
|
|
167
|
+
return ctx.obj(entries)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** `{ heartbeat?, retry?, maxDuration? }` (`{}` without options). */
|
|
171
|
+
function sseNode(ctx: Context, sse: SseRouteData): GenNode {
|
|
172
|
+
const entries: [string, GenInput][] = []
|
|
173
|
+
if (sse.heartbeat !== undefined) {
|
|
174
|
+
entries.push(['heartbeat', sse.heartbeat])
|
|
175
|
+
}
|
|
176
|
+
if (sse.retry !== undefined) {
|
|
177
|
+
entries.push(['retry', sse.retry])
|
|
178
|
+
}
|
|
179
|
+
if (sse.maxDuration !== undefined) {
|
|
180
|
+
entries.push(['maxDuration', sse.maxDuration])
|
|
181
|
+
}
|
|
182
|
+
return ctx.obj(entries)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function paramNode(ctx: Context, param: HttpParamData): GenNode {
|
|
186
|
+
const entries: [string, GenInput][] = [
|
|
187
|
+
['index', param.index],
|
|
188
|
+
['source', param.source],
|
|
189
|
+
]
|
|
190
|
+
if (param.key !== undefined) {
|
|
191
|
+
entries.push(['name', param.key])
|
|
192
|
+
}
|
|
193
|
+
if (param.type !== undefined) {
|
|
194
|
+
entries.push(['type', param.type])
|
|
195
|
+
}
|
|
196
|
+
if (param.optional) {
|
|
197
|
+
entries.push(['optional', true])
|
|
198
|
+
}
|
|
199
|
+
if (param.schema !== undefined) {
|
|
200
|
+
entries.push(['schema', true])
|
|
201
|
+
}
|
|
202
|
+
return ctx.obj(entries)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** `{ field?, multiple?, required?, maxSize?, maxFiles?, mimeTypes? }` (absent values omitted). */
|
|
206
|
+
function uploadNode(ctx: Context, upload: UploadData): GenNode {
|
|
207
|
+
const entries: [string, GenInput][] = []
|
|
208
|
+
if (upload.field !== undefined) {
|
|
209
|
+
entries.push(['field', upload.field])
|
|
210
|
+
}
|
|
211
|
+
if (upload.multiple) {
|
|
212
|
+
entries.push(['multiple', true])
|
|
213
|
+
}
|
|
214
|
+
if (upload.required) {
|
|
215
|
+
entries.push(['required', true])
|
|
216
|
+
}
|
|
217
|
+
if (upload.maxSize !== undefined) {
|
|
218
|
+
entries.push(['maxSize', upload.maxSize])
|
|
219
|
+
}
|
|
220
|
+
if (upload.maxFiles !== undefined) {
|
|
221
|
+
entries.push(['maxFiles', upload.maxFiles])
|
|
222
|
+
}
|
|
223
|
+
if (upload.mimeTypes !== undefined) {
|
|
224
|
+
entries.push(['mimeTypes', ctx.lit(upload.mimeTypes)])
|
|
225
|
+
}
|
|
226
|
+
return ctx.obj(entries)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function metaNode(ctx: Context, meta: readonly MetaEntry[]): GenNode {
|
|
230
|
+
return ctx.obj(meta.map(entry => [entry.key, ctx.lit(entry.value)] as const))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function tokens(ctx: Context, refs: readonly ClassRef[]): GenNode {
|
|
234
|
+
return ctx.arr(refs.map(ref => ctx.tokenOf(ref) ?? ref.token))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// -------------------------------------------------------------------------------------------------
|
|
238
|
+
// invoke
|
|
239
|
+
// -------------------------------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* `(c: Controller, ctx: HttpContext) => c.handler(...args)`: `async` (awaiting every `validate()`)
|
|
243
|
+
* when the route validates something with a schema, synchronous otherwise.
|
|
244
|
+
*/
|
|
245
|
+
function invokeNode(ctx: Context, helpers: Helpers, controller: HttpControllerData, route: HttpRouteData): GenNode {
|
|
246
|
+
const async = route.bodySchema !== undefined || route.params.some(param => param.schema !== undefined)
|
|
247
|
+
const params: [string, GenNode][] = [['c', ctx.importType(controller.identity)]]
|
|
248
|
+
// `ctx` is declared only when used (generated code type-checks with `noUnusedParameters`)
|
|
249
|
+
if (route.params.length > 0 || route.bodySchema !== undefined) {
|
|
250
|
+
params.push(['ctx', helpers.context()])
|
|
251
|
+
}
|
|
252
|
+
return ctx.arrow(
|
|
253
|
+
params,
|
|
254
|
+
(c, http) => {
|
|
255
|
+
const args: GenNode[] = []
|
|
256
|
+
for (const param of route.params) {
|
|
257
|
+
args[param.index] = argumentNode(ctx, helpers, http as GenNode, param)
|
|
258
|
+
}
|
|
259
|
+
const call = ctx.call(
|
|
260
|
+
ctx.member(c, route.handler),
|
|
261
|
+
Array.from(args, arg => arg ?? ctx.lit(undefined)),
|
|
262
|
+
)
|
|
263
|
+
if (route.bodySchema === undefined) {
|
|
264
|
+
return call
|
|
265
|
+
}
|
|
266
|
+
// @Validate(S) without @Body() parameter: the body is validated (and discarded)
|
|
267
|
+
const body = ctx.code`await ${ctx.call(ctx.member(http as GenNode, 'parseBody'))}`
|
|
268
|
+
const check = validation(ctx, helpers, route.bodySchema, body, 'body')
|
|
269
|
+
return ctx.code`{ ${check}; return ${call} }`
|
|
270
|
+
},
|
|
271
|
+
{ async },
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The expression passed to the handler for one parameter. */
|
|
276
|
+
function argumentNode(ctx: Context, helpers: Helpers, http: GenNode, param: HttpParamData): GenNode {
|
|
277
|
+
switch (param.source) {
|
|
278
|
+
case 'context':
|
|
279
|
+
return http
|
|
280
|
+
case 'request':
|
|
281
|
+
return ctx.code`${ctx.member(http, 'request')} as never`
|
|
282
|
+
case 'response':
|
|
283
|
+
return ctx.code`${ctx.member(http, 'response')} as never`
|
|
284
|
+
case 'body': {
|
|
285
|
+
const body = ctx.member(http, 'body')
|
|
286
|
+
return param.schema === undefined
|
|
287
|
+
? ctx.code`${body} as never`
|
|
288
|
+
: validation(ctx, helpers, param.schema, body, 'body')
|
|
289
|
+
}
|
|
290
|
+
case 'file':
|
|
291
|
+
case 'files': {
|
|
292
|
+
const upload = param.upload as UploadData
|
|
293
|
+
const helper = UPLOAD_HELPERS[upload.multiple ? 'files' : 'file'][upload.required ? 'required' : 'optional']
|
|
294
|
+
const call = ctx.call(ctx.member(helpers.uploads(), helper), [http, ctx.lit(upload.field)])
|
|
295
|
+
return upload.cast ? ctx.code`${call} as never` : call
|
|
296
|
+
}
|
|
297
|
+
case 'headers':
|
|
298
|
+
case 'param':
|
|
299
|
+
case 'query': {
|
|
300
|
+
const record = ctx.member(http, param.source === 'param' ? 'params' : param.source)
|
|
301
|
+
if (param.key === undefined) {
|
|
302
|
+
const location = param.source === 'param' ? 'params' : param.source
|
|
303
|
+
return param.schema === undefined
|
|
304
|
+
? ctx.code`${record} as never`
|
|
305
|
+
: validation(ctx, helpers, param.schema, record, location)
|
|
306
|
+
}
|
|
307
|
+
const value = ctx.code`${record}[${param.key}]`
|
|
308
|
+
const label = `${param.source === 'headers' ? 'header' : param.source} "${param.key}"`
|
|
309
|
+
if (param.schema !== undefined) {
|
|
310
|
+
return validation(ctx, helpers, param.schema, value, label)
|
|
311
|
+
}
|
|
312
|
+
return coercion(ctx, helpers, param.coercion ?? { plan: { kind: 'raw' }, cast: false }, value, label)
|
|
313
|
+
}
|
|
314
|
+
case 'cookie':
|
|
315
|
+
case 'signedCookie': {
|
|
316
|
+
if (param.key === undefined) {
|
|
317
|
+
// @Cookie() / @Cookie(S): every cookie
|
|
318
|
+
const cookies = ctx.member(http, 'cookies')
|
|
319
|
+
return param.schema === undefined
|
|
320
|
+
? ctx.code`${cookies} as never`
|
|
321
|
+
: validation(ctx, helpers, param.schema, cookies, 'cookies')
|
|
322
|
+
}
|
|
323
|
+
const value =
|
|
324
|
+
param.source === 'cookie'
|
|
325
|
+
? ctx.code`${ctx.member(http, 'cookies')}[${param.key}]`
|
|
326
|
+
: ctx.call(ctx.member(http, 'signedCookie'), [param.key])
|
|
327
|
+
const label = `cookie "${param.key}"`
|
|
328
|
+
if (param.schema !== undefined) {
|
|
329
|
+
return validation(ctx, helpers, param.schema, value, label)
|
|
330
|
+
}
|
|
331
|
+
return coercion(ctx, helpers, param.coercion ?? { plan: { kind: 'raw' }, cast: false }, value, label)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** `await validate(schema, value, location)`. */
|
|
337
|
+
function validation(ctx: Context, helpers: Helpers, schema: ValueRef, value: GenNode, location: string): GenNode {
|
|
338
|
+
return ctx.code`await ${ctx.call(helpers.validate(), [ctx.ref(schema), value, location])}`
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** `coerce.X(value, label, ...)`, cast with `as never` when the result does not have the declared type. */
|
|
342
|
+
function coercion(ctx: Context, helpers: Helpers, coercion: ParamCoercion, value: GenNode, label: string): GenNode {
|
|
343
|
+
const node = coerceCall(ctx, helpers, coercion.plan, value, label)
|
|
344
|
+
return coercion.cast ? ctx.code`${node} as never` : node
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function coerceCall(ctx: Context, helpers: Helpers, plan: CoercionPlan, value: GenNode, label: GenInput): GenNode {
|
|
348
|
+
const helper = (name: string): GenNode => ctx.member(helpers.coerce(), name)
|
|
349
|
+
switch (plan.kind) {
|
|
350
|
+
case 'raw':
|
|
351
|
+
return ctx.code`${value} as never`
|
|
352
|
+
case 'enum':
|
|
353
|
+
return ctx.call(helper('enum'), [value, label, ctx.lit(plan.values)])
|
|
354
|
+
case 'array':
|
|
355
|
+
case 'optional':
|
|
356
|
+
return ctx.call(helper(plan.kind), [value, label, itemCoercer(ctx, helpers, plan.item)])
|
|
357
|
+
default:
|
|
358
|
+
return ctx.call(helper(plan.kind), [value, label])
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** A `Coercer` value: `coerce.number`, or `(v, l) => coerce.enum(v, l, [...])`. */
|
|
363
|
+
function itemCoercer(ctx: Context, helpers: Helpers, plan: CoercionPlan): GenNode {
|
|
364
|
+
switch (plan.kind) {
|
|
365
|
+
case 'enum':
|
|
366
|
+
case 'array':
|
|
367
|
+
case 'optional':
|
|
368
|
+
return ctx.arrow(['v', 'l'], (v, l) => coerceCall(ctx, helpers, plan, v, l))
|
|
369
|
+
case 'raw':
|
|
370
|
+
return ctx.arrow(['v'], v => v)
|
|
371
|
+
default:
|
|
372
|
+
return ctx.member(helpers.coerce(), plan.kind)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@znight/http/compiler`: the compiler extension of `@znight/http` (architecture §6.5), loaded by
|
|
3
|
+
* `@znight/compiler` through `package.json#znight.compiler`.
|
|
4
|
+
*
|
|
5
|
+
* - `analyzeClass`: `@Controller` classes become providers; their decorated methods become routes
|
|
6
|
+
* (paths, parameters with type-based coercion or Standard Schema validation, cookies, uploaded
|
|
7
|
+
* files, guards, interceptors, status, headers, body limits, `@Sse` event streams,
|
|
8
|
+
* `createRouteDecorator` metadata);
|
|
9
|
+
* - `validate`: route paths, `:param` ↔ `@Param`, duplicate routes (ZNIGHT007);
|
|
10
|
+
* - `generate`: `metadata.extensions.http` with one `invoke` function per route (README,
|
|
11
|
+
* "Compiler contract").
|
|
12
|
+
*
|
|
13
|
+
* This entry only `import type`s from `@znight/compiler` and never loads the HTTP runtime.
|
|
14
|
+
*/
|
|
15
|
+
import type { CompilerExtension, DecoratorSpec } from '@znight/compiler'
|
|
16
|
+
import type { HttpControllerData } from './model.ts'
|
|
17
|
+
|
|
18
|
+
import { analyzeHttpClass, EXTENSION_NAME } from './analyze.ts'
|
|
19
|
+
import { generateHttp } from './generate.ts'
|
|
20
|
+
import { Ids, ROUTE_METHODS } from './model.ts'
|
|
21
|
+
import { validateHttp } from './validate.ts'
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
CoercionPlan,
|
|
25
|
+
HttpControllerData,
|
|
26
|
+
HttpParamData,
|
|
27
|
+
HttpRouteData,
|
|
28
|
+
MetaEntry,
|
|
29
|
+
ParamCoercion,
|
|
30
|
+
ScalarCoercion,
|
|
31
|
+
SseRouteData,
|
|
32
|
+
UploadData,
|
|
33
|
+
} from './model.ts'
|
|
34
|
+
|
|
35
|
+
const ROUTE: DecoratorSpec = { targets: ['method'], params: ['static'] }
|
|
36
|
+
const NAME_OR_SCHEMA: DecoratorSpec = { targets: ['parameter'], params: ['value', 'value'] }
|
|
37
|
+
const NO_ARGUMENT: DecoratorSpec = { targets: ['parameter'], params: [] }
|
|
38
|
+
const CLASS_REFS: DecoratorSpec = { targets: ['class', 'method'], params: [], rest: 'value' }
|
|
39
|
+
const UPLOAD: DecoratorSpec = { targets: ['parameter'], params: ['static', 'static'] }
|
|
40
|
+
|
|
41
|
+
/** Decorators owned by the extension, by canonical identity (static / value arguments, §4.6). */
|
|
42
|
+
const decorators: Readonly<Record<string, DecoratorSpec>> = Object.freeze({
|
|
43
|
+
[Ids.Controller]: { targets: ['class'], params: ['static'] },
|
|
44
|
+
...Object.fromEntries([...ROUTE_METHODS.keys()].map(identity => [identity, ROUTE])),
|
|
45
|
+
[Ids.Param]: NAME_OR_SCHEMA,
|
|
46
|
+
[Ids.Query]: NAME_OR_SCHEMA,
|
|
47
|
+
[Ids.Body]: { targets: ['parameter'], params: ['value'] },
|
|
48
|
+
[Ids.Headers]: { targets: ['parameter'], params: ['static'] },
|
|
49
|
+
[Ids.Req]: NO_ARGUMENT,
|
|
50
|
+
[Ids.Res]: NO_ARGUMENT,
|
|
51
|
+
[Ids.Ctx]: NO_ARGUMENT,
|
|
52
|
+
[Ids.HttpCode]: { targets: ['method'], params: ['static'] },
|
|
53
|
+
[Ids.Header]: { targets: ['method'], params: ['static', 'static'] },
|
|
54
|
+
[Ids.UseGuard]: CLASS_REFS,
|
|
55
|
+
[Ids.UseInterceptor]: CLASS_REFS,
|
|
56
|
+
[Ids.Validate]: { targets: ['method'], params: ['value'] },
|
|
57
|
+
[Ids.UploadedFile]: UPLOAD,
|
|
58
|
+
[Ids.UploadedFiles]: UPLOAD,
|
|
59
|
+
[Ids.BodyLimit]: { targets: ['class', 'method'], params: ['static'] },
|
|
60
|
+
[Ids.Cookie]: NAME_OR_SCHEMA,
|
|
61
|
+
[Ids.SignedCookie]: NAME_OR_SCHEMA,
|
|
62
|
+
// after the ROUTE spread: `@Sse(path?, options?)` takes two static arguments
|
|
63
|
+
[Ids.Sse]: { targets: ['method'], params: ['static', 'static'] },
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
/** The `@znight/http` compiler extension. */
|
|
67
|
+
const http: CompilerExtension<HttpControllerData> = Object.freeze({
|
|
68
|
+
apiVersion: 1,
|
|
69
|
+
name: EXTENSION_NAME,
|
|
70
|
+
decorators,
|
|
71
|
+
analyzeClass: analyzeHttpClass,
|
|
72
|
+
validate: validateHttp,
|
|
73
|
+
generate: generateHttp,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export default http
|