@znight/http 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +154 -35
- package/dist/compiler.d.mts +77 -2
- package/dist/compiler.mjs +259 -101
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.mjs +3 -2
- package/dist/express.mjs.map +1 -1
- package/dist/fastify.mjs +200 -23
- package/dist/fastify.mjs.map +1 -1
- package/dist/hono.mjs +174 -43
- package/dist/hono.mjs.map +1 -1
- package/dist/index.d.mts +36 -4
- package/dist/index.mjs +1005 -257
- package/dist/index.mjs.map +1 -1
- package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
- package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
- package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
- package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
- package/dist/node.mjs +1 -1
- package/dist/utils-DIWdzVht.mjs +46 -0
- package/dist/utils-DIWdzVht.mjs.map +1 -0
- package/package.json +4 -4
- package/src/adapters/express.ts +4 -1
- package/src/adapters/fastify.ts +265 -25
- package/src/adapters/hono.ts +225 -58
- package/src/adapters/node-io.ts +128 -58
- package/src/adapters/node.ts +4 -1
- package/src/body.ts +31 -2
- package/src/coerce.ts +31 -0
- package/src/compiler/analyze.ts +3 -3
- package/src/compiler/generate.ts +127 -22
- package/src/compiler/index.ts +13 -0
- package/src/compiler/route-decorators.ts +45 -2
- package/src/compiler/route-groups.ts +216 -0
- package/src/compiler/validate.ts +15 -126
- package/src/context.ts +148 -78
- package/src/decorators.ts +19 -3
- package/src/index.ts +2 -1
- package/src/pipeline.ts +936 -177
- package/src/plugin-routes.ts +19 -0
- package/src/plugin.ts +7 -3
- package/src/response.ts +6 -8
- package/src/serialize.ts +48 -21
- package/src/utils.ts +25 -2
- package/src/validate.ts +28 -12
- package/dist/utils-DCbu3a4w.mjs +0 -27
- package/dist/utils-DCbu3a4w.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1222,8 +1222,18 @@ export class AdminController {
|
|
|
1222
1222
|
HttpPlugin.configure({ guards: [AuthGuard] })
|
|
1223
1223
|
```
|
|
1224
1224
|
|
|
1225
|
-
`decorator.get(ctx)` reads the route metadata (`ctx.route.meta`) with its type.
|
|
1226
|
-
|
|
1225
|
+
`decorator.get(ctx)` reads the route metadata (`ctx.route.meta`) with its type. It accepts any
|
|
1226
|
+
`RouteMetaCarrier` (`{ route?: { meta } }`), not only an `HttpContext`: the contexts of other
|
|
1227
|
+
transports that reuse route decorators (a GraphQL resolver, a WebSocket gateway) expose their
|
|
1228
|
+
effective metadata the same way, so one guard helper serves every transport:
|
|
1229
|
+
|
|
1230
|
+
```ts
|
|
1231
|
+
import type { RouteMetaCarrier } from '@znight/http'
|
|
1232
|
+
|
|
1233
|
+
const isPublic = (ctx: RouteMetaCarrier): boolean => Public.get(ctx) === true
|
|
1234
|
+
```
|
|
1235
|
+
|
|
1236
|
+
Guards, interceptors and mappers are providers: they may be request-scoped (resolved per request) and have
|
|
1227
1237
|
dependencies. Plugin options also accept instances (`guards: [new IpAllowList(['10.0.0.0/8'])]`).
|
|
1228
1238
|
|
|
1229
1239
|
## Route decisions
|
|
@@ -1493,6 +1503,16 @@ app.hooks.wrap('http:request', async (event, next) => {
|
|
|
1493
1503
|
stream, `run` resolves when the stream opens (`status: 200`): durations measure the time to open.
|
|
1494
1504
|
A stream-lifetime hook is not provided.
|
|
1495
1505
|
|
|
1506
|
+
When CORS is off, routes (other than `@All`) without guards, interceptors, `@Sse`, `@Res()` or
|
|
1507
|
+
`@Header` take a fast lane: the plugin decides at boot what the pipeline would do for them, runs a
|
|
1508
|
+
synchronous handler without creating a promise and builds the headers of a JSON reply from a
|
|
1509
|
+
template. The replies are the same as through the full pipeline (a thrown error, an `HttpResponse`
|
|
1510
|
+
or anything unusual continues there, in the same request scope; a promise is awaited first). The
|
|
1511
|
+
lane runs the `http:request` hooks too: listeners keep it synchronous, and wrappers (such as those of
|
|
1512
|
+
`@znight/observability`) see the same event, in the same request scope and async context, as with
|
|
1513
|
+
the full pipeline; a reply whose wrappers added response headers or cookies is completed like any
|
|
1514
|
+
other.
|
|
1515
|
+
|
|
1496
1516
|
## Plugin options
|
|
1497
1517
|
|
|
1498
1518
|
`HttpPlugin.configure(options)` — `options` may also be a function of the container, evaluated
|
|
@@ -1579,6 +1599,7 @@ export class StatusPlugin implements Plugin, PluginHttpRoutes {
|
|
|
1579
1599
|
handler: ctx => ({ up: true }), // (ctx: HttpContext) => result, serialised like a controller result
|
|
1580
1600
|
meta: [Public.meta()], // route metadata: read by guards, checked by the route decision check
|
|
1581
1601
|
guards: [], // extra guards (classes or instances)
|
|
1602
|
+
bodyLimit: '100kb', // optional: this route's body limit, instead of the HttpPlugin bodyLimit
|
|
1582
1603
|
},
|
|
1583
1604
|
]
|
|
1584
1605
|
}
|
|
@@ -1591,9 +1612,12 @@ export class StatusPlugin implements Plugin, PluginHttpRoutes {
|
|
|
1591
1612
|
then the route `guards`, the handler and the exception mappers. Global interceptors do not wrap
|
|
1592
1613
|
plugin routes (a response envelope would break a document). The body is read lazily
|
|
1593
1614
|
(`ctx.parseBody()`). `GET` routes answer `HEAD`; `ALL` works as for controllers.
|
|
1615
|
+
- `bodyLimit` (a `ByteSize`: `1048576`, `'100kb'`, `Infinity`, the grammar of the plugin option)
|
|
1616
|
+
replaces the `HttpPlugin` `bodyLimit` for that route only, lower or higher, like `@BodyLimit` on a
|
|
1617
|
+
controller route: reading a larger body answers 413. Without it, the plugin limit applies.
|
|
1594
1618
|
- `meta` entries (`decorator.meta(value)`, one per group) are the route metadata. A required group
|
|
1595
1619
|
needs a decision there too: without one the boot fails (ZNIGHT023), with a hint naming the plugin.
|
|
1596
|
-
- An invalid route (method, path grammar, name, handler, meta) or a collision with another route is
|
|
1620
|
+
- An invalid route (method, path grammar, name, handler, meta, body limit) or a collision with another route is
|
|
1597
1621
|
ZNIGHT007 naming the plugin. `HttpPlugin.routes` lists the routes with `source: '<plugin name>'`.
|
|
1598
1622
|
|
|
1599
1623
|
## Adapters
|
|
@@ -1638,8 +1662,18 @@ HttpPlugin.configure({ adapter: fastifyAdapter({ instance }) })
|
|
|
1638
1662
|
|
|
1639
1663
|
Znight routes are registered in an encapsulated context with their own body handling and 404
|
|
1640
1664
|
handler, so your other routes keep Fastify's parsers. A `content-type` Fastify cannot parse (`foo`,
|
|
1641
|
-
an empty one)
|
|
1642
|
-
instead of Fastify's `415`.
|
|
1665
|
+
an empty one) never reaches Fastify's parsers for Znight routes, which then answer like on the other
|
|
1666
|
+
adapters instead of Fastify's `415`.
|
|
1667
|
+
|
|
1668
|
+
Znight parses bodies itself, so a Znight route that no other hook needs is served straight from the
|
|
1669
|
+
`onRequest` hook of its context: Fastify skips its content type parsing and the steps before the
|
|
1670
|
+
handler, and a text reply is written on the raw response exactly as `reply.send()` writes it. Your
|
|
1671
|
+
hooks always run: with a `preParsing`, `preValidation` or `preHandler` hook (or an `onRoute` hook
|
|
1672
|
+
wrapping the handler, a schema, a subscriber of the `fastify.request.handler` tracing channel), the
|
|
1673
|
+
request goes through Fastify's whole lifecycle; with an `onSend` or `onResponse` hook, or headers
|
|
1674
|
+
and trailers set by an `onRequest` hook, the reply goes through `reply.send()`. The adapter reads
|
|
1675
|
+
these hooks on Fastify's internal route and reply state: should a Fastify release change it, every
|
|
1676
|
+
request goes through the lifecycle (slower, same responses). Created instances use
|
|
1643
1677
|
`exposeHeadRoutes: false` (Znight serves HEAD), a `maxParamLength` of 8 KiB and a
|
|
1644
1678
|
`frameworkErrors` handler rendering Znight errors (with your own instance, malformed URLs are
|
|
1645
1679
|
answered by Fastify). `inject()` uses `fastify.inject()`. Event streams are sent through
|
|
@@ -1695,10 +1729,13 @@ handler returning its own `Response`.
|
|
|
1695
1729
|
| `serveOptions` | Options of `Bun.serve()` (`tls`, `idleTimeout`, `development`, `error`...; Znight sets `port`, `hostname`, `fetch`). Bun only |
|
|
1696
1730
|
|
|
1697
1731
|
On Bun, the body limits are Znight's (`bodyLimit`, `@BodyLimit`): `maxRequestBodySize` defaults to
|
|
1698
|
-
unlimited instead of Bun's 128 MiB.
|
|
1732
|
+
unlimited instead of Bun's 128 MiB. A body with a `Content-Length` is read at once
|
|
1733
|
+
(`request.bytes()`, refused with 413 before reading when the length is above the limit), a chunked
|
|
1734
|
+
one as a stream (413 as soon as it passes the limit). Bun's `idleTimeout` (10 s by default) still closes idle
|
|
1699
1735
|
connections and stalled requests, but is lifted while a streamed response is sent (SSE, slow
|
|
1700
1736
|
producers), then restored. In your own `Bun.serve({ fetch: adapter.fetch })` the adapter cannot
|
|
1701
|
-
lift it: keep the SSE `heartbeat` below `idleTimeout` (the 8 s default does).
|
|
1737
|
+
lift it: keep the SSE `heartbeat` below `idleTimeout` (the 8 s default does). Bun checks idle timeouts
|
|
1738
|
+
every 4 seconds, so an `idleTimeout` of 4 s or less closes a connection even while it is written to.
|
|
1702
1739
|
|
|
1703
1740
|
`Bun.serve()` sends streamed bodies with chunked transfer encoding even when a `Content-Length`
|
|
1704
1741
|
header is set (unless the whole stream is available at once): files sent with
|
|
@@ -2084,7 +2121,8 @@ Rules:
|
|
|
2084
2121
|
boot.
|
|
2085
2122
|
- `@Validate(S)` applies `S` to the `@Body()` parameter (`@Validate(S)` + `@Body()` ≡ `@Body(S)`);
|
|
2086
2123
|
`@Validate(S)` + `@Body(S2)` is ZNIGHT005. Without a `@Body()` parameter the body is still
|
|
2087
|
-
validated (and discarded): `invoke: async (c, ctx) => { await validate(S, await ctx.parseBody(), 'body'); return c.m() }
|
|
2124
|
+
validated (and discarded): `invoke: async (c, ctx) => { await validate(S, await ctx.parseBody(), 'body'); return c.m() }`
|
|
2125
|
+
(this `async` arrow `await`s its other validations too).
|
|
2088
2126
|
- The plugin parses the body before the interceptors when `params` contains a `source: 'body'`
|
|
2089
2127
|
entry or `uploads` is not empty; `ctx.body` is `undefined` otherwise (generated code then uses
|
|
2090
2128
|
`await ctx.parseBody()`).
|
|
@@ -2117,31 +2155,53 @@ Rules:
|
|
|
2117
2155
|
|
|
2118
2156
|
### `invoke` generation
|
|
2119
2157
|
|
|
2120
|
-
`invoke(controller, ctx)` extracts every argument in parameter order and calls the handler. It
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2158
|
+
`invoke(controller, ctx)` extracts every argument in parameter order and calls the handler. It
|
|
2159
|
+
returns the handler result as is (promises included); the plugin awaits it only when it is a
|
|
2160
|
+
thenable. Without schema it is a synchronous arrow:
|
|
2161
|
+
`(c: UserController, ctx: HttpContext) => c.find(coerce.number(ctx.params['id'], 'param "id"'))`.
|
|
2162
|
+
|
|
2163
|
+
With schemas it stays synchronous as long as every `validate()` returns a value (Zod, Valibot or
|
|
2164
|
+
ArkType schemas without async refinements): no promise, no `await` per request. The arguments up to
|
|
2165
|
+
the last validated one are evaluated in order into `a<index>` constants (`@Ctx()` stays `ctx`), the
|
|
2166
|
+
later ones are inlined in the call, and each validated argument is followed by a fallback for
|
|
2167
|
+
asynchronous schemas:
|
|
2168
|
+
|
|
2169
|
+
```ts
|
|
2170
|
+
invoke: (c: UserController, ctx: HttpContext) => {
|
|
2171
|
+
const a0 = coerce.number(ctx.params['id'], 'param "id"')
|
|
2172
|
+
const a1 = validate(UpdateUser, ctx.body, 'body')
|
|
2173
|
+
if (isThenable(a1)) return (async () => c.update(a0, await a1, ctx))()
|
|
2174
|
+
return c.update(a0, a1, ctx)
|
|
2175
|
+
},
|
|
2176
|
+
```
|
|
2177
|
+
|
|
2178
|
+
When `validate()` returns a thenable (an async refinement, or an output that is itself a promise),
|
|
2179
|
+
the `async` arrow awaits it, then evaluates the remaining arguments (`await`ing their validations)
|
|
2180
|
+
and calls the handler: the order of evaluation, the error of the first invalid argument and the
|
|
2181
|
+
values received by the handler are those of an `async` invoke `await`ing every `validate()`. The
|
|
2182
|
+
expressions of the table below are `validate(S, ...)` calls in both forms.
|
|
2183
|
+
|
|
2184
|
+
| Parameter | Generated expression |
|
|
2185
|
+
| --------------------------------------- | ----------------------------------------------------------------------------- |
|
|
2186
|
+
| `@Param('id') id: T` | `coerce.X(ctx.params['id'], 'param "id"')` (see below) |
|
|
2187
|
+
| `@Param('id', S)` | `validate(S, ctx.params['id'], 'param "id"')` |
|
|
2188
|
+
| `@Param()` | `ctx.params as never` · with schema: `validate(S, ctx.params, 'params')` |
|
|
2189
|
+
| `@Query('page') page: T` | `coerce.X(ctx.query['page'], 'query "page"')` |
|
|
2190
|
+
| `@Query('page', S)` | `validate(S, ctx.query['page'], 'query "page"')` |
|
|
2191
|
+
| `@Query()` / `@Query(S)` | `ctx.query as never` / `validate(S, ctx.query, 'query')` |
|
|
2192
|
+
| `@Body()` / `@Body(S)` / `@Validate(S)` | `ctx.body as never` / `validate(S, ctx.body, 'body')` |
|
|
2193
|
+
| `@Headers('X-Api-Key') key: T` | `coerce.X(ctx.headers['x-api-key'], 'header "x-api-key"')` (name lower-cased) |
|
|
2194
|
+
| `@Headers()` | `ctx.headers as never` |
|
|
2195
|
+
| `@Cookie('sid') sid: T` | `coerce.X(ctx.cookies['sid'], 'cookie "sid"')` (or `coerce.optional(...)`) |
|
|
2196
|
+
| `@Cookie('sid', S)` | `validate(S, ctx.cookies['sid'], 'cookie "sid"')` |
|
|
2197
|
+
| `@Cookie()` / `@Cookie(S)` | `ctx.cookies as never` / `validate(S, ctx.cookies, 'cookies')` |
|
|
2198
|
+
| `@SignedCookie('sid') sid: T` | `coerce.X(ctx.signedCookie('sid'), 'cookie "sid"')` |
|
|
2199
|
+
| `@SignedCookie('sid', S)` | `validate(S, ctx.signedCookie('sid'), 'cookie "sid"')` |
|
|
2200
|
+
| `@Req()` / `@Res()` | `ctx.request as never` / `ctx.response as never` (+ `rawResponse: true`) |
|
|
2201
|
+
| `@UploadedFile('f') f: File` | `uploads.file(ctx, 'f')` · optional: `uploads.optionalFile(ctx, 'f')` |
|
|
2202
|
+
| `@UploadedFiles('f') f: File[]` | `uploads.files(ctx, 'f')` · optional: `uploads.optionalFiles(ctx, 'f')` |
|
|
2203
|
+
| `@UploadedFile()` / `@UploadedFiles()` | the same helpers with `undefined` as field (any field) |
|
|
2204
|
+
| `@Ctx()` | `ctx` |
|
|
2145
2205
|
|
|
2146
2206
|
`coerce.X` by declared type (`T`):
|
|
2147
2207
|
|
|
@@ -2187,6 +2247,8 @@ function validate<S extends StandardSchemaV1>(
|
|
|
2187
2247
|
location: string,
|
|
2188
2248
|
): InferSchemaOutput<S> | Promise<InferSchemaOutput<S>>
|
|
2189
2249
|
|
|
2250
|
+
function isThenable(value: unknown): value is PromiseLike<unknown> // a `then` method: validate() result to await
|
|
2251
|
+
|
|
2190
2252
|
const uploads: {
|
|
2191
2253
|
file(ctx: HttpContext, field: string | undefined): File // 400 `Missing file "field"` when absent
|
|
2192
2254
|
optionalFile(ctx: HttpContext, field: string | undefined): File | undefined
|
|
@@ -2199,8 +2261,9 @@ Every coercion failure throws `BadRequestException` (400); `validate` throws `Va
|
|
|
2199
2261
|
(400, `details: [{ path, message }]`) and `TypeError` when `schema` is not a Standard Schema; the
|
|
2200
2262
|
`uploads` helpers read `ctx.files` (`undefined` field: every field) and throw `BadRequestException`
|
|
2201
2263
|
for missing required files (`Expected a file in field "f", received a text value` when the field
|
|
2202
|
-
was sent as text).
|
|
2203
|
-
|
|
2264
|
+
was sent as text). `validate` returns the output synchronously when the schema validates
|
|
2265
|
+
synchronously, a promise otherwise. Generated modules import them with
|
|
2266
|
+
`import { coerce, isThenable, uploads, validate } from '@znight/http'` and the context type with
|
|
2204
2267
|
`import type { HttpContext } from '@znight/http'`.
|
|
2205
2268
|
|
|
2206
2269
|
### Manifest
|
|
@@ -2246,6 +2309,62 @@ Controllers are stored in identity order of the classes; sort them by `token` fo
|
|
|
2246
2309
|
output. The effective metadata of a route is the controller's entries whose `group` has no entry on
|
|
2247
2310
|
the route, then the route's entries.
|
|
2248
2311
|
|
|
2312
|
+
### Route metadata in other extensions
|
|
2313
|
+
|
|
2314
|
+
Extensions whose own targets carry route decorators (WebSocket gateways, GraphQL resolvers) read
|
|
2315
|
+
them with the functions of `@znight/http/compiler`, which apply exactly the rules of controllers and
|
|
2316
|
+
routes (RFC 0001 groups). They are stable API, like the types above, and the entry stays
|
|
2317
|
+
runtime-free (it never loads the HTTP runtime):
|
|
2318
|
+
|
|
2319
|
+
| Function | What it does |
|
|
2320
|
+
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2321
|
+
| `readRouteMeta(ctx, cls, method?)` | The `MetaEntry[]` of the route decorators applied to a class (`method` omitted) or to one of its methods, in source order. Works with the `AnalyzeContext` of any extension. Problems (uncalled decorator, two decisions of one group...) are reported as ZNIGHT005, once per call: read each target once |
|
|
2322
|
+
| `effectiveRouteMeta(classMeta, own)` | The effective metadata of a method: the class entries whose group the method does not decide, then the method entries (the rule of `ctx.route.meta`) |
|
|
2323
|
+
| `requiredRouteGroups(ctx, applied?)` | The `RequiredRouteGroup[]` of the application (in the `ValidateContext` of any extension): every group declared `required`, applied anywhere or not. `applied` are the entries the caller read. Inconsistent definitions are left out (the http extension reports them) |
|
|
2324
|
+
|
|
2325
|
+
A `RequiredRouteGroup` has `group`, `decorators` (`RouteGroupMember[]`, source order: `identity`,
|
|
2326
|
+
`name`, `key`, `group`, `required`, `definition`, `flag`) and `suggestion`
|
|
2327
|
+
(`@Public(), @Authenticated() or @Roles(...)`). A target of the extension without a decision of a
|
|
2328
|
+
required group is **ZNIGHT023**, reported by the extension itself:
|
|
2329
|
+
|
|
2330
|
+
```ts
|
|
2331
|
+
import type { CompilerExtension, Location } from '@znight/compiler'
|
|
2332
|
+
import type { MetaEntry } from '@znight/http/compiler'
|
|
2333
|
+
|
|
2334
|
+
import { readRouteMeta, requiredRouteGroups } from '@znight/http/compiler'
|
|
2335
|
+
|
|
2336
|
+
interface GatewayData {
|
|
2337
|
+
readonly name: string
|
|
2338
|
+
readonly meta: readonly MetaEntry[] // the class entries (methods: effectiveRouteMeta(classMeta, own))
|
|
2339
|
+
readonly location: Location
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
const ws: CompilerExtension<GatewayData> = {
|
|
2343
|
+
apiVersion: 1,
|
|
2344
|
+
name: 'ws',
|
|
2345
|
+
decorators: { '@acme/ws#Gateway': { targets: ['class'], params: [] } },
|
|
2346
|
+
analyzeClass(cls, ctx) {
|
|
2347
|
+
ctx.store({ name: cls.displayName, meta: readRouteMeta(ctx, cls), location: cls.location })
|
|
2348
|
+
},
|
|
2349
|
+
validate(ctx) {
|
|
2350
|
+
const required = requiredRouteGroups(
|
|
2351
|
+
ctx,
|
|
2352
|
+
ctx.stored.flatMap(gateway => gateway.meta),
|
|
2353
|
+
)
|
|
2354
|
+
for (const gateway of ctx.stored)
|
|
2355
|
+
for (const group of required)
|
|
2356
|
+
if (!gateway.meta.some(entry => entry.group === group.group))
|
|
2357
|
+
ctx.report({
|
|
2358
|
+
code: 'ZNIGHT023',
|
|
2359
|
+
severity: 'error',
|
|
2360
|
+
message: `${gateway.name} has no "${group.group}" decision`,
|
|
2361
|
+
location: gateway.location,
|
|
2362
|
+
hints: [`Add ${group.suggestion} to ${gateway.name}`],
|
|
2363
|
+
})
|
|
2364
|
+
},
|
|
2365
|
+
}
|
|
2366
|
+
```
|
|
2367
|
+
|
|
2249
2368
|
## Writing an adapter
|
|
2250
2369
|
|
|
2251
2370
|
An adapter implements `HttpAdapter` (exported types): it routes requests to the handlers given by
|
package/dist/compiler.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _ as HttpMethod, y as HttpParamSource } from "./adapter-J0UXFnNM.mjs";
|
|
2
|
-
import { ClassRef, CompilerExtension, Location, StaticValue, ValueRef } from "@znight/compiler";
|
|
2
|
+
import { AnalyzeContext, ClassInfo, ClassRef, CompilerExtension, Location, MethodInfo, StaticValue, ValidateContext, ValueRef } from "@znight/compiler";
|
|
3
3
|
/** Primitive coercions (`coerce.<kind>`). */
|
|
4
4
|
type ScalarCoercion = "string" | "number" | "integer" | "boolean" | "bigint" | "date";
|
|
5
5
|
/**
|
|
@@ -150,7 +150,82 @@ interface HttpControllerData {
|
|
|
150
150
|
/** Location of the base path argument (the decorator when absent). */
|
|
151
151
|
readonly pathLocation: Location;
|
|
152
152
|
}
|
|
153
|
+
/** A route decorator definition of the application (`export const Roles = createRouteDecorator(...)`). */
|
|
154
|
+
interface RouteGroupMember {
|
|
155
|
+
/** Canonical identity of the decorator `const` (`src/auth/access.ts#Roles`). */
|
|
156
|
+
readonly identity: string;
|
|
157
|
+
/** Name of the `const` (`Roles`). */
|
|
158
|
+
readonly name: string;
|
|
159
|
+
readonly key: string;
|
|
160
|
+
/** Decision group (`options.group ?? key`). */
|
|
161
|
+
readonly group: string;
|
|
162
|
+
readonly required: boolean;
|
|
163
|
+
/** Location of the definition. */
|
|
164
|
+
readonly definition: Location;
|
|
165
|
+
/**
|
|
166
|
+
* A flag (`@Public()`): created without type argument, and applied without argument wherever it
|
|
167
|
+
* was seen. Decides how the decorator is suggested: `@Public()` or `@Roles(...)`.
|
|
168
|
+
*/
|
|
169
|
+
readonly flag: boolean;
|
|
170
|
+
}
|
|
171
|
+
/** A required route decorator group: every route-like target must carry a decision of it. */
|
|
172
|
+
interface RequiredRouteGroup {
|
|
173
|
+
/** Group name (`access`). */
|
|
174
|
+
readonly group: string;
|
|
175
|
+
/** The decorators of the group, in source order (file, then position). Never empty. */
|
|
176
|
+
readonly decorators: readonly RouteGroupMember[];
|
|
177
|
+
/** The decorators as a hint: `@Public(), @Authenticated() or @Roles(...)`. */
|
|
178
|
+
readonly suggestion: string;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The required route decorator groups of the application (RFC 0001): what every route-like target
|
|
182
|
+
* of another extension (a gateway, a GraphQL root field) must decide, exactly as HTTP routes must.
|
|
183
|
+
* A target without a decision of a returned group is **ZNIGHT023**, reported by the caller.
|
|
184
|
+
*
|
|
185
|
+
* `applied` are the route metadata entries the caller read on its own targets
|
|
186
|
+
* ({@link readRouteMeta}): they make decorators known even when the compiler has no
|
|
187
|
+
* `definitionsOf`, and refine how flags are suggested. Inconsistent definitions are left out and
|
|
188
|
+
* not reported here: the http extension reports them (ZNIGHT005) once.
|
|
189
|
+
*
|
|
190
|
+
* ```ts
|
|
191
|
+
* validate(ctx) {
|
|
192
|
+
* const required = requiredRouteGroups(ctx, ctx.stored.flatMap(gateway => gateway.meta))
|
|
193
|
+
* for (const gateway of ctx.stored)
|
|
194
|
+
* for (const group of required)
|
|
195
|
+
* if (!gateway.meta.some(entry => entry.group === group.group))
|
|
196
|
+
* ctx.report({ code: 'ZNIGHT023', severity: 'error', message: `${gateway.name} has no "${group.group}" decision`,
|
|
197
|
+
* location: gateway.location, hints: [`Add ${group.suggestion} to ${gateway.name}`] })
|
|
198
|
+
* }
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
export declare function requiredRouteGroups(ctx: ValidateContext<unknown>, applied?: Iterable<MetaEntry>): readonly RequiredRouteGroup[];
|
|
202
|
+
/**
|
|
203
|
+
* The route metadata entries of a class (`method` omitted) or of one of its methods: the route
|
|
204
|
+
* decorators (`createRouteDecorator` consts) applied to that target, in source order, read exactly
|
|
205
|
+
* as the http extension reads controllers and routes (keys, statically evaluated values, groups,
|
|
206
|
+
* `required`). Problems are reported as ZNIGHT005 through `ctx`, once per call: read each target
|
|
207
|
+
* once. Decorators that are not route decorators are ignored.
|
|
208
|
+
*
|
|
209
|
+
* Works with the analysis context of any extension. The entries of a target are its own: combine a
|
|
210
|
+
* class and a method with {@link effectiveRouteMeta}.
|
|
211
|
+
*
|
|
212
|
+
* ```ts
|
|
213
|
+
* analyzeClass(cls, ctx) {
|
|
214
|
+
* const classMeta = readRouteMeta(ctx, cls)
|
|
215
|
+
* for (const method of cls.methods) {
|
|
216
|
+
* const meta = effectiveRouteMeta(classMeta, readRouteMeta(ctx, cls, method))
|
|
217
|
+
* }
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
export declare function readRouteMeta(ctx: AnalyzeContext<unknown>, cls: ClassInfo, method?: MethodInfo): MetaEntry[];
|
|
222
|
+
/**
|
|
223
|
+
* The effective metadata of a method from the entries of its class and its own (RFC 0001 §5.1):
|
|
224
|
+
* the class entries, except those of a group the method decides itself, then the method entries.
|
|
225
|
+
* The same rule as `ctx.route.meta` at runtime.
|
|
226
|
+
*/
|
|
227
|
+
export declare function effectiveRouteMeta(classMeta: readonly MetaEntry[], methodMeta: readonly MetaEntry[]): MetaEntry[];
|
|
153
228
|
/** The `@znight/http` compiler extension. */
|
|
154
229
|
declare const http: CompilerExtension<HttpControllerData>;
|
|
155
|
-
export { type CoercionPlan, type HttpControllerData, type HttpParamData, type HttpRouteData, type MetaEntry, type ParamCoercion, type ScalarCoercion, type SseRouteData, type UploadData, http as default };
|
|
230
|
+
export { type CoercionPlan, type HttpControllerData, type HttpParamData, type HttpRouteData, type MetaEntry, type ParamCoercion, type RequiredRouteGroup, type RouteGroupMember, type ScalarCoercion, type SseRouteData, type UploadData, http as default };
|
|
156
231
|
//# sourceMappingURL=compiler.d.mts.map
|