@decohere/http 0.0.1-dev.20261105.97 → 0.0.1-dev.20261106.132

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 (70) hide show
  1. package/es6/src/routing/services/route-registry.js +5 -0
  2. package/package.json +9 -14
  3. package/commonjs/src/app/app.js +0 -166
  4. package/commonjs/src/app/index.js +0 -19
  5. package/commonjs/src/app/module.js +0 -17
  6. package/commonjs/src/auth/ability.js +0 -54
  7. package/commonjs/src/auth/auth.config.js +0 -23
  8. package/commonjs/src/auth/auth.extensions.js +0 -106
  9. package/commonjs/src/auth/auth.types.js +0 -65
  10. package/commonjs/src/auth/casl-authorization.service.js +0 -36
  11. package/commonjs/src/auth/index.js +0 -23
  12. package/commonjs/src/auth/principal-provider.js +0 -19
  13. package/commonjs/src/auth/safe-outbound-url.js +0 -102
  14. package/commonjs/src/auth/token-service.js +0 -151
  15. package/commonjs/src/builder/builder.js +0 -95
  16. package/commonjs/src/builder/index.js +0 -18
  17. package/commonjs/src/const.js +0 -86
  18. package/commonjs/src/health/health.controller.js +0 -75
  19. package/commonjs/src/health/health.extensions.js +0 -50
  20. package/commonjs/src/health/index.js +0 -21
  21. package/commonjs/src/health/readiness.service.js +0 -67
  22. package/commonjs/src/health/readiness.types.js +0 -7
  23. package/commonjs/src/hosting/hosted-service-host.js +0 -88
  24. package/commonjs/src/hosting/hosted-service.js +0 -11
  25. package/commonjs/src/hosting/index.js +0 -19
  26. package/commonjs/src/http/http.compression.js +0 -27
  27. package/commonjs/src/http/http.extensions.js +0 -269
  28. package/commonjs/src/http/http.options.js +0 -26
  29. package/commonjs/src/http/http.options.store.js +0 -20
  30. package/commonjs/src/http/http.security.js +0 -27
  31. package/commonjs/src/http/http.static.js +0 -248
  32. package/commonjs/src/http/index.js +0 -22
  33. package/commonjs/src/index.js +0 -27
  34. package/commonjs/src/openapi/index.js +0 -45
  35. package/commonjs/src/openapi/openapi.generator.js +0 -103
  36. package/commonjs/src/openapi/openapi.types.js +0 -4
  37. package/commonjs/src/openapi/request-schema.js +0 -187
  38. package/commonjs/src/openapi/request-validator.js +0 -85
  39. package/commonjs/src/providers/fastify.provider.js +0 -391
  40. package/commonjs/src/providers/index.js +0 -18
  41. package/commonjs/src/routing/decorators/argument.decorator.js +0 -49
  42. package/commonjs/src/routing/decorators/auth.decorator.js +0 -70
  43. package/commonjs/src/routing/decorators/controller.decorator.js +0 -16
  44. package/commonjs/src/routing/decorators/field.decorator.js +0 -25
  45. package/commonjs/src/routing/decorators/index.js +0 -24
  46. package/commonjs/src/routing/decorators/request.decorator.js +0 -30
  47. package/commonjs/src/routing/decorators/response.decorator.js +0 -34
  48. package/commonjs/src/routing/decorators/route.decorator.js +0 -67
  49. package/commonjs/src/routing/filters/exception-filter-host.js +0 -145
  50. package/commonjs/src/routing/filters/exception-filter.js +0 -28
  51. package/commonjs/src/routing/http.types.js +0 -99
  52. package/commonjs/src/routing/index.js +0 -25
  53. package/commonjs/src/routing/middleware/http-access-log.middleware.js +0 -47
  54. package/commonjs/src/routing/middleware/middleware.js +0 -20
  55. package/commonjs/src/routing/middleware/request-id.js +0 -30
  56. package/commonjs/src/routing/path.util.js +0 -51
  57. package/commonjs/src/routing/pipes/index.js +0 -18
  58. package/commonjs/src/routing/pipes/pipes.js +0 -145
  59. package/commonjs/src/routing/services/index.js +0 -18
  60. package/commonjs/src/routing/services/route-argument-resolver.js +0 -77
  61. package/commonjs/src/routing/services/route-registry.js +0 -194
  62. package/commonjs/src/routing/services/router.js +0 -158
  63. package/commonjs/src/sse/index.js +0 -26
  64. package/commonjs/src/sse/sse.decorator.js +0 -42
  65. package/commonjs/src/sse/sse.encoder.js +0 -73
  66. package/commonjs/src/sse/sse.types.js +0 -36
  67. package/commonjs/src/sse/sse.wire.js +0 -40
  68. package/commonjs/src/testing/boot-module.js +0 -38
  69. package/commonjs/src/testing/index.js +0 -20
  70. package/commonjs/src/testing/sse-reader.js +0 -98
@@ -1,248 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- function _export(target, all) {
6
- for(var name in all)Object.defineProperty(target, name, {
7
- enumerable: true,
8
- get: Object.getOwnPropertyDescriptor(all, name).get
9
- });
10
- }
11
- _export(exports, {
12
- get STATIC_DEFAULT_CACHE_CONTROL () {
13
- return STATIC_DEFAULT_CACHE_CONTROL;
14
- },
15
- get STATIC_DEFAULT_IMMUTABLE () {
16
- return STATIC_DEFAULT_IMMUTABLE;
17
- },
18
- get STATIC_DEFAULT_NO_CACHE () {
19
- return STATIC_DEFAULT_NO_CACHE;
20
- },
21
- get STATIC_IMMUTABLE_CACHE_CONTROL () {
22
- return STATIC_IMMUTABLE_CACHE_CONTROL;
23
- },
24
- get STATIC_NO_CACHE_CONTROL () {
25
- return STATIC_NO_CACHE_CONTROL;
26
- },
27
- get staticFiles () {
28
- return staticFiles;
29
- }
30
- });
31
- const _nodepath = /*#__PURE__*/ _interop_require_default(require("node:path"));
32
- const _picomatch = /*#__PURE__*/ _interop_require_default(require("picomatch"));
33
- const _authtypes = require("../auth/auth.types.js");
34
- function _interop_require_default(obj) {
35
- return obj && obj.__esModule ? obj : {
36
- default: obj
37
- };
38
- }
39
- const STATIC_IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
40
- const STATIC_NO_CACHE_CONTROL = "no-cache";
41
- const STATIC_DEFAULT_CACHE_CONTROL = "public, max-age=0, must-revalidate";
42
- const STATIC_DEFAULT_NO_CACHE = [
43
- "index.html",
44
- "remoteEntry.js"
45
- ];
46
- const STATIC_DEFAULT_IMMUTABLE = /[.-](?=[A-Za-z0-9_]*(?:\d|[a-z][A-Z]))[A-Za-z0-9_]{8,}\.[A-Za-z0-9]+(?:\.map)?$/;
47
- /** `"ui"`, `"/ui"` and `"/ui/"` all mean `/ui/`; `"/"` is the root. Refuses an empty prefix. */ function normalizePrefix(prefix) {
48
- const segments = prefix.split("/").filter((segment)=>segment !== "");
49
- if (segments.length === 0) {
50
- if (prefix.trim() === "") {
51
- throw new Error('withStatic needs a prefix, such as "/ui/" or "/" for the root.');
52
- }
53
- return "/";
54
- }
55
- if (segments.some((segment)=>segment === "." || segment === ".." || /[*:?]/.test(segment))) {
56
- throw new Error(`withStatic prefix "${prefix}" must be plain path segments, such as "/ui/".`);
57
- }
58
- return `/${segments.join("/")}/`;
59
- }
60
- function toMount(prefix, directory, options = {}) {
61
- if (directory.trim() === "") {
62
- throw new Error(`withStatic("${prefix}") needs the directory that holds the built files.`);
63
- }
64
- return {
65
- prefix: normalizePrefix(prefix),
66
- directory,
67
- options
68
- };
69
- }
70
- /**
71
- * Checks every mount at start. Returns the ones that can be served; a missing directory fails or
72
- * warns according to the mount's `onMissingDirectory`. Two mounts on one prefix always fail.
73
- */ async function resolve(mounts, env) {
74
- const cwd = env.cwd ?? process.cwd();
75
- const resolved = [];
76
- const seen = new Set();
77
- for (const mount of mounts){
78
- if (seen.has(mount.prefix)) {
79
- throw new Error(`withStatic: ${mount.prefix} is mounted twice.`);
80
- }
81
- seen.add(mount.prefix);
82
- const root = _nodepath.default.resolve(cwd, mount.directory);
83
- if (!await env.directoryExists(root)) {
84
- const policy = mount.options.onMissingDirectory ?? ((0, _authtypes.isDevelopmentNodeEnv)(env.nodeEnv) ? "warn" : "fail");
85
- const message = `withStatic("${mount.prefix}", "${mount.directory}"): ${root} does not exist. ` + "Build the UI first, or run the app from the folder that holds it.";
86
- if (policy === "fail") {
87
- throw new Error(message);
88
- }
89
- env.warn(`${message} ${mount.prefix} will answer 404 until it is built and the app restarts.`);
90
- continue;
91
- }
92
- const options = mount.options;
93
- resolved.push({
94
- prefix: mount.prefix,
95
- root,
96
- spaFallback: options.spaFallback ?? true,
97
- index: options.index ?? "index.html",
98
- noCache: options.noCache ?? STATIC_DEFAULT_NO_CACHE,
99
- immutable: options.immutable ?? STATIC_DEFAULT_IMMUTABLE,
100
- cacheControl: options.cacheControl ?? STATIC_DEFAULT_CACHE_CONTROL,
101
- exclude: options.exclude ?? []
102
- });
103
- }
104
- return resolved;
105
- }
106
- /** `Cache-Control` for a file, given its path relative to the mount's directory. */ function cacheControlFor(mount, relativePath) {
107
- const posix = relativePath.split(_nodepath.default.sep).join("/");
108
- const name = _nodepath.default.posix.basename(posix);
109
- if (mount.noCache.some((entry)=>entry === name || entry === posix)) {
110
- return STATIC_NO_CACHE_CONTROL;
111
- }
112
- if (mount.immutable !== false && mount.immutable.test(posix)) {
113
- return STATIC_IMMUTABLE_CACHE_CONTROL;
114
- }
115
- return mount.cacheControl;
116
- }
117
- /**
118
- * Does this path look like a file: an extension on the last segment, or a dotfile anywhere?
119
- * Then a miss is a real 404, not a client-side route.
120
- */ function looksLikeFile(pathname) {
121
- const segments = pathname.split("/").filter((segment)=>segment !== "");
122
- if (segments.some((segment)=>segment.startsWith("."))) {
123
- return true;
124
- }
125
- const last = pathname.endsWith("/") ? undefined : segments.at(-1);
126
- return last?.includes(".") === true;
127
- }
128
- /** The request path as the mount sees it: no query, decoded, repeated slashes collapsed. */ function normalizePathname(url) {
129
- const raw = url.split("?")[0] ?? "";
130
- let decoded = raw;
131
- try {
132
- decoded = decodeURIComponent(raw);
133
- } catch {
134
- // Keep the raw text: a malformed escape is matched as written.
135
- }
136
- return decoded.replace(/\/{2,}/g, "/");
137
- }
138
- /** Builds the test for a mount's `exclude` patterns. Matches on the normalised path. */ function excluder(patterns) {
139
- if (patterns.length === 0) {
140
- return ()=>false;
141
- }
142
- const matches = (0, _picomatch.default)([
143
- ...patterns
144
- ], {
145
- dot: true
146
- });
147
- return (pathname)=>matches(pathname);
148
- }
149
- /**
150
- * The first path segment of every registered route, for {@link isUnderRoute}. A segment that is a
151
- * parameter or wildcard (`:id`, `*`) says nothing about the path and is skipped.
152
- */ function routeSegments(routePaths) {
153
- const segments = new Set();
154
- for (const routePath of routePaths){
155
- const first = routePath.split("/").find((segment)=>segment !== "");
156
- if (first != null && !/[:*]/.test(first)) {
157
- segments.add(first);
158
- }
159
- }
160
- return segments;
161
- }
162
- /**
163
- * Is this path under one of the app's own routes? True when its first segment is the first
164
- * segment of a registered route: with a controller route `/lp/portfolio`, `/lp/portfolo` and
165
- * `/lp/anything/else` belong to the API and keep the JSON 404, while `/lpx` does not.
166
- */ function isUnderRoute(pathname, segments) {
167
- const first = pathname.split("/").find((segment)=>segment !== "");
168
- return first != null && segments.has(first);
169
- }
170
- /** Is `pathname` the path `base` or below it? `/api` holds `/api` and `/api/x`, not `/apiary`. */ function isWithin(pathname, base) {
171
- const trimmed = base.replace(/\/+$/, "");
172
- return pathname === trimmed || pathname.startsWith(`${trimmed}/`);
173
- }
174
- /**
175
- * The literal start of a route path, up to its first parameter or wildcard: `/webhooks/:id` gives
176
- * `/webhooks`, `/:id` gives `""`.
177
- */ function staticStem(routePath) {
178
- const stem = [];
179
- for (const segment of routePath.split("/")){
180
- if (segment.startsWith(":") || segment.includes("*")) {
181
- break;
182
- }
183
- if (segment !== "") {
184
- stem.push(segment);
185
- }
186
- }
187
- return stem.length === 0 ? "" : `/${stem.join("/")}`;
188
- }
189
- /**
190
- * What the fallback treats as the API, as a test on the normalised path. With a global prefix it
191
- * is the prefix, the OpenAPI path and every route left outside the prefix
192
- * (`@Controller({ globalPrefix: false })`, such as `/health`), each with the paths below it, so a
193
- * client route may share a first segment with a controller. Without a prefix it is
194
- * {@link isUnderRoute} (the first segment of every registered route), the safe guess when nothing
195
- * says where the API lives.
196
- */ function apiBoundary(boundary) {
197
- if (boundary.globalPrefix === "") {
198
- const segments = routeSegments([
199
- ...boundary.routePaths,
200
- ...boundary.openApiPath == null ? [] : [
201
- boundary.openApiPath
202
- ]
203
- ]);
204
- return (pathname)=>isUnderRoute(pathname, segments);
205
- }
206
- const outside = [
207
- ...boundary.routePaths
208
- ].filter((routePath)=>!isWithin(routePath, boundary.globalPrefix)).map(staticStem).filter((stem)=>stem !== "");
209
- const bases = [
210
- boundary.globalPrefix,
211
- boundary.openApiPath,
212
- ...outside
213
- ].filter((base)=>base != null);
214
- return (pathname)=>bases.some((base)=>isWithin(pathname, base));
215
- }
216
- /** Does the `Accept` header ask for HTML (a browser navigation)? `text/html;q=0` does not. */ function acceptsHtml(accept) {
217
- if (accept == null) {
218
- return false;
219
- }
220
- return accept.split(",").some((entry)=>{
221
- const [type = "", ...params] = entry.split(";").map((part)=>part.trim().toLowerCase());
222
- if (type !== "text/html") {
223
- return false;
224
- }
225
- const quality = params.find((param)=>param.startsWith("q="));
226
- return quality == null || Number.parseFloat(quality.slice(2)) > 0;
227
- });
228
- }
229
- /**
230
- * Should an unknown path get the app's `index.html`? Only a browser navigation: GET or HEAD, no
231
- * file extension and no dotfile in the path, an `Accept` that includes `text/html`, and not under
232
- * the API (see {@link apiBoundary}). Everything else keeps the normal JSON 404.
233
- */ function shouldFallBack(request, belongsToApi) {
234
- return (request.method === "GET" || request.method === "HEAD") && acceptsHtml(request.accept) && !looksLikeFile(request.pathname) && !belongsToApi(request.pathname);
235
- }
236
- const staticFiles = {
237
- toMount,
238
- resolve,
239
- cacheControlFor,
240
- looksLikeFile,
241
- normalizePathname,
242
- excluder,
243
- routeSegments,
244
- isUnderRoute,
245
- apiBoundary,
246
- acceptsHtml,
247
- shouldFallBack
248
- };
@@ -1,22 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- _export_star(require("./http.compression.js"), exports);
6
- _export_star(require("./http.extensions.js"), exports);
7
- _export_star(require("./http.options.js"), exports);
8
- _export_star(require("./http.security.js"), exports);
9
- _export_star(require("./http.static.js"), exports);
10
- function _export_star(from, to) {
11
- Object.keys(from).forEach(function(k) {
12
- if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
13
- Object.defineProperty(to, k, {
14
- enumerable: true,
15
- get: function() {
16
- return from[k];
17
- }
18
- });
19
- }
20
- });
21
- return from;
22
- }
@@ -1,27 +0,0 @@
1
- // Resolve StartOptions re-export ambiguity (app vs routing) — prefer app's shape
2
- "use strict";
3
- Object.defineProperty(exports, "__esModule", {
4
- value: true
5
- });
6
- _export_star(require("./app/index.js"), exports);
7
- _export_star(require("./auth/index.js"), exports);
8
- _export_star(require("./builder/index.js"), exports);
9
- _export_star(require("./const.js"), exports);
10
- _export_star(require("./health/index.js"), exports);
11
- _export_star(require("./hosting/index.js"), exports);
12
- _export_star(require("./openapi/index.js"), exports);
13
- _export_star(require("./routing/index.js"), exports);
14
- _export_star(require("./sse/index.js"), exports);
15
- function _export_star(from, to) {
16
- Object.keys(from).forEach(function(k) {
17
- if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
18
- Object.defineProperty(to, k, {
19
- enumerable: true,
20
- get: function() {
21
- return from[k];
22
- }
23
- });
24
- }
25
- });
26
- return from;
27
- }
@@ -1,45 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- function _export(target, all) {
6
- for(var name in all)Object.defineProperty(target, name, {
7
- enumerable: true,
8
- get: Object.getOwnPropertyDescriptor(all, name).get
9
- });
10
- }
11
- _export(exports, {
12
- get ValidationError () {
13
- return _requestvalidator.ValidationError;
14
- },
15
- get buildRouteRequestSchemas () {
16
- return _requestschema.buildRouteRequestSchemas;
17
- },
18
- get generateOpenApiDocument () {
19
- return _openapigenerator.generateOpenApiDocument;
20
- },
21
- get mergeObjectSchemas () {
22
- return _requestschema.mergeObjectSchemas;
23
- },
24
- get objectArgToParts () {
25
- return _requestschema.objectArgToParts;
26
- },
27
- get pathParamsToSchema () {
28
- return _requestschema.pathParamsToSchema;
29
- },
30
- get resolveOpenApiPath () {
31
- return _openapigenerator.resolveOpenApiPath;
32
- },
33
- get toOpenApiPath () {
34
- return _openapigenerator.toOpenApiPath;
35
- },
36
- get typeToSchema () {
37
- return _requestschema.typeToSchema;
38
- },
39
- get validateRequest () {
40
- return _requestvalidator.validateRequest;
41
- }
42
- });
43
- const _openapigenerator = require("./openapi.generator.js");
44
- const _requestschema = require("./request-schema.js");
45
- const _requestvalidator = require("./request-validator.js");
@@ -1,103 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- function _export(target, all) {
6
- for(var name in all)Object.defineProperty(target, name, {
7
- enumerable: true,
8
- get: Object.getOwnPropertyDescriptor(all, name).get
9
- });
10
- }
11
- _export(exports, {
12
- get generateOpenApiDocument () {
13
- return generateOpenApiDocument;
14
- },
15
- get resolveOpenApiPath () {
16
- return resolveOpenApiPath;
17
- },
18
- get toOpenApiPath () {
19
- return toOpenApiPath;
20
- }
21
- });
22
- const _requestschema = require("./request-schema.js");
23
- const DEFAULT_SPEC_PATH = "/openapi.json";
24
- function resolveOpenApiPath(options) {
25
- const path = options.path?.trim() || DEFAULT_SPEC_PATH;
26
- return path.startsWith("/") ? path : `/${path}`;
27
- }
28
- function toOpenApiPath(fastifyPath) {
29
- return fastifyPath.replace(/:([A-Za-z_][\w]*)/g, "{$1}");
30
- }
31
- function generateOpenApiDocument(registry, options) {
32
- const paths = {};
33
- for (const route of registry.routes){
34
- const path = toOpenApiPath(route.path);
35
- const method = route.method.toLowerCase();
36
- let pathItem = paths[path];
37
- if (pathItem == null) {
38
- pathItem = {};
39
- paths[path] = pathItem;
40
- }
41
- pathItem[method] = buildOperation(route);
42
- }
43
- const document = {
44
- openapi: "3.0.3",
45
- info: options.info,
46
- paths
47
- };
48
- if (options.servers?.length) {
49
- document.servers = options.servers;
50
- }
51
- return document;
52
- }
53
- function buildOperation(route) {
54
- const { queryParams, pathParams, body: bodySchema } = (0, _requestschema.buildRouteRequestSchemas)(route);
55
- const parameters = [
56
- ...queryParams,
57
- ...pathParams
58
- ];
59
- const status = String(route.httpCode ?? 200);
60
- const operation = {
61
- operationId: `${route.controller.name}.${route.handler}`,
62
- tags: [
63
- route.controller.name
64
- ],
65
- responses: {
66
- [status]: route.sse ? {
67
- description: "Server-sent event stream",
68
- content: {
69
- "text/event-stream": {
70
- schema: {
71
- type: "string"
72
- }
73
- }
74
- }
75
- } : {
76
- description: "Successful response"
77
- }
78
- }
79
- };
80
- if (route.routeArgs?.some((arg)=>arg.argumentLocation === "lastEventId")) {
81
- parameters.push({
82
- name: "Last-Event-ID",
83
- in: "header",
84
- schema: {
85
- type: "string"
86
- }
87
- });
88
- }
89
- if (parameters.length > 0) {
90
- operation.parameters = parameters;
91
- }
92
- if (bodySchema != null) {
93
- operation.requestBody = {
94
- required: true,
95
- content: {
96
- "application/json": {
97
- schema: bodySchema
98
- }
99
- }
100
- };
101
- }
102
- return operation;
103
- }
@@ -1,4 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
@@ -1,187 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", {
3
- value: true
4
- });
5
- function _export(target, all) {
6
- for(var name in all)Object.defineProperty(target, name, {
7
- enumerable: true,
8
- get: Object.getOwnPropertyDescriptor(all, name).get
9
- });
10
- }
11
- _export(exports, {
12
- get buildRouteRequestSchemas () {
13
- return buildRouteRequestSchemas;
14
- },
15
- get mergeObjectSchemas () {
16
- return mergeObjectSchemas;
17
- },
18
- get objectArgToParts () {
19
- return objectArgToParts;
20
- },
21
- get pathParamsToSchema () {
22
- return pathParamsToSchema;
23
- },
24
- get typeToSchema () {
25
- return typeToSchema;
26
- }
27
- });
28
- const _reflection = require("@decohere/core/reflection");
29
- const _httptypes = require("../routing/http.types.js");
30
- function buildRouteRequestSchemas(route) {
31
- const queryParams = [];
32
- const pathParams = [];
33
- let bodySchema;
34
- for (const routeArg of route.routeArgs ?? []){
35
- if (routeArg.argumentLocation != null && _httptypes.InjectedParts.includes(routeArg.argumentLocation)) {
36
- continue;
37
- }
38
- if ((0, _reflection.isPrimitiveOrDateType)(routeArg.argumentType)) {
39
- const location = routeArg.argumentLocation;
40
- if (location === "query" || location === "params") {
41
- const param = {
42
- name: routeArg.mappingKey ?? "value",
43
- in: location === "params" ? "path" : "query",
44
- required: location === "params",
45
- schema: typeToSchema(routeArg.argumentType.name)
46
- };
47
- if (location === "params") {
48
- pathParams.push(param);
49
- } else {
50
- queryParams.push(param);
51
- }
52
- }
53
- continue;
54
- }
55
- const parts = objectArgToParts(routeArg);
56
- queryParams.push(...parts.queryParams);
57
- pathParams.push(...parts.pathParams);
58
- if (parts.body != null) {
59
- bodySchema = mergeObjectSchemas(bodySchema, parts.body);
60
- }
61
- }
62
- return {
63
- queryParams,
64
- pathParams,
65
- body: bodySchema
66
- };
67
- }
68
- function objectArgToParts(routeArg) {
69
- const queryParams = [];
70
- const pathParams = [];
71
- const bodyProperties = {};
72
- const bodyRequired = [];
73
- const argLocation = routeArg.argumentLocation;
74
- for (const [, meta] of routeArg.fieldMap?.entries() ?? []){
75
- const location = meta.fieldLocation ?? argLocation;
76
- const name = meta.mappingKey || meta.fieldKey;
77
- const schema = typeToSchema(meta.fieldType?.name);
78
- if (location === "query") {
79
- queryParams.push({
80
- name,
81
- in: "query",
82
- required: false,
83
- schema
84
- });
85
- } else if (location === "params") {
86
- pathParams.push({
87
- name,
88
- in: "path",
89
- required: true,
90
- schema
91
- });
92
- } else if (location === "body" || location == null && argLocation === "body") {
93
- bodyProperties[name] = schema;
94
- bodyRequired.push(name);
95
- } else if (location == null && argLocation == null) {
96
- // Unlocated object fields default to body for OpenAPI (common DTO pattern).
97
- bodyProperties[name] = schema;
98
- bodyRequired.push(name);
99
- }
100
- }
101
- // Whole-object body argument with no fields still advertises an object body.
102
- if (argLocation === "body" && Object.keys(bodyProperties).length === 0) {
103
- return {
104
- queryParams,
105
- pathParams,
106
- body: {
107
- type: "object",
108
- additionalProperties: true
109
- }
110
- };
111
- }
112
- const body = Object.keys(bodyProperties).length > 0 ? {
113
- type: "object",
114
- properties: bodyProperties,
115
- required: bodyRequired,
116
- additionalProperties: false
117
- } : undefined;
118
- return {
119
- queryParams,
120
- pathParams,
121
- body
122
- };
123
- }
124
- function mergeObjectSchemas(a, b) {
125
- if (a == null) {
126
- return b;
127
- }
128
- return {
129
- type: "object",
130
- properties: {
131
- ...a.properties,
132
- ...b.properties
133
- },
134
- required: [
135
- ...new Set([
136
- ...a.required ?? [],
137
- ...b.required ?? []
138
- ])
139
- ],
140
- additionalProperties: false
141
- };
142
- }
143
- function typeToSchema(typeName) {
144
- switch(typeName){
145
- case "Number":
146
- return {
147
- type: "number"
148
- };
149
- case "Boolean":
150
- return {
151
- type: "boolean"
152
- };
153
- case "Date":
154
- return {
155
- type: "string",
156
- format: "date-time"
157
- };
158
- case "String":
159
- return {
160
- type: "string"
161
- };
162
- default:
163
- return {};
164
- }
165
- }
166
- function pathParamsToSchema(params) {
167
- if (params.length === 0) {
168
- return undefined;
169
- }
170
- const properties = {};
171
- const required = [];
172
- for (const param of params){
173
- properties[param.name] = {
174
- type: "string",
175
- minLength: 1
176
- };
177
- if (param.required !== false) {
178
- required.push(param.name);
179
- }
180
- }
181
- return {
182
- type: "object",
183
- properties,
184
- required,
185
- additionalProperties: true
186
- };
187
- }