@znight/http 0.2.2 → 0.4.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/README.md +51 -2
- package/dist/{adapter-J0UXFnNM.d.mts → adapter-DmhQGqAR.d.mts} +18 -5
- package/dist/compiler.d.mts +23 -1
- package/dist/compiler.mjs +126 -9
- package/dist/compiler.mjs.map +1 -1
- package/dist/express.d.mts +1 -1
- package/dist/fastify.d.mts +1 -1
- package/dist/hono.d.mts +1 -1
- package/dist/index.d.mts +9 -1
- package/dist/index.mjs +54 -8
- package/dist/index.mjs.map +1 -1
- package/dist/node.d.mts +1 -1
- package/package.json +4 -4
- package/src/compiler/analyze.ts +43 -0
- package/src/compiler/generate.ts +122 -13
- package/src/compiler/model.ts +21 -0
- package/src/context.ts +7 -0
- package/src/metadata.ts +13 -4
- package/src/pipeline.ts +12 -7
- package/src/plugin-routes.ts +34 -1
- package/src/plugin.ts +39 -2
package/README.md
CHANGED
|
@@ -236,6 +236,36 @@ the server's time zone) answer `400`. A missing required value answers `400 Miss
|
|
|
236
236
|
`| undefined`, default value) receive `undefined` when absent, so default values apply:
|
|
237
237
|
`@Query('page') page = 1`. A repeated query key for a scalar parameter is a `400`.
|
|
238
238
|
|
|
239
|
+
### Custom parameter decorators
|
|
240
|
+
|
|
241
|
+
`createParamDecorator` from `@znight/core` defines your own parameter decorators, such as a
|
|
242
|
+
`@CurrentUser()` reading what an authentication guard stored (RFC 0010). The `http` resolver
|
|
243
|
+
receives the `HttpContext` and the data argument of the use:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import { createParamDecorator } from '@znight/core'
|
|
247
|
+
import { UnauthorizedException } from '@znight/http'
|
|
248
|
+
import type { User } from './user.ts'
|
|
249
|
+
|
|
250
|
+
export const CurrentUser = createParamDecorator<User>({
|
|
251
|
+
http: ctx => {
|
|
252
|
+
const user = ctx.state.get('user') as User | undefined
|
|
253
|
+
if (user === undefined) throw new UnauthorizedException()
|
|
254
|
+
return user
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// @Get('/me') me(@CurrentUser() user: User) { return user }
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The resolver runs inside `invoke`, after the guards and the interceptors, in parameter order with
|
|
262
|
+
the other arguments; what it throws is answered like a handler error. It may return a promise (the
|
|
263
|
+
route then awaits it), or be the class of a provider implementing `ParamResolver` (constructor
|
|
264
|
+
injection, any scope: a request-scoped resolver is created once per request). The compiler checks
|
|
265
|
+
every use: the decorator must have an `http` resolver, be the only parameter decorator of the
|
|
266
|
+
parameter, and inject a type the parameter accepts. Custom parameters are not documented by
|
|
267
|
+
`@znight/openapi`: document the header or cookie they read with `@ApiHeader` or `@ApiSecurity`.
|
|
268
|
+
|
|
239
269
|
## Validation
|
|
240
270
|
|
|
241
271
|
Any library implementing [Standard Schema v1](https://standardschema.dev) works, without adapter:
|
|
@@ -1622,6 +1652,13 @@ export class StatusPlugin implements Plugin, PluginHttpRoutes {
|
|
|
1622
1652
|
needs a decision there too: without one the boot fails (ZNIGHT023), with a hint naming the plugin.
|
|
1623
1653
|
- An invalid route (method, path grammar, name, handler, meta, body limit) or a collision with another route is
|
|
1624
1654
|
ZNIGHT007 naming the plugin. `HttpPlugin.routes` lists the routes with `source: '<plugin name>'`.
|
|
1655
|
+
- `onHttpDrain()` (optional) is called once when `HttpPlugin` starts draining, in its `onStop`,
|
|
1656
|
+
whatever the plugin order: after it stopped accepting connections and started closing its event
|
|
1657
|
+
streams, before it waits for the requests in flight. A plugin whose routes answer long-lived
|
|
1658
|
+
streams `HttpPlugin` does not know of (a streamed `Response` produced by another server, such as a
|
|
1659
|
+
GraphQL driver's subscriptions) ends them there, so that the drain does not wait for them until
|
|
1660
|
+
`drainTimeoutMs`. The drain waits for a returned promise too (within `drainTimeoutMs`); a failure
|
|
1661
|
+
is logged, never thrown.
|
|
1625
1662
|
|
|
1626
1663
|
## Adapters
|
|
1627
1664
|
|
|
@@ -2144,8 +2181,12 @@ Rules:
|
|
|
2144
2181
|
stream (an `HttpResponse` or a Web `Response` is sent as usual). Only on `GET` routes; invalid
|
|
2145
2182
|
values are ZNIGHT007 at boot.
|
|
2146
2183
|
- Param sources (`HttpParamSource`): `param`, `query`, `body`, `headers`, `request`, `response`,
|
|
2147
|
-
`context`, `file`, `files`, `cookie
|
|
2148
|
-
`@Cookie()`)
|
|
2184
|
+
`context`, `file`, `files`, `cookie`, `signedCookie` (`name`: the cookie name; absent for
|
|
2185
|
+
`@Cookie()`) and `custom` (a custom parameter decorator, `name`: the decorator's name).
|
|
2186
|
+
- `paramResolvers` (`Token[]`, omitted when empty): the provider classes resolving custom
|
|
2187
|
+
parameters (`ParamResolver`), in order of first use. `invoke` then takes a third parameter `x`,
|
|
2188
|
+
the instances in that order; the plugin resolves them once at boot when they are all singletons,
|
|
2189
|
+
otherwise in the request scope of each call. A route with a `signedCookie` parameter fails the boot without `cookies.secrets`
|
|
2149
2190
|
(ZNIGHT006).
|
|
2150
2191
|
- Guards, interceptors and controllers referenced by tokens must be providers (the compiler
|
|
2151
2192
|
registers injectable helper classes, §6.5 `resolveClassRef`). Request-scoped guards/interceptors
|
|
@@ -2205,6 +2246,14 @@ expressions of the table below are `validate(S, ...)` calls in both forms.
|
|
|
2205
2246
|
| `@UploadedFiles('f') f: File[]` | `uploads.files(ctx, 'f')` · optional: `uploads.optionalFiles(ctx, 'f')` |
|
|
2206
2247
|
| `@UploadedFile()` / `@UploadedFiles()` | the same helpers with `undefined` as field (any field) |
|
|
2207
2248
|
| `@Ctx()` | `ctx` |
|
|
2249
|
+
| custom, function resolver | `(D.resolvers.http as ParamFunctionOf<typeof D, 'http'>)(ctx, data)` |
|
|
2250
|
+
| custom, class resolver | `x[i].resolve(ctx, data)` (`data` left out when `resolve` declares one param) |
|
|
2251
|
+
|
|
2252
|
+
Custom parameters (`createParamDecorator`, RFC 0010) are treated like validated arguments: the
|
|
2253
|
+
resolver may return a thenable, so each one is followed by the `isThenable` fallback, and the route
|
|
2254
|
+
stays synchronous while the resolvers return values. `data` is the literal data argument or an
|
|
2255
|
+
imported reference (`undefined` without argument). The cast `as never` is added when the compiler
|
|
2256
|
+
could not check the value type against the parameter.
|
|
2208
2257
|
|
|
2209
2258
|
`coerce.X` by declared type (`T`):
|
|
2210
2259
|
|
|
@@ -403,6 +403,12 @@ interface HttpRouteInfo {
|
|
|
403
403
|
*/
|
|
404
404
|
readonly meta: Readonly<Record<string, unknown>>;
|
|
405
405
|
}
|
|
406
|
+
declare module "@znight/core" {
|
|
407
|
+
/** Custom parameter decorators (`createParamDecorator`) resolve HTTP parameters from the {@link HttpContext}. */
|
|
408
|
+
interface ParamContexts {
|
|
409
|
+
http: HttpContext;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
406
412
|
/**
|
|
407
413
|
* Adapter-neutral view of the current HTTP request, shared by guards, interceptors, exception
|
|
408
414
|
* mappers and handlers (`@Ctx()`), and injectable in request-scoped providers.
|
|
@@ -528,7 +534,7 @@ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTION
|
|
|
528
534
|
* `name` being the multipart field; `cookie` / `signedCookie`: `@Cookie` / `@SignedCookie`, the
|
|
529
535
|
* `name` being the cookie name).
|
|
530
536
|
*/
|
|
531
|
-
type HttpParamSource = "param" | "query" | "body" | "headers" | "request" | "response" | "context" | "file" | "files" | "cookie" | "signedCookie";
|
|
537
|
+
type HttpParamSource = "param" | "query" | "body" | "headers" | "request" | "response" | "context" | "file" | "files" | "cookie" | "signedCookie" | "custom";
|
|
532
538
|
/** A handler parameter (informational: extraction, coercion and validation happen in `invoke`). */
|
|
533
539
|
interface HttpParamDefinition {
|
|
534
540
|
/** Position in the handler signature. */
|
|
@@ -613,10 +619,17 @@ interface HttpRouteDefinition {
|
|
|
613
619
|
*/
|
|
614
620
|
readonly sse?: HttpSseDefinition;
|
|
615
621
|
/**
|
|
616
|
-
*
|
|
617
|
-
* the
|
|
622
|
+
* Provider classes resolving custom parameters (`ParamResolver`, RFC 0010), in order of first use:
|
|
623
|
+
* the plugin resolves them in the request scope and passes the instances to `invoke` as `x`
|
|
624
|
+
* (`x[i]` is the instance of `paramResolvers[i]`). Absent when the route has none.
|
|
625
|
+
*/
|
|
626
|
+
readonly paramResolvers?: readonly Token[];
|
|
627
|
+
/**
|
|
628
|
+
* Calls the handler: extracts, coerces and validates every argument from the context (custom
|
|
629
|
+
* parameters included), then calls the method on the controller instance. May return a promise.
|
|
630
|
+
* `resolvers` is passed when the route has {@link paramResolvers}.
|
|
618
631
|
*/
|
|
619
|
-
readonly invoke: (controller: any, ctx: HttpContext) => unknown;
|
|
632
|
+
readonly invoke: (controller: any, ctx: HttpContext, resolvers?: any) => unknown;
|
|
620
633
|
}
|
|
621
634
|
/** A controller: a provider whose methods are routes. */
|
|
622
635
|
interface HttpControllerDefinition {
|
|
@@ -879,4 +892,4 @@ interface HttpAdapter {
|
|
|
879
892
|
injectStream?(request: InjectStreamRequest): Promise<InjectStreamResponse>;
|
|
880
893
|
}
|
|
881
894
|
export { PayloadTooLargeException as $, UploadedFileOptions as A, BadRequestException as B, HttpUploadDefinition as C, CookieOptions as Ct, RequestAbortedError as D, parseSetCookie as Dt, RequestAbortKind as E, parseCookieHeader as Et, formatByteSize as F, HeaderValue as G, ForbiddenException as H, parseByteSize as I, HttpExceptionOptions as J, HttpErrorBody as K, QueryRecord as L, Uploads as M, uploads as N, RequestId as O, serializeClearCookie as Ot, ByteSize as P, NotImplementedException as Q, parseQuery as R, HttpSseDefinition as S, CookieDefaults as St, HttpRouteInfo as T, SetCookieInfo as Tt, GatewayTimeoutException as U, ConflictException as V, GoneException as W, MethodNotAllowedException as X, InternalServerErrorException as Y, NotFoundException as Z, HttpMethod as _, StandardSchemaV1Result as _t, AdapterRoute as a, ValidationException as at, HttpRouteDefinition as b, isStandardSchema as bt, HttpServerAddress as c, toIssueDetail as ct, InjectStreamOptions as d, StandardSchemaV1 as dt, ServiceUnavailableException as et, InjectStreamRequest as f, StandardSchemaV1FailureResult as ft, HttpMetadata as g, StandardSchemaV1Props as gt, HttpControllerDefinition as h, StandardSchemaV1PathSegment as ht, AdapterRequest as i, UnsupportedMediaTypeException as it, UploadedFilesOptions as j, UploadLimits as k, serializeCookie as kt, InjectRequest as l, InferSchemaInput as lt, ReplyBody as m, StandardSchemaV1Options as mt, AdapterHandler as n, UnauthorizedException as nt, FallbackHandler as o, ValidationIssueDetail as ot, InjectStreamResponse as p, StandardSchemaV1Issue as pt, HttpException as q, AdapterReply as r, UnprocessableEntityException as rt, HttpAdapter as s, isHttpException as st, AdapterCloseOptions as t, TooManyRequestsException as tt, InjectResponse as u, InferSchemaOutput as ut, HttpParamDefinition as v, StandardSchemaV1SuccessResult as vt, HttpContext as w, ResponseCookie as wt, HttpRouteGroupDefinition as x, ClearCookieOptions as xt, HttpParamSource as y, StandardSchemaV1Types as yt, BadGatewayException as z };
|
|
882
|
-
//# sourceMappingURL=adapter-
|
|
895
|
+
//# sourceMappingURL=adapter-DmhQGqAR.d.mts.map
|
package/dist/compiler.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as HttpMethod, y as HttpParamSource } from "./adapter-
|
|
1
|
+
import { _ as HttpMethod, y as HttpParamSource } from "./adapter-DmhQGqAR.mjs";
|
|
2
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";
|
|
@@ -46,6 +46,26 @@ interface UploadData {
|
|
|
46
46
|
/** The injected value does not have the declared type (`node:buffer` File, `File | null`): `as never`. */
|
|
47
47
|
readonly cast: boolean;
|
|
48
48
|
}
|
|
49
|
+
/** A custom parameter (`createParamDecorator`, RFC 0010). */
|
|
50
|
+
interface CustomParamData {
|
|
51
|
+
/** Canonical identity of the decorator `const` (imported by the generated module). */
|
|
52
|
+
readonly identity: string;
|
|
53
|
+
/** Name of the decorator (`CurrentUser`). */
|
|
54
|
+
readonly name: string;
|
|
55
|
+
/** The data argument: evaluated value (it may contain references) or a reference; `undefined` without argument. */
|
|
56
|
+
readonly data: {
|
|
57
|
+
readonly value: StaticValue;
|
|
58
|
+
} | undefined;
|
|
59
|
+
/** The class resolver (`x[i].resolve(ctx, data)`); `undefined` for a function resolver. */
|
|
60
|
+
readonly resolver: ClassRef | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Parameters of the class resolver's `resolve` (own, inherited or an arrow function property);
|
|
63
|
+
* `undefined` for a function resolver, or when the compiler cannot read it (the call is cast).
|
|
64
|
+
*/
|
|
65
|
+
readonly arity?: number;
|
|
66
|
+
/** The value type could not be checked against the parameter: `as never`. */
|
|
67
|
+
readonly cast: boolean;
|
|
68
|
+
}
|
|
49
69
|
/** A handler parameter. */
|
|
50
70
|
interface HttpParamData {
|
|
51
71
|
/** Position in the handler signature. */
|
|
@@ -65,6 +85,8 @@ interface HttpParamData {
|
|
|
65
85
|
readonly coercion: ParamCoercion | undefined;
|
|
66
86
|
/** Upload parameter (`source` `file` / `files`). */
|
|
67
87
|
readonly upload?: UploadData;
|
|
88
|
+
/** Custom parameter (`source` `custom`). */
|
|
89
|
+
readonly custom?: CustomParamData;
|
|
68
90
|
/**
|
|
69
91
|
* The default value of the handler parameter (`@Query('page') page = 1` → `1`), when it is
|
|
70
92
|
* statically evaluable (literals, `const` references; references to exported bindings are left
|
package/dist/compiler.mjs
CHANGED
|
@@ -1150,7 +1150,15 @@ function analyzeParams(ctx, cls, method, where, bodyValidation) {
|
|
|
1150
1150
|
const params = [];
|
|
1151
1151
|
let complete = true;
|
|
1152
1152
|
for (const param of method.params) {
|
|
1153
|
-
const
|
|
1153
|
+
const decorators = unique(param.decorators).filter((decorator) => decorator.owner === "http" && PARAM_SOURCES.has(decorator.identity ?? ""));
|
|
1154
|
+
const custom = typeof ctx.paramDecorator === "function" ? ctx.paramDecorator(param, "http", cls) : void 0;
|
|
1155
|
+
if (custom !== void 0) {
|
|
1156
|
+
for (const other of decorators) error(ctx, other.location, `Parameter '${param.name}' of ${where} has several parameter decorators (@${custom.decorator.name} and @${other.name})`, ["Keep a single parameter decorator"]);
|
|
1157
|
+
if (custom.ok && decorators.length === 0) params.push(customParam(param, custom));
|
|
1158
|
+
else complete = false;
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
const [decorator, ...extra] = decorators;
|
|
1154
1162
|
const foreign = decorator === void 0 ? foreignParameterDecorator(param) : void 0;
|
|
1155
1163
|
if (foreign !== void 0) {
|
|
1156
1164
|
error(ctx, foreign.decorator.location, `@${foreign.decorator.name} on parameter '${param.name}' of ${where} is imported from '${foreign.module}': route parameters use ${foreign.name} from '${HTTP_PACKAGE}'`, [`Import it from '${HTTP_PACKAGE}': import { ${foreign.name} } from '${HTTP_PACKAGE}'`]);
|
|
@@ -1177,6 +1185,29 @@ function analyzeParams(ctx, cls, method, where, bodyValidation) {
|
|
|
1177
1185
|
complete
|
|
1178
1186
|
};
|
|
1179
1187
|
}
|
|
1188
|
+
/** A custom parameter (`source: 'custom'`, the decorator name as `name`). */
|
|
1189
|
+
function customParam(param, use) {
|
|
1190
|
+
const data = use.data;
|
|
1191
|
+
return {
|
|
1192
|
+
index: param.index,
|
|
1193
|
+
name: param.name,
|
|
1194
|
+
source: "custom",
|
|
1195
|
+
key: use.name,
|
|
1196
|
+
type: void 0,
|
|
1197
|
+
optional: param.optional || param.hasDefault,
|
|
1198
|
+
schema: void 0,
|
|
1199
|
+
coercion: void 0,
|
|
1200
|
+
custom: {
|
|
1201
|
+
identity: use.identity,
|
|
1202
|
+
name: use.name,
|
|
1203
|
+
data: data === void 0 ? void 0 : { value: data.ref ?? data.value },
|
|
1204
|
+
resolver: use.resolver.kind === "class" ? use.resolver.ref : void 0,
|
|
1205
|
+
...use.resolver.kind === "class" && use.resolver.arity !== void 0 ? { arity: use.resolver.arity } : {},
|
|
1206
|
+
cast: use.cast
|
|
1207
|
+
},
|
|
1208
|
+
location: use.location
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1180
1211
|
function analyzeParam(ctx, cls, method, param, decorator, where, bodyValidation) {
|
|
1181
1212
|
const source = PARAM_SOURCES.get(decorator.identity ?? "");
|
|
1182
1213
|
if (source === void 0) return;
|
|
@@ -1730,6 +1761,7 @@ function createHelpers(ctx) {
|
|
|
1730
1761
|
let isThenable;
|
|
1731
1762
|
let uploads;
|
|
1732
1763
|
let context;
|
|
1764
|
+
let paramFunctionOf;
|
|
1733
1765
|
return {
|
|
1734
1766
|
coerce: () => {
|
|
1735
1767
|
coerce ??= ctx.importFrom(HTTP_PACKAGE, "coerce");
|
|
@@ -1750,6 +1782,10 @@ function createHelpers(ctx) {
|
|
|
1750
1782
|
context: () => {
|
|
1751
1783
|
context ??= ctx.importFrom(HTTP_PACKAGE, "HttpContext", { type: true });
|
|
1752
1784
|
return context;
|
|
1785
|
+
},
|
|
1786
|
+
paramFunctionOf: () => {
|
|
1787
|
+
paramFunctionOf ??= ctx.importFrom("@znight/core", "ParamFunctionOf", { type: true });
|
|
1788
|
+
return paramFunctionOf;
|
|
1753
1789
|
}
|
|
1754
1790
|
};
|
|
1755
1791
|
}
|
|
@@ -1778,7 +1814,9 @@ function routeNode(ctx, helpers, controller, route) {
|
|
|
1778
1814
|
if (uploads.length > 0) entries.push(["uploads", ctx.arr(uploads.map((upload) => uploadNode(ctx, upload)))]);
|
|
1779
1815
|
if (route.rawResponse) entries.push(["rawResponse", true]);
|
|
1780
1816
|
if (route.sse !== void 0) entries.push(["sse", sseNode(ctx, route.sse)]);
|
|
1781
|
-
|
|
1817
|
+
const resolvers = paramResolvers(route);
|
|
1818
|
+
if (resolvers.length > 0) entries.push(["paramResolvers", tokens(ctx, resolvers)]);
|
|
1819
|
+
entries.push(["invoke", invokeNode(ctx, helpers, controller, route, resolvers)]);
|
|
1782
1820
|
return ctx.obj(entries);
|
|
1783
1821
|
}
|
|
1784
1822
|
/** `{ heartbeat?, retry?, maxDuration? }` (`{}` without options). */
|
|
@@ -1821,20 +1859,73 @@ function tokens(ctx, refs) {
|
|
|
1821
1859
|
* `@Body()` parameter reads the body in `invoke`: that arrow is `async` and `await`s every
|
|
1822
1860
|
* `validate()`.
|
|
1823
1861
|
*/
|
|
1824
|
-
function invokeNode(ctx, helpers, controller, route) {
|
|
1862
|
+
function invokeNode(ctx, helpers, controller, route, resolvers) {
|
|
1825
1863
|
const params = [["c", ctx.importType(controller.identity)]];
|
|
1826
|
-
if (route.params.length > 0 || route.bodySchema !== void 0) params.push(["ctx", helpers.context()]);
|
|
1864
|
+
if (route.params.length > 0 || route.bodySchema !== void 0) params.push([readsContext(route) ? "ctx" : "_ctx", helpers.context()]);
|
|
1865
|
+
if (resolvers.length > 0) {
|
|
1866
|
+
const tuple = resolvers.map((ref) => ctx.importType(ref));
|
|
1867
|
+
params.push(["x", ctx.code`readonly [${joinNodes(ctx, tuple)}]`]);
|
|
1868
|
+
const index = new Map(resolvers.map((ref, position) => [ref.token, position]));
|
|
1869
|
+
const withResolvers = (x) => x === void 0 ? helpers : {
|
|
1870
|
+
...helpers,
|
|
1871
|
+
resolvers: {
|
|
1872
|
+
x,
|
|
1873
|
+
index
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
return invokeArrow(ctx, params, route, (c, http, x) => [
|
|
1877
|
+
c,
|
|
1878
|
+
http,
|
|
1879
|
+
withResolvers(x)
|
|
1880
|
+
]);
|
|
1881
|
+
}
|
|
1882
|
+
return invokeArrow(ctx, params, route, (c, http) => [
|
|
1883
|
+
c,
|
|
1884
|
+
http,
|
|
1885
|
+
helpers
|
|
1886
|
+
]);
|
|
1887
|
+
}
|
|
1888
|
+
/** `true` when an argument (or the body validation) reads the context: all but a `resolve()` without parameter. */
|
|
1889
|
+
function readsContext(route) {
|
|
1890
|
+
return route.bodySchema !== void 0 || route.params.some((param) => param.custom?.resolver === void 0 || param.custom.arity !== 0);
|
|
1891
|
+
}
|
|
1892
|
+
/** The provider classes resolving custom parameters, in order of first use (`x[i]`). */
|
|
1893
|
+
function paramResolvers(route) {
|
|
1894
|
+
const found = /* @__PURE__ */ new Map();
|
|
1895
|
+
for (const param of route.params) {
|
|
1896
|
+
const resolver = param.custom?.resolver;
|
|
1897
|
+
if (resolver !== void 0 && !found.has(resolver.token)) found.set(resolver.token, resolver);
|
|
1898
|
+
}
|
|
1899
|
+
return [...found.values()];
|
|
1900
|
+
}
|
|
1901
|
+
/** `a, b, c`. */
|
|
1902
|
+
function joinNodes(ctx, nodes) {
|
|
1903
|
+
return nodes.slice(1).reduce((joined, node) => ctx.code`${joined}, ${node}`, nodes[0]);
|
|
1904
|
+
}
|
|
1905
|
+
/** The arrow of {@link invokeNode}; `bind` maps the arrow parameters to the controller, the context and the helpers. */
|
|
1906
|
+
function invokeArrow(ctx, params, route, bind) {
|
|
1827
1907
|
const slots = [];
|
|
1828
1908
|
for (const param of route.params) slots[param.index] = param;
|
|
1829
1909
|
const bodySchema = route.bodySchema;
|
|
1830
|
-
if (bodySchema !== void 0) return ctx.arrow(params, (
|
|
1910
|
+
if (bodySchema !== void 0) return ctx.arrow(params, (...bound) => {
|
|
1911
|
+
const [c, http, helpers] = bind(...bound);
|
|
1831
1912
|
const call = handlerCall(ctx, c, route, Array.from(slots, (param) => awaitedArgument(ctx, helpers, http, param)));
|
|
1832
1913
|
const body = ctx.code`await ${ctx.call(ctx.member(http, "parseBody"))}`;
|
|
1833
1914
|
const check = ctx.code`await ${validation(ctx, helpers, bodySchema, body, "body")}`;
|
|
1834
1915
|
return ctx.code`{ ${check}; return ${call} }`;
|
|
1835
1916
|
}, { async: true });
|
|
1836
|
-
const last = slots.findLastIndex((param) => param
|
|
1837
|
-
return ctx.arrow(params, (
|
|
1917
|
+
const last = slots.findLastIndex((param) => param !== void 0 && isDeferred(param));
|
|
1918
|
+
return ctx.arrow(params, (...bound) => {
|
|
1919
|
+
const [c, http, helpers] = bind(...bound);
|
|
1920
|
+
return last === -1 ? handlerCall(ctx, c, route, Array.from(slots, (param) => plainArgument(ctx, helpers, http, param))) : validatingBlock(ctx, helpers, route, slots, last, c, http);
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* `true` for an argument whose expression may return a thenable: a validation with a schema, or a
|
|
1925
|
+
* custom parameter (its resolver may return a promise).
|
|
1926
|
+
*/
|
|
1927
|
+
function isDeferred(param) {
|
|
1928
|
+
return param.schema !== void 0 || param.custom !== void 0;
|
|
1838
1929
|
}
|
|
1839
1930
|
/** `c.handler(args)`. */
|
|
1840
1931
|
function handlerCall(ctx, c, route, args) {
|
|
@@ -1873,7 +1964,7 @@ function validatingBlock(ctx, helpers, route, slots, last, c, http) {
|
|
|
1873
1964
|
const param = slots[index];
|
|
1874
1965
|
const name = local.get(index);
|
|
1875
1966
|
statements.push(ctx.code`const ${name} = ${plainArgument(ctx, helpers, http, param)}`);
|
|
1876
|
-
if (param
|
|
1967
|
+
if (isDeferred(param)) {
|
|
1877
1968
|
const rest = Array.from(slots, (other, position) => {
|
|
1878
1969
|
if (position < index) return evaluated(position);
|
|
1879
1970
|
return position === index ? ctx.code`await ${name}` : awaitedArgument(ctx, helpers, http, other);
|
|
@@ -1890,7 +1981,7 @@ function validatingBlock(ctx, helpers, route, slots, last, c, http) {
|
|
|
1890
1981
|
/** An argument evaluated in an `async` arrow: its validation, if any, is `await`ed. */
|
|
1891
1982
|
function awaitedArgument(ctx, helpers, http, param) {
|
|
1892
1983
|
const node = plainArgument(ctx, helpers, http, param);
|
|
1893
|
-
return param
|
|
1984
|
+
return param === void 0 || !isDeferred(param) ? node : ctx.code`await ${node}`;
|
|
1894
1985
|
}
|
|
1895
1986
|
/** An argument as an expression (`validate()` not awaited); `undefined` for a hole. */
|
|
1896
1987
|
function plainArgument(ctx, helpers, http, param) {
|
|
@@ -1943,7 +2034,33 @@ function argumentNode(ctx, helpers, http, param) {
|
|
|
1943
2034
|
cast: false
|
|
1944
2035
|
}, value, label);
|
|
1945
2036
|
}
|
|
2037
|
+
case "custom": return customArgument(ctx, helpers, http, param.custom);
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* A custom parameter (RFC 0010): `(D.resolvers.http as ParamFunctionOf<typeof D, 'http'>)(ctx, data)`
|
|
2042
|
+
* for a function resolver, `x[i].resolve(ctx, data)` for a class resolver; cast with `as never` when
|
|
2043
|
+
* the value type could not be checked against the parameter.
|
|
2044
|
+
*/
|
|
2045
|
+
function customArgument(ctx, helpers, http, custom) {
|
|
2046
|
+
const data = custom.data === void 0 ? ctx.lit(void 0) : dataNode(ctx, custom.data.value);
|
|
2047
|
+
let call;
|
|
2048
|
+
if (custom.resolver === void 0) {
|
|
2049
|
+
const decorator = ctx.importValue(custom.identity);
|
|
2050
|
+
const callee = ctx.code`${ctx.member(decorator, "resolvers", "http")} as ${helpers.paramFunctionOf()}<typeof ${decorator}, 'http'>`;
|
|
2051
|
+
call = ctx.call(callee, [http, data]);
|
|
2052
|
+
} else {
|
|
2053
|
+
const resolvers = helpers.resolvers;
|
|
2054
|
+
const index = resolvers.index.get(custom.resolver.token);
|
|
2055
|
+
if (custom.arity === void 0) return ctx.code`(${resolvers.x}[${index}] as { resolve(ctx: unknown, data: unknown): unknown }).resolve(${http}, ${data}) as never`;
|
|
2056
|
+
const args = [http, data].slice(0, custom.arity);
|
|
2057
|
+
call = ctx.code`${resolvers.x}[${index}].resolve(${args.length === 0 ? ctx.code`` : joinNodes(ctx, args)})`;
|
|
1946
2058
|
}
|
|
2059
|
+
return custom.cast ? ctx.code`${call} as never` : call;
|
|
2060
|
+
}
|
|
2061
|
+
/** The data argument of a custom parameter: a literal (references imported) or a reference. */
|
|
2062
|
+
function dataNode(ctx, value) {
|
|
2063
|
+
return isValueRef(value) ? ctx.ref(value) : ctx.lit(value);
|
|
1947
2064
|
}
|
|
1948
2065
|
/** `validate(schema, value, location)` (the value, or a promise when the schema validates asynchronously). */
|
|
1949
2066
|
function validation(ctx, helpers, schema, value, location) {
|