@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/dist/compiler.mjs
CHANGED
|
@@ -514,6 +514,38 @@ function routeMeta(ctx, cls, decorators, target) {
|
|
|
514
514
|
}
|
|
515
515
|
return entries;
|
|
516
516
|
}
|
|
517
|
+
/**
|
|
518
|
+
* The route metadata entries of a class (`method` omitted) or of one of its methods: the route
|
|
519
|
+
* decorators (`createRouteDecorator` consts) applied to that target, in source order, read exactly
|
|
520
|
+
* as the http extension reads controllers and routes (keys, statically evaluated values, groups,
|
|
521
|
+
* `required`). Problems are reported as ZNIGHT005 through `ctx`, once per call: read each target
|
|
522
|
+
* once. Decorators that are not route decorators are ignored.
|
|
523
|
+
*
|
|
524
|
+
* Works with the analysis context of any extension. The entries of a target are its own: combine a
|
|
525
|
+
* class and a method with {@link effectiveRouteMeta}.
|
|
526
|
+
*
|
|
527
|
+
* ```ts
|
|
528
|
+
* analyzeClass(cls, ctx) {
|
|
529
|
+
* const classMeta = readRouteMeta(ctx, cls)
|
|
530
|
+
* for (const method of cls.methods) {
|
|
531
|
+
* const meta = effectiveRouteMeta(classMeta, readRouteMeta(ctx, cls, method))
|
|
532
|
+
* }
|
|
533
|
+
* }
|
|
534
|
+
* ```
|
|
535
|
+
*/
|
|
536
|
+
function readRouteMeta(ctx, cls, method) {
|
|
537
|
+
return method === void 0 ? routeMeta(ctx, cls, cls.decorators, cls.displayName) : routeMeta(ctx, cls, method.decorators, `${cls.displayName}.${method.name}`);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* The effective metadata of a method from the entries of its class and its own (RFC 0001 §5.1):
|
|
541
|
+
* the class entries, except those of a group the method decides itself, then the method entries.
|
|
542
|
+
* The same rule as `ctx.route.meta` at runtime.
|
|
543
|
+
*/
|
|
544
|
+
function effectiveRouteMeta(classMeta, methodMeta) {
|
|
545
|
+
if (methodMeta.length === 0) return [...classMeta];
|
|
546
|
+
const decided = new Set(methodMeta.map((entry) => entry.group));
|
|
547
|
+
return [...classMeta.filter((entry) => !decided.has(entry.group)), ...methodMeta];
|
|
548
|
+
}
|
|
517
549
|
/** `true` when the decorator is a route metadata decorator. */
|
|
518
550
|
function isRouteMetaDecorator(ctx, decorator) {
|
|
519
551
|
return decorator.owner === void 0 && decorator.identity !== void 0 && resolveRouteDecorator(ctx, decorator.identity).kind !== "other";
|
|
@@ -661,7 +693,7 @@ function analyzeHttpClass(cls, ctx) {
|
|
|
661
693
|
const basePath = normalizePath(options.path);
|
|
662
694
|
const guards = classRefs(ctx, cls, cls.decorators, Ids.UseGuard, "guard");
|
|
663
695
|
const interceptors = classRefs(ctx, cls, cls.decorators, Ids.UseInterceptor, "interceptor");
|
|
664
|
-
const meta =
|
|
696
|
+
const meta = readRouteMeta(ctx, cls);
|
|
665
697
|
const bodyLimit = bodyLimitOf(ctx, cls.decorators);
|
|
666
698
|
const routes = [];
|
|
667
699
|
for (const method of cls.methods) routes.push(...analyzeMethod(ctx, cls, method, basePath, bodyLimit));
|
|
@@ -796,7 +828,7 @@ function analyzeMethod(ctx, cls, method, basePath, controllerBodyLimit) {
|
|
|
796
828
|
let headers = responseHeaders(ctx, decorators);
|
|
797
829
|
const guards = classRefs(ctx, cls, decorators, Ids.UseGuard, "guard");
|
|
798
830
|
const interceptors = classRefs(ctx, cls, decorators, Ids.UseInterceptor, "interceptor");
|
|
799
|
-
const meta =
|
|
831
|
+
const meta = readRouteMeta(ctx, cls, method);
|
|
800
832
|
const bodySchema = bodyValidation !== void 0 && !params.some((param) => param.source === "body") ? bodyValidation.schema : void 0;
|
|
801
833
|
let rawResponse = params.some((param) => param.source === "response");
|
|
802
834
|
const bodyLimit = bodyLimitOf(ctx, decorators) ?? controllerBodyLimit;
|
|
@@ -1695,6 +1727,7 @@ function groupsNode(ctx, controllers) {
|
|
|
1695
1727
|
function createHelpers(ctx) {
|
|
1696
1728
|
let coerce;
|
|
1697
1729
|
let validate;
|
|
1730
|
+
let isThenable;
|
|
1698
1731
|
let uploads;
|
|
1699
1732
|
let context;
|
|
1700
1733
|
return {
|
|
@@ -1706,6 +1739,10 @@ function createHelpers(ctx) {
|
|
|
1706
1739
|
validate ??= ctx.importFrom(HTTP_PACKAGE, "validate");
|
|
1707
1740
|
return validate;
|
|
1708
1741
|
},
|
|
1742
|
+
isThenable: () => {
|
|
1743
|
+
isThenable ??= ctx.importFrom(HTTP_PACKAGE, "isThenable");
|
|
1744
|
+
return isThenable;
|
|
1745
|
+
},
|
|
1709
1746
|
uploads: () => {
|
|
1710
1747
|
uploads ??= ctx.importFrom(HTTP_PACKAGE, "uploads");
|
|
1711
1748
|
return uploads;
|
|
@@ -1778,22 +1815,86 @@ function tokens(ctx, refs) {
|
|
|
1778
1815
|
return ctx.arr(refs.map((ref) => ctx.tokenOf(ref) ?? ref.token));
|
|
1779
1816
|
}
|
|
1780
1817
|
/**
|
|
1781
|
-
* `(c: Controller, ctx: HttpContext) => c.handler(...args)
|
|
1782
|
-
*
|
|
1818
|
+
* `(c: Controller, ctx: HttpContext) => c.handler(...args)`, synchronous when no argument is
|
|
1819
|
+
* validated with a schema. A route validating arguments gets a block that stays synchronous as long
|
|
1820
|
+
* as every `validate()` returns a value (see {@link validatingBlock}). `@Validate(S)` without
|
|
1821
|
+
* `@Body()` parameter reads the body in `invoke`: that arrow is `async` and `await`s every
|
|
1822
|
+
* `validate()`.
|
|
1783
1823
|
*/
|
|
1784
1824
|
function invokeNode(ctx, helpers, controller, route) {
|
|
1785
|
-
const async = route.bodySchema !== void 0 || route.params.some((param) => param.schema !== void 0);
|
|
1786
1825
|
const params = [["c", ctx.importType(controller.identity)]];
|
|
1787
1826
|
if (route.params.length > 0 || route.bodySchema !== void 0) params.push(["ctx", helpers.context()]);
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1827
|
+
const slots = [];
|
|
1828
|
+
for (const param of route.params) slots[param.index] = param;
|
|
1829
|
+
const bodySchema = route.bodySchema;
|
|
1830
|
+
if (bodySchema !== void 0) return ctx.arrow(params, (c, http) => {
|
|
1831
|
+
const call = handlerCall(ctx, c, route, Array.from(slots, (param) => awaitedArgument(ctx, helpers, http, param)));
|
|
1793
1832
|
const body = ctx.code`await ${ctx.call(ctx.member(http, "parseBody"))}`;
|
|
1794
|
-
const check = validation(ctx, helpers,
|
|
1833
|
+
const check = ctx.code`await ${validation(ctx, helpers, bodySchema, body, "body")}`;
|
|
1795
1834
|
return ctx.code`{ ${check}; return ${call} }`;
|
|
1796
|
-
}, { async });
|
|
1835
|
+
}, { async: true });
|
|
1836
|
+
const last = slots.findLastIndex((param) => param?.schema !== void 0);
|
|
1837
|
+
return ctx.arrow(params, (c, http) => last === -1 ? handlerCall(ctx, c, route, Array.from(slots, (param) => plainArgument(ctx, helpers, http, param))) : validatingBlock(ctx, helpers, route, slots, last, c, http));
|
|
1838
|
+
}
|
|
1839
|
+
/** `c.handler(args)`. */
|
|
1840
|
+
function handlerCall(ctx, c, route, args) {
|
|
1841
|
+
return ctx.call(ctx.member(c, route.handler), args);
|
|
1842
|
+
}
|
|
1843
|
+
/**
|
|
1844
|
+
* The body of the `invoke` of a route validating arguments with schemas: the arguments up to the
|
|
1845
|
+
* last validated one are evaluated in order into `const`s (`a<index>`), the handler is called with
|
|
1846
|
+
* them. `validate()` returns the value when the schema validates synchronously (Zod, Valibot and
|
|
1847
|
+
* ArkType without async refinements), so the request needs no promise; when one returns a
|
|
1848
|
+
* thenable, the rest runs in an `async` arrow that awaits it, then evaluates the remaining arguments
|
|
1849
|
+
* (`await`ing their validations) and calls the handler: the order of evaluation, the error reported
|
|
1850
|
+
* when several arguments are invalid and the value received by the handler are those of an `async`
|
|
1851
|
+
* `invoke` awaiting every `validate()`.
|
|
1852
|
+
*
|
|
1853
|
+
* ```ts
|
|
1854
|
+
* (c: UserController, ctx: HttpContext) => {
|
|
1855
|
+
* const a0 = coerce.number(ctx.params['id'], 'param "id"')
|
|
1856
|
+
* const a1 = validate(UpdateUser, ctx.body, 'body')
|
|
1857
|
+
* if (isThenable(a1)) return (async () => c.update(a0, await a1, ctx))()
|
|
1858
|
+
* return c.update(a0, a1, ctx)
|
|
1859
|
+
* }
|
|
1860
|
+
* ```
|
|
1861
|
+
*/
|
|
1862
|
+
function validatingBlock(ctx, helpers, route, slots, last, c, http) {
|
|
1863
|
+
const bound = [];
|
|
1864
|
+
for (let index = 0; index <= last; index++) {
|
|
1865
|
+
const param = slots[index];
|
|
1866
|
+
if (param !== void 0 && param.source !== "context") bound.push(index);
|
|
1867
|
+
}
|
|
1868
|
+
return ctx.block(bound.map((index) => `a${index}`), (...locals) => {
|
|
1869
|
+
const local = new Map(bound.map((index, position) => [index, locals[position]]));
|
|
1870
|
+
const evaluated = (index) => local.get(index) ?? plainArgument(ctx, helpers, http, slots[index]);
|
|
1871
|
+
const statements = [];
|
|
1872
|
+
for (const index of bound) {
|
|
1873
|
+
const param = slots[index];
|
|
1874
|
+
const name = local.get(index);
|
|
1875
|
+
statements.push(ctx.code`const ${name} = ${plainArgument(ctx, helpers, http, param)}`);
|
|
1876
|
+
if (param.schema !== void 0) {
|
|
1877
|
+
const rest = Array.from(slots, (other, position) => {
|
|
1878
|
+
if (position < index) return evaluated(position);
|
|
1879
|
+
return position === index ? ctx.code`await ${name}` : awaitedArgument(ctx, helpers, http, other);
|
|
1880
|
+
});
|
|
1881
|
+
const resume = ctx.arrow([], () => handlerCall(ctx, c, route, rest), { async: true });
|
|
1882
|
+
statements.push(ctx.code`if (${ctx.call(helpers.isThenable(), [name])}) return (${resume})()`);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const args = Array.from(slots, (param, position) => position <= last ? evaluated(position) : plainArgument(ctx, helpers, http, param));
|
|
1886
|
+
statements.push(ctx.code`return ${handlerCall(ctx, c, route, args)}`);
|
|
1887
|
+
return statements;
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
/** An argument evaluated in an `async` arrow: its validation, if any, is `await`ed. */
|
|
1891
|
+
function awaitedArgument(ctx, helpers, http, param) {
|
|
1892
|
+
const node = plainArgument(ctx, helpers, http, param);
|
|
1893
|
+
return param?.schema === void 0 ? node : ctx.code`await ${node}`;
|
|
1894
|
+
}
|
|
1895
|
+
/** An argument as an expression (`validate()` not awaited); `undefined` for a hole. */
|
|
1896
|
+
function plainArgument(ctx, helpers, http, param) {
|
|
1897
|
+
return param === void 0 ? ctx.lit(void 0) : argumentNode(ctx, helpers, http, param);
|
|
1797
1898
|
}
|
|
1798
1899
|
/** The expression passed to the handler for one parameter. */
|
|
1799
1900
|
function argumentNode(ctx, helpers, http, param) {
|
|
@@ -1844,13 +1945,13 @@ function argumentNode(ctx, helpers, http, param) {
|
|
|
1844
1945
|
}
|
|
1845
1946
|
}
|
|
1846
1947
|
}
|
|
1847
|
-
/** `
|
|
1948
|
+
/** `validate(schema, value, location)` (the value, or a promise when the schema validates asynchronously). */
|
|
1848
1949
|
function validation(ctx, helpers, schema, value, location) {
|
|
1849
|
-
return ctx.
|
|
1950
|
+
return ctx.call(helpers.validate(), [
|
|
1850
1951
|
ctx.ref(schema),
|
|
1851
1952
|
value,
|
|
1852
1953
|
location
|
|
1853
|
-
])
|
|
1954
|
+
]);
|
|
1854
1955
|
}
|
|
1855
1956
|
/** `coerce.X(value, label, ...)`, cast with `as never` when the result does not have the declared type. */
|
|
1856
1957
|
function coercion(ctx, helpers, coercion, value, label) {
|
|
@@ -1885,77 +1986,26 @@ function itemCoercer(ctx, helpers, plan) {
|
|
|
1885
1986
|
default: return ctx.member(helpers.coerce(), plan.kind);
|
|
1886
1987
|
}
|
|
1887
1988
|
}
|
|
1888
|
-
// src/compiler/
|
|
1889
|
-
/** Validates the routes of every controller. */
|
|
1890
|
-
function validateHttp(ctx) {
|
|
1891
|
-
const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token));
|
|
1892
|
-
const registered = /* @__PURE__ */ new Map();
|
|
1893
|
-
for (const controller of controllers) {
|
|
1894
|
-
const baseIssue = checkRoutePath(controller.rawBasePath);
|
|
1895
|
-
if (baseIssue !== void 0) {
|
|
1896
|
-
invalidRoute(ctx, controller.pathLocation, `Invalid base path ${JSON.stringify(controller.rawBasePath)} of ${controller.displayName}: ${baseIssue.message}`, GRAMMAR_HINTS);
|
|
1897
|
-
continue;
|
|
1898
|
-
}
|
|
1899
|
-
for (const route of controller.routes) {
|
|
1900
|
-
const where = `${controller.displayName}.${route.handler}`;
|
|
1901
|
-
const ownIssue = checkRoutePath(route.rawPath);
|
|
1902
|
-
const issue = ownIssue ?? checkRoutePath(route.fullPath);
|
|
1903
|
-
if (issue !== void 0) {
|
|
1904
|
-
const path = ownIssue === void 0 ? route.fullPath : route.rawPath;
|
|
1905
|
-
invalidRoute(ctx, route.pathLocation, `Invalid route path ${JSON.stringify(path)} of ${where}: ${issue.message}`, GRAMMAR_HINTS);
|
|
1906
|
-
continue;
|
|
1907
|
-
}
|
|
1908
|
-
const parsed = parseRoutePath(route.fullPath);
|
|
1909
|
-
checkParams(ctx, route, parsed, where);
|
|
1910
|
-
const key = `${route.method} ${parsed.shape}`;
|
|
1911
|
-
const previous = registered.get(key);
|
|
1912
|
-
if (previous === void 0) {
|
|
1913
|
-
registered.set(key, {
|
|
1914
|
-
where,
|
|
1915
|
-
route
|
|
1916
|
-
});
|
|
1917
|
-
continue;
|
|
1918
|
-
}
|
|
1919
|
-
ctx.report({
|
|
1920
|
-
code: Codes.InvalidRoute,
|
|
1921
|
-
severity: "error",
|
|
1922
|
-
message: `Duplicate route ${route.method} ${route.fullPath}: ${where} conflicts with ${previous.where} (${previous.route.method} ${previous.route.fullPath})`,
|
|
1923
|
-
location: route.location,
|
|
1924
|
-
hints: ["Two routes with the same method and the same path shape (parameter names aside) cannot coexist"],
|
|
1925
|
-
related: [{
|
|
1926
|
-
message: `${previous.where} is declared here`,
|
|
1927
|
-
location: previous.route.location
|
|
1928
|
-
}]
|
|
1929
|
-
});
|
|
1930
|
-
}
|
|
1931
|
-
}
|
|
1932
|
-
validateDecisions(ctx, controllers);
|
|
1933
|
-
}
|
|
1989
|
+
// src/compiler/route-groups.ts
|
|
1934
1990
|
/**
|
|
1935
|
-
* Every route decorator definition, sorted by identity: the applied ones (first usage
|
|
1936
|
-
* the compiler provides `definitionsOf`, the ones applied nowhere (invalid ones are
|
|
1937
|
-
* usages, which report them).
|
|
1991
|
+
* Every route decorator definition, sorted by identity: the applied ones (`applied`, first usage
|
|
1992
|
+
* wins) and, when the compiler provides `definitionsOf`, the ones applied nowhere (invalid ones are
|
|
1993
|
+
* left to the usages, which report them).
|
|
1938
1994
|
*/
|
|
1939
|
-
function routeDecoratorDefinitions(ctx,
|
|
1995
|
+
function routeDecoratorDefinitions(ctx, applied) {
|
|
1940
1996
|
const definitions = /* @__PURE__ */ new Map();
|
|
1941
|
-
const
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
else if (entry.value !== true) known.flag = false;
|
|
1954
|
-
}
|
|
1955
|
-
};
|
|
1956
|
-
for (const controller of controllers) {
|
|
1957
|
-
visit(controller.meta);
|
|
1958
|
-
for (const route of controller.routes) visit(route.meta);
|
|
1997
|
+
for (const entry of applied) {
|
|
1998
|
+
const known = definitions.get(entry.identity);
|
|
1999
|
+
if (known === void 0) definitions.set(entry.identity, {
|
|
2000
|
+
identity: entry.identity,
|
|
2001
|
+
name: entry.decorator,
|
|
2002
|
+
key: entry.key,
|
|
2003
|
+
group: entry.group,
|
|
2004
|
+
required: entry.required,
|
|
2005
|
+
definition: entry.definition,
|
|
2006
|
+
flag: entry.value === true
|
|
2007
|
+
});
|
|
2008
|
+
else if (entry.value !== true) known.flag = false;
|
|
1959
2009
|
}
|
|
1960
2010
|
const all = typeof ctx.definitionsOf === "function" ? ctx.definitionsOf(Ids.createRouteDecorator) : [];
|
|
1961
2011
|
for (const found of all) {
|
|
@@ -1982,20 +2032,26 @@ function routeDecoratorDefinitions(ctx, controllers) {
|
|
|
1982
2032
|
return [...definitions.values()].sort((a, b) => compareStrings(a.identity, b.identity));
|
|
1983
2033
|
}
|
|
1984
2034
|
/**
|
|
1985
|
-
*
|
|
1986
|
-
* decorators of a group agree on `required` (
|
|
1987
|
-
*
|
|
2035
|
+
* The route decorator groups of the application (RFC 0001 §5.1): every definition of a key must use
|
|
2036
|
+
* the same options and the decorators of a group must agree on `required` (a conflict is returned
|
|
2037
|
+
* at the second definition, which is then ignored); the required groups with their decorators.
|
|
2038
|
+
* `applied` are the entries of the targets the caller analysed (their first usage of a decorator
|
|
2039
|
+
* wins over its `definitionsOf` entry).
|
|
1988
2040
|
*/
|
|
1989
|
-
function
|
|
1990
|
-
const definitions = routeDecoratorDefinitions(ctx,
|
|
1991
|
-
if (definitions.length === 0) return
|
|
2041
|
+
function analyzeRouteGroups(ctx, applied) {
|
|
2042
|
+
const definitions = routeDecoratorDefinitions(ctx, applied);
|
|
2043
|
+
if (definitions.length === 0) return {
|
|
2044
|
+
conflicts: [],
|
|
2045
|
+
required: []
|
|
2046
|
+
};
|
|
2047
|
+
const conflicts = [];
|
|
1992
2048
|
const byKey = /* @__PURE__ */ new Map();
|
|
1993
2049
|
const byGroup = /* @__PURE__ */ new Map();
|
|
1994
2050
|
const members = /* @__PURE__ */ new Map();
|
|
1995
2051
|
for (const definition of definitions) {
|
|
1996
2052
|
const sameKey = byKey.get(definition.key);
|
|
1997
2053
|
if (sameKey !== void 0 && (sameKey.group !== definition.group || sameKey.required !== definition.required)) {
|
|
1998
|
-
|
|
2054
|
+
conflicts.push({
|
|
1999
2055
|
code: Codes.InvalidDecorator,
|
|
2000
2056
|
severity: "error",
|
|
2001
2057
|
message: `The route decorator ${definition.name} defines the key "${definition.key}" with ${describeOptions(definition)}, but ${sameKey.name} defines it with ${describeOptions(sameKey)}`,
|
|
@@ -2010,7 +2066,7 @@ function validateDecisions(ctx, controllers) {
|
|
|
2010
2066
|
}
|
|
2011
2067
|
const sameGroup = byGroup.get(definition.group);
|
|
2012
2068
|
if (sameGroup !== void 0 && sameGroup.required !== definition.required) {
|
|
2013
|
-
|
|
2069
|
+
conflicts.push({
|
|
2014
2070
|
code: Codes.InvalidDecorator,
|
|
2015
2071
|
severity: "error",
|
|
2016
2072
|
message: `The "${definition.group}" group is ${definition.required ? "" : "not "}required for ${definition.name} but ${sameGroup.required ? "" : "not "}required for ${sameGroup.name}`,
|
|
@@ -2029,7 +2085,112 @@ function validateDecisions(ctx, controllers) {
|
|
|
2029
2085
|
list.push(definition);
|
|
2030
2086
|
members.set(definition.group, list);
|
|
2031
2087
|
}
|
|
2032
|
-
const required = [
|
|
2088
|
+
const required = [];
|
|
2089
|
+
for (const first of byGroup.values()) {
|
|
2090
|
+
if (!first.required) continue;
|
|
2091
|
+
const decorators = [...members.get(first.group) ?? [first]].sort((a, b) => compareStrings(a.definition.file, b.definition.file) || a.definition.start - b.definition.start).map((member) => Object.freeze({ ...member }));
|
|
2092
|
+
const usages = decorators.map((member) => `@${member.name}(${member.flag ? "" : "..."})`);
|
|
2093
|
+
const suggestion = usages.length === 1 ? usages[0] : `${usages.slice(0, -1).join(", ")} or ${usages.at(-1)}`;
|
|
2094
|
+
required.push(Object.freeze({
|
|
2095
|
+
group: first.group,
|
|
2096
|
+
decorators: Object.freeze(decorators),
|
|
2097
|
+
suggestion
|
|
2098
|
+
}));
|
|
2099
|
+
}
|
|
2100
|
+
return {
|
|
2101
|
+
conflicts,
|
|
2102
|
+
required
|
|
2103
|
+
};
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* The required route decorator groups of the application (RFC 0001): what every route-like target
|
|
2107
|
+
* of another extension (a gateway, a GraphQL root field) must decide, exactly as HTTP routes must.
|
|
2108
|
+
* A target without a decision of a returned group is **ZNIGHT023**, reported by the caller.
|
|
2109
|
+
*
|
|
2110
|
+
* `applied` are the route metadata entries the caller read on its own targets
|
|
2111
|
+
* ({@link readRouteMeta}): they make decorators known even when the compiler has no
|
|
2112
|
+
* `definitionsOf`, and refine how flags are suggested. Inconsistent definitions are left out and
|
|
2113
|
+
* not reported here: the http extension reports them (ZNIGHT005) once.
|
|
2114
|
+
*
|
|
2115
|
+
* ```ts
|
|
2116
|
+
* validate(ctx) {
|
|
2117
|
+
* const required = requiredRouteGroups(ctx, ctx.stored.flatMap(gateway => gateway.meta))
|
|
2118
|
+
* for (const gateway of ctx.stored)
|
|
2119
|
+
* for (const group of required)
|
|
2120
|
+
* if (!gateway.meta.some(entry => entry.group === group.group))
|
|
2121
|
+
* ctx.report({ code: 'ZNIGHT023', severity: 'error', message: `${gateway.name} has no "${group.group}" decision`,
|
|
2122
|
+
* location: gateway.location, hints: [`Add ${group.suggestion} to ${gateway.name}`] })
|
|
2123
|
+
* }
|
|
2124
|
+
* ```
|
|
2125
|
+
*/
|
|
2126
|
+
function requiredRouteGroups(ctx, applied = []) {
|
|
2127
|
+
return analyzeRouteGroups(ctx, applied).required;
|
|
2128
|
+
}
|
|
2129
|
+
function describeOptions(entry) {
|
|
2130
|
+
const group = entry.group === entry.key ? "no group" : `group "${entry.group}"`;
|
|
2131
|
+
return entry.required ? `${group}, required` : group;
|
|
2132
|
+
}
|
|
2133
|
+
// src/compiler/validate.ts
|
|
2134
|
+
/** Validates the routes of every controller. */
|
|
2135
|
+
function validateHttp(ctx) {
|
|
2136
|
+
const controllers = [...ctx.stored].sort((a, b) => compareStrings(a.token, b.token));
|
|
2137
|
+
const registered = /* @__PURE__ */ new Map();
|
|
2138
|
+
for (const controller of controllers) {
|
|
2139
|
+
const baseIssue = checkRoutePath(controller.rawBasePath);
|
|
2140
|
+
if (baseIssue !== void 0) {
|
|
2141
|
+
invalidRoute(ctx, controller.pathLocation, `Invalid base path ${JSON.stringify(controller.rawBasePath)} of ${controller.displayName}: ${baseIssue.message}`, GRAMMAR_HINTS);
|
|
2142
|
+
continue;
|
|
2143
|
+
}
|
|
2144
|
+
for (const route of controller.routes) {
|
|
2145
|
+
const where = `${controller.displayName}.${route.handler}`;
|
|
2146
|
+
const ownIssue = checkRoutePath(route.rawPath);
|
|
2147
|
+
const issue = ownIssue ?? checkRoutePath(route.fullPath);
|
|
2148
|
+
if (issue !== void 0) {
|
|
2149
|
+
const path = ownIssue === void 0 ? route.fullPath : route.rawPath;
|
|
2150
|
+
invalidRoute(ctx, route.pathLocation, `Invalid route path ${JSON.stringify(path)} of ${where}: ${issue.message}`, GRAMMAR_HINTS);
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
const parsed = parseRoutePath(route.fullPath);
|
|
2154
|
+
checkParams(ctx, route, parsed, where);
|
|
2155
|
+
const key = `${route.method} ${parsed.shape}`;
|
|
2156
|
+
const previous = registered.get(key);
|
|
2157
|
+
if (previous === void 0) {
|
|
2158
|
+
registered.set(key, {
|
|
2159
|
+
where,
|
|
2160
|
+
route
|
|
2161
|
+
});
|
|
2162
|
+
continue;
|
|
2163
|
+
}
|
|
2164
|
+
ctx.report({
|
|
2165
|
+
code: Codes.InvalidRoute,
|
|
2166
|
+
severity: "error",
|
|
2167
|
+
message: `Duplicate route ${route.method} ${route.fullPath}: ${where} conflicts with ${previous.where} (${previous.route.method} ${previous.route.fullPath})`,
|
|
2168
|
+
location: route.location,
|
|
2169
|
+
hints: ["Two routes with the same method and the same path shape (parameter names aside) cannot coexist"],
|
|
2170
|
+
related: [{
|
|
2171
|
+
message: `${previous.where} is declared here`,
|
|
2172
|
+
location: previous.route.location
|
|
2173
|
+
}]
|
|
2174
|
+
});
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
validateDecisions(ctx, controllers);
|
|
2178
|
+
}
|
|
2179
|
+
/** Every route metadata entry of the controllers: controller entries, then route entries. */
|
|
2180
|
+
function* appliedEntries(controllers) {
|
|
2181
|
+
for (const controller of controllers) {
|
|
2182
|
+
yield* controller.meta;
|
|
2183
|
+
for (const route of controller.routes) yield* route.meta;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
2187
|
+
* Route decorator groups (RFC 0001 §5.1): every definition of a key uses the same options and the
|
|
2188
|
+
* decorators of a group agree on `required` (ZNIGHT005, at the second definition); every route has
|
|
2189
|
+
* a decision of every required group, its own or its controller's (ZNIGHT023).
|
|
2190
|
+
*/
|
|
2191
|
+
function validateDecisions(ctx, controllers) {
|
|
2192
|
+
const { conflicts, required } = analyzeRouteGroups(ctx, appliedEntries(controllers));
|
|
2193
|
+
for (const conflict of conflicts) ctx.report(conflict);
|
|
2033
2194
|
if (required.length === 0) return;
|
|
2034
2195
|
for (const controller of controllers) {
|
|
2035
2196
|
const controllerGroups = new Set(controller.meta.map((entry) => entry.group));
|
|
@@ -2037,18 +2198,15 @@ function validateDecisions(ctx, controllers) {
|
|
|
2037
2198
|
const decided = /* @__PURE__ */ new Set([...controllerGroups, ...route.meta.map((entry) => entry.group)]);
|
|
2038
2199
|
for (const group of required) {
|
|
2039
2200
|
if (decided.has(group.group)) continue;
|
|
2040
|
-
const inOrder = [...members.get(group.group) ?? [group]].sort((a, b) => compareStrings(a.definition.file, b.definition.file) || a.definition.start - b.definition.start);
|
|
2041
|
-
const decorators = inOrder.map((definition) => `@${definition.name}(${definition.flag ? "" : "..."})`);
|
|
2042
|
-
const listed = decorators.length === 1 ? decorators[0] : `${decorators.slice(0, -1).join(", ")} or ${decorators.at(-1)}`;
|
|
2043
2201
|
ctx.report({
|
|
2044
2202
|
code: Codes.MissingRouteDecision,
|
|
2045
2203
|
severity: "error",
|
|
2046
2204
|
message: `Route ${route.method} ${route.fullPath} (${controller.displayName}.${route.handler}) has no "${group.group}" decision`,
|
|
2047
2205
|
location: route.location,
|
|
2048
|
-
hints: [`Add ${
|
|
2206
|
+
hints: [`Add ${group.suggestion} to the route or to ${controller.displayName}`],
|
|
2049
2207
|
related: [{
|
|
2050
2208
|
message: `The "${group.group}" group is required`,
|
|
2051
|
-
location: (
|
|
2209
|
+
location: firstDefinition(group)
|
|
2052
2210
|
}, {
|
|
2053
2211
|
message: `${controller.displayName} is declared here`,
|
|
2054
2212
|
location: controller.location
|
|
@@ -2058,9 +2216,9 @@ function validateDecisions(ctx, controllers) {
|
|
|
2058
2216
|
}
|
|
2059
2217
|
}
|
|
2060
2218
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
return
|
|
2219
|
+
/** Where the first decorator of a group (source order) is defined. */
|
|
2220
|
+
function firstDefinition(group) {
|
|
2221
|
+
return group.decorators[0].definition;
|
|
2064
2222
|
}
|
|
2065
2223
|
const GRAMMAR_HINTS = ["Paths are made of static segments (letters, digits and - . _ ~ ! $ & ' + , ; = @), :name parameters and a trailing * wildcard"];
|
|
2066
2224
|
/** `:param` of the path ↔ `@Param('name')` parameters of the handler. */
|
|
@@ -2175,6 +2333,6 @@ const http = Object.freeze({
|
|
|
2175
2333
|
validate: validateHttp,
|
|
2176
2334
|
generate: generateHttp
|
|
2177
2335
|
});
|
|
2178
|
-
export { http as default };
|
|
2336
|
+
export { http as default, effectiveRouteMeta, readRouteMeta, requiredRouteGroups };
|
|
2179
2337
|
|
|
2180
2338
|
//# sourceMappingURL=compiler.mjs.map
|