@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.
Files changed (46) hide show
  1. package/README.md +154 -35
  2. package/dist/compiler.d.mts +77 -2
  3. package/dist/compiler.mjs +259 -101
  4. package/dist/compiler.mjs.map +1 -1
  5. package/dist/express.mjs +3 -2
  6. package/dist/express.mjs.map +1 -1
  7. package/dist/fastify.mjs +200 -23
  8. package/dist/fastify.mjs.map +1 -1
  9. package/dist/hono.mjs +174 -43
  10. package/dist/hono.mjs.map +1 -1
  11. package/dist/index.d.mts +36 -4
  12. package/dist/index.mjs +1005 -257
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/{node-Clm1Ia7T.mjs → node-DnYv_AMI.mjs} +4 -3
  15. package/dist/{node-Clm1Ia7T.mjs.map → node-DnYv_AMI.mjs.map} +1 -1
  16. package/dist/{node-io-CVnqpVNS.mjs → node-io-BkdRSQeg.mjs} +99 -60
  17. package/dist/{node-io-CVnqpVNS.mjs.map → node-io-BkdRSQeg.mjs.map} +1 -1
  18. package/dist/node.mjs +1 -1
  19. package/dist/utils-DIWdzVht.mjs +46 -0
  20. package/dist/utils-DIWdzVht.mjs.map +1 -0
  21. package/package.json +4 -4
  22. package/src/adapters/express.ts +4 -1
  23. package/src/adapters/fastify.ts +265 -25
  24. package/src/adapters/hono.ts +225 -58
  25. package/src/adapters/node-io.ts +128 -58
  26. package/src/adapters/node.ts +4 -1
  27. package/src/body.ts +31 -2
  28. package/src/coerce.ts +31 -0
  29. package/src/compiler/analyze.ts +3 -3
  30. package/src/compiler/generate.ts +127 -22
  31. package/src/compiler/index.ts +13 -0
  32. package/src/compiler/route-decorators.ts +45 -2
  33. package/src/compiler/route-groups.ts +216 -0
  34. package/src/compiler/validate.ts +15 -126
  35. package/src/context.ts +148 -78
  36. package/src/decorators.ts +19 -3
  37. package/src/index.ts +2 -1
  38. package/src/pipeline.ts +936 -177
  39. package/src/plugin-routes.ts +19 -0
  40. package/src/plugin.ts +7 -3
  41. package/src/response.ts +6 -8
  42. package/src/serialize.ts +48 -21
  43. package/src/utils.ts +25 -2
  44. package/src/validate.ts +28 -12
  45. package/dist/utils-DCbu3a4w.mjs +0 -27
  46. 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. Guards,
1226
- interceptors and mappers are providers: they may be request-scoped (resolved per request) and have
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) is hidden from Fastify for Znight routes, which then answer like on the other adapters
1642
- instead of Fastify's `415`. Created instances use
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. Bun's `idleTimeout` (10 s by default) still closes idle
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 is
2121
- `async` and `await`s every `validate()` call when the route has at least one schema; otherwise it
2122
- is synchronous. It returns the handler result as is (promises included).
2123
-
2124
- | Parameter | Generated expression |
2125
- | --------------------------------------- | ------------------------------------------------------------------------------ |
2126
- | `@Param('id') id: T` | `coerce.X(ctx.params['id'], 'param "id"')` (see below) |
2127
- | `@Param('id', S)` | `await validate(S, ctx.params['id'], 'param "id"')` |
2128
- | `@Param()` | `ctx.params as never` · with schema: `await validate(S, ctx.params, 'params')` |
2129
- | `@Query('page') page: T` | `coerce.X(ctx.query['page'], 'query "page"')` |
2130
- | `@Query('page', S)` | `await validate(S, ctx.query['page'], 'query "page"')` |
2131
- | `@Query()` / `@Query(S)` | `ctx.query as never` / `await validate(S, ctx.query, 'query')` |
2132
- | `@Body()` / `@Body(S)` / `@Validate(S)` | `ctx.body as never` / `await validate(S, ctx.body, 'body')` |
2133
- | `@Headers('X-Api-Key') key: T` | `coerce.X(ctx.headers['x-api-key'], 'header "x-api-key"')` (name lower-cased) |
2134
- | `@Headers()` | `ctx.headers as never` |
2135
- | `@Cookie('sid') sid: T` | `coerce.X(ctx.cookies['sid'], 'cookie "sid"')` (or `coerce.optional(...)`) |
2136
- | `@Cookie('sid', S)` | `await validate(S, ctx.cookies['sid'], 'cookie "sid"')` |
2137
- | `@Cookie()` / `@Cookie(S)` | `ctx.cookies as never` / `await validate(S, ctx.cookies, 'cookies')` |
2138
- | `@SignedCookie('sid') sid: T` | `coerce.X(ctx.signedCookie('sid'), 'cookie "sid"')` |
2139
- | `@SignedCookie('sid', S)` | `await validate(S, ctx.signedCookie('sid'), 'cookie "sid"')` |
2140
- | `@Req()` / `@Res()` | `ctx.request as never` / `ctx.response as never` (+ `rawResponse: true`) |
2141
- | `@UploadedFile('f') f: File` | `uploads.file(ctx, 'f')` · optional: `uploads.optionalFile(ctx, 'f')` |
2142
- | `@UploadedFiles('f') f: File[]` | `uploads.files(ctx, 'f')` · optional: `uploads.optionalFiles(ctx, 'f')` |
2143
- | `@UploadedFile()` / `@UploadedFiles()` | the same helpers with `undefined` as field (any field) |
2144
- | `@Ctx()` | `ctx` |
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). Generated modules import them with
2203
- `import { coerce, uploads, validate } from '@znight/http'` and the context type with
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
@@ -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