@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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `validate` of the `@znight/http` compiler extension: route paths (grammar of `@znight/http/path`),
|
|
3
|
+
* `:param` ↔ `@Param` consistency and duplicate routes across every controller (ZNIGHT007); route
|
|
4
|
+
* decorator groups: consistent definitions (ZNIGHT005) and a decision of every required group on
|
|
5
|
+
* every route (ZNIGHT023).
|
|
6
|
+
*/
|
|
7
|
+
import type { Location, ValidateContext } from '@znight/compiler'
|
|
8
|
+
import type { RoutePath } from '../path.ts'
|
|
9
|
+
import type { HttpControllerData, HttpRouteData, MetaEntry } from './model.ts'
|
|
10
|
+
|
|
11
|
+
import { checkRoutePath, compareStrings, parseRoutePath, WILDCARD_PARAM } from '../path.ts'
|
|
12
|
+
import { Codes, Ids } from './model.ts'
|
|
13
|
+
import { parseRouteDecoratorOptions } from './route-decorators.ts'
|
|
14
|
+
|
|
15
|
+
type Context = ValidateContext<HttpControllerData>
|
|
16
|
+
|
|
17
|
+
interface RegisteredRoute {
|
|
18
|
+
readonly where: string
|
|
19
|
+
readonly route: HttpRouteData
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Validates the routes of every controller. */
|
|
23
|
+
export function validateHttp(ctx: Context): void {
|
|
24
|
+
const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token))
|
|
25
|
+
const registered = new Map<string, RegisteredRoute>()
|
|
26
|
+
for (const controller of controllers) {
|
|
27
|
+
const baseIssue = checkRoutePath(controller.rawBasePath)
|
|
28
|
+
if (baseIssue !== undefined) {
|
|
29
|
+
invalidRoute(
|
|
30
|
+
ctx,
|
|
31
|
+
controller.pathLocation,
|
|
32
|
+
`Invalid base path ${JSON.stringify(controller.rawBasePath)} of ${controller.displayName}: ${baseIssue.message}`,
|
|
33
|
+
GRAMMAR_HINTS,
|
|
34
|
+
)
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
for (const route of controller.routes) {
|
|
38
|
+
const where = `${controller.displayName}.${route.handler}`
|
|
39
|
+
const ownIssue = checkRoutePath(route.rawPath)
|
|
40
|
+
const issue = ownIssue ?? checkRoutePath(route.fullPath)
|
|
41
|
+
if (issue !== undefined) {
|
|
42
|
+
const path = ownIssue === undefined ? route.fullPath : route.rawPath
|
|
43
|
+
invalidRoute(
|
|
44
|
+
ctx,
|
|
45
|
+
route.pathLocation,
|
|
46
|
+
`Invalid route path ${JSON.stringify(path)} of ${where}: ${issue.message}`,
|
|
47
|
+
GRAMMAR_HINTS,
|
|
48
|
+
)
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
const parsed = parseRoutePath(route.fullPath)
|
|
52
|
+
checkParams(ctx, route, parsed, where)
|
|
53
|
+
const key = `${route.method} ${parsed.shape}`
|
|
54
|
+
const previous = registered.get(key)
|
|
55
|
+
if (previous === undefined) {
|
|
56
|
+
registered.set(key, { where, route })
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
ctx.report({
|
|
60
|
+
code: Codes.InvalidRoute,
|
|
61
|
+
severity: 'error',
|
|
62
|
+
message: `Duplicate route ${route.method} ${route.fullPath}: ${where} conflicts with ${previous.where} (${previous.route.method} ${previous.route.fullPath})`,
|
|
63
|
+
location: route.location,
|
|
64
|
+
hints: ['Two routes with the same method and the same path shape (parameter names aside) cannot coexist'],
|
|
65
|
+
related: [{ message: `${previous.where} is declared here`, location: previous.route.location }],
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
validateDecisions(ctx, controllers)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A route decorator definition seen during the compile (applied, or found by `definitionsOf`). */
|
|
73
|
+
interface Definition {
|
|
74
|
+
readonly identity: string
|
|
75
|
+
readonly name: string
|
|
76
|
+
readonly key: string
|
|
77
|
+
readonly group: string
|
|
78
|
+
readonly required: boolean
|
|
79
|
+
readonly definition: Location
|
|
80
|
+
/** A flag (`@Public()`): no type argument, or every usage without argument. */
|
|
81
|
+
flag: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Every route decorator definition, sorted by identity: the applied ones (first usage) and, when
|
|
86
|
+
* the compiler provides `definitionsOf`, the ones applied nowhere (invalid ones are left to the
|
|
87
|
+
* usages, which report them).
|
|
88
|
+
*/
|
|
89
|
+
function routeDecoratorDefinitions(ctx: Context, controllers: readonly HttpControllerData[]): Definition[] {
|
|
90
|
+
const definitions = new Map<string, Definition>()
|
|
91
|
+
const visit = (entries: readonly MetaEntry[]): void => {
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const known = definitions.get(entry.identity)
|
|
94
|
+
if (known === undefined) {
|
|
95
|
+
definitions.set(entry.identity, {
|
|
96
|
+
identity: entry.identity,
|
|
97
|
+
name: entry.decorator,
|
|
98
|
+
key: entry.key,
|
|
99
|
+
group: entry.group,
|
|
100
|
+
required: entry.required,
|
|
101
|
+
definition: entry.definition,
|
|
102
|
+
flag: entry.value === true,
|
|
103
|
+
})
|
|
104
|
+
} else if (entry.value !== true) {
|
|
105
|
+
known.flag = false
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const controller of controllers) {
|
|
110
|
+
visit(controller.meta)
|
|
111
|
+
for (const route of controller.routes) visit(route.meta)
|
|
112
|
+
}
|
|
113
|
+
// compilers before RFC 0001 have no definitionsOf: only applied decorators are known then
|
|
114
|
+
const all = typeof ctx.definitionsOf === 'function' ? ctx.definitionsOf(Ids.createRouteDecorator) : []
|
|
115
|
+
for (const found of all) {
|
|
116
|
+
const [keyArg, optionsArg, ...extra] = found.args
|
|
117
|
+
const key = keyArg?.evaluated === true ? keyArg.value : undefined
|
|
118
|
+
if (
|
|
119
|
+
typeof key !== 'string' ||
|
|
120
|
+
key === '' ||
|
|
121
|
+
extra.length > 0 ||
|
|
122
|
+
(optionsArg !== undefined && !optionsArg.evaluated)
|
|
123
|
+
) {
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
const parsed = parseRouteDecoratorOptions(key, optionsArg?.value)
|
|
127
|
+
if ('error' in parsed) {
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
const flag = found.typeArgs.length === 0
|
|
131
|
+
const known = definitions.get(found.identity)
|
|
132
|
+
if (known !== undefined) {
|
|
133
|
+
known.flag &&= flag
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
definitions.set(found.identity, {
|
|
137
|
+
identity: found.identity,
|
|
138
|
+
name: found.name,
|
|
139
|
+
key,
|
|
140
|
+
...parsed,
|
|
141
|
+
definition: found.location,
|
|
142
|
+
flag,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
return [...definitions.values()].sort((a, b) => compareStrings(a.identity, b.identity))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Route decorator groups (RFC 0001 §5.1): every definition of a key uses the same options and the
|
|
150
|
+
* decorators of a group agree on `required` (ZNIGHT005, at the second definition); every route has
|
|
151
|
+
* a decision of every required group, its own or its controller's (ZNIGHT023).
|
|
152
|
+
*/
|
|
153
|
+
function validateDecisions(ctx: Context, controllers: readonly HttpControllerData[]): void {
|
|
154
|
+
const definitions = routeDecoratorDefinitions(ctx, controllers)
|
|
155
|
+
if (definitions.length === 0) {
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
const byKey = new Map<string, Definition>()
|
|
159
|
+
const byGroup = new Map<string, Definition>()
|
|
160
|
+
const members = new Map<string, Definition[]>()
|
|
161
|
+
for (const definition of definitions) {
|
|
162
|
+
const sameKey = byKey.get(definition.key)
|
|
163
|
+
if (sameKey !== undefined && (sameKey.group !== definition.group || sameKey.required !== definition.required)) {
|
|
164
|
+
ctx.report({
|
|
165
|
+
code: Codes.InvalidDecorator,
|
|
166
|
+
severity: 'error',
|
|
167
|
+
message: `The route decorator ${definition.name} defines the key "${definition.key}" with ${describeOptions(definition)}, but ${sameKey.name} defines it with ${describeOptions(sameKey)}`,
|
|
168
|
+
location: definition.definition,
|
|
169
|
+
hints: ['Every definition of a key must use the same options: share one definition'],
|
|
170
|
+
related: [{ message: `${sameKey.name} is defined here`, location: sameKey.definition }],
|
|
171
|
+
})
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
const sameGroup = byGroup.get(definition.group)
|
|
175
|
+
if (sameGroup !== undefined && sameGroup.required !== definition.required) {
|
|
176
|
+
ctx.report({
|
|
177
|
+
code: Codes.InvalidDecorator,
|
|
178
|
+
severity: 'error',
|
|
179
|
+
message: `The "${definition.group}" group is ${definition.required ? '' : 'not '}required for ${definition.name} but ${sameGroup.required ? '' : 'not '}required for ${sameGroup.name}`,
|
|
180
|
+
location: definition.definition,
|
|
181
|
+
hints: [`All the decorators of the "${definition.group}" group must agree on required`],
|
|
182
|
+
related: [{ message: `${sameGroup.name} is defined here`, location: sameGroup.definition }],
|
|
183
|
+
})
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
byKey.set(definition.key, sameKey ?? definition)
|
|
187
|
+
byGroup.set(definition.group, sameGroup ?? definition)
|
|
188
|
+
const list = members.get(definition.group) ?? []
|
|
189
|
+
list.push(definition)
|
|
190
|
+
members.set(definition.group, list)
|
|
191
|
+
}
|
|
192
|
+
const required = [...byGroup.values()].filter(definition => definition.required)
|
|
193
|
+
if (required.length === 0) {
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
for (const controller of controllers) {
|
|
197
|
+
const controllerGroups = new Set(controller.meta.map(entry => entry.group))
|
|
198
|
+
for (const route of controller.routes) {
|
|
199
|
+
const decided = new Set([...controllerGroups, ...route.meta.map(entry => entry.group)])
|
|
200
|
+
for (const group of required) {
|
|
201
|
+
if (decided.has(group.group)) {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
// the decorators of the group in source order: `@Public(), @Authenticated() or @Roles(...)`
|
|
205
|
+
const inOrder = [...(members.get(group.group) ?? [group])].sort(
|
|
206
|
+
(a, b) => compareStrings(a.definition.file, b.definition.file) || a.definition.start - b.definition.start,
|
|
207
|
+
)
|
|
208
|
+
const decorators = inOrder.map(definition => `@${definition.name}(${definition.flag ? '' : '...'})`)
|
|
209
|
+
const listed =
|
|
210
|
+
decorators.length === 1
|
|
211
|
+
? (decorators[0] as string)
|
|
212
|
+
: `${decorators.slice(0, -1).join(', ')} or ${decorators.at(-1) as string}`
|
|
213
|
+
ctx.report({
|
|
214
|
+
code: Codes.MissingRouteDecision,
|
|
215
|
+
severity: 'error',
|
|
216
|
+
message: `Route ${route.method} ${route.fullPath} (${controller.displayName}.${route.handler}) has no "${group.group}" decision`,
|
|
217
|
+
location: route.location,
|
|
218
|
+
hints: [`Add ${listed} to the route or to ${controller.displayName}`],
|
|
219
|
+
related: [
|
|
220
|
+
{ message: `The "${group.group}" group is required`, location: (inOrder[0] ?? group).definition },
|
|
221
|
+
{ message: `${controller.displayName} is declared here`, location: controller.location },
|
|
222
|
+
],
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function describeOptions(entry: Definition): string {
|
|
230
|
+
const group = entry.group === entry.key ? 'no group' : `group "${entry.group}"`
|
|
231
|
+
return entry.required ? `${group}, required` : group
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const GRAMMAR_HINTS: readonly string[] = [
|
|
235
|
+
"Paths are made of static segments (letters, digits and - . _ ~ ! $ & ' + , ; = @), :name parameters and a trailing * wildcard",
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
/** `:param` of the path ↔ `@Param('name')` parameters of the handler. */
|
|
239
|
+
function checkParams(ctx: Context, route: HttpRouteData, path: RoutePath, where: string): void {
|
|
240
|
+
const declared = new Set(path.params)
|
|
241
|
+
const consumed = new Set<string>()
|
|
242
|
+
let everything = false
|
|
243
|
+
for (const param of route.params) {
|
|
244
|
+
if (param.source === 'context' || param.source === 'request') {
|
|
245
|
+
everything = true // ctx.params / the native request give access to every parameter
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
if (param.source !== 'param') {
|
|
249
|
+
continue
|
|
250
|
+
}
|
|
251
|
+
if (param.key === undefined) {
|
|
252
|
+
everything = true
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
if (declared.has(param.key)) {
|
|
256
|
+
consumed.add(param.key)
|
|
257
|
+
continue
|
|
258
|
+
}
|
|
259
|
+
const available = path.params.filter(name => name !== WILDCARD_PARAM).map(name => `:${name}`)
|
|
260
|
+
invalidRoute(
|
|
261
|
+
ctx,
|
|
262
|
+
param.location,
|
|
263
|
+
`@Param('${param.key}') of ${where}: the route ${route.method} ${route.fullPath} has no ${param.key === WILDCARD_PARAM ? 'wildcard' : `":${param.key}" parameter`}`,
|
|
264
|
+
[
|
|
265
|
+
available.length > 0
|
|
266
|
+
? `Parameters of the route: ${available.join(', ')}`
|
|
267
|
+
: `Add ":${param.key}" to the route path`,
|
|
268
|
+
],
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
if (everything || !route.complete) {
|
|
272
|
+
return // a parameter could not be analysed (already reported): do not guess what it consumes
|
|
273
|
+
}
|
|
274
|
+
for (const name of path.params) {
|
|
275
|
+
if (name !== WILDCARD_PARAM && !consumed.has(name)) {
|
|
276
|
+
invalidRoute(
|
|
277
|
+
ctx,
|
|
278
|
+
route.pathLocation,
|
|
279
|
+
`Route ${route.method} ${route.fullPath} declares ":${name}" but ${where} has no @Param('${name}') parameter`,
|
|
280
|
+
[`Add a parameter: @Param('${name}') ${name}: string`],
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function invalidRoute(ctx: Context, location: Location, message: string, hints: readonly string[]): void {
|
|
287
|
+
ctx.report({ code: Codes.InvalidRoute, severity: 'error', message, location, hints })
|
|
288
|
+
}
|