@jskit-ai/kernel 0.1.55 → 0.1.57
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/package.json +3 -2
- package/server/actions/ActionRuntimeServiceProvider.test.js +23 -15
- package/server/http/lib/kernel.test.js +447 -0
- package/server/http/lib/routeRegistration.js +236 -15
- package/server/http/lib/routeTransport.js +126 -0
- package/server/http/lib/routeValidator.js +133 -198
- package/server/http/lib/routeValidator.test.js +385 -278
- package/server/http/lib/router.js +17 -2
- package/server/platform/providerRuntime.test.js +7 -7
- package/server/runtime/bootBootstrapRoutes.js +2 -18
- package/server/runtime/bootBootstrapRoutes.test.js +5 -14
- package/server/runtime/fastifyBootstrap.js +119 -0
- package/server/runtime/fastifyBootstrap.test.js +119 -1
- package/server/runtime/moduleConfig.js +32 -62
- package/server/runtime/moduleConfig.test.js +48 -24
- package/server/support/pageTargets.js +15 -9
- package/server/support/pageTargets.test.js +1 -1
- package/shared/actions/actionContributorHelpers.js +5 -11
- package/shared/actions/actionDefinitions.js +37 -150
- package/shared/actions/actionDefinitions.test.js +117 -136
- package/shared/actions/policies.js +25 -169
- package/shared/actions/policies.test.js +76 -87
- package/shared/actions/registry.test.js +24 -50
- package/shared/support/crudFieldContract.js +322 -0
- package/shared/support/crudFieldContract.test.js +67 -0
- package/shared/support/crudListFilters.js +582 -38
- package/shared/support/crudListFilters.test.js +178 -8
- package/shared/support/crudLookup.js +14 -7
- package/shared/support/crudLookup.test.js +91 -66
- package/shared/support/normalize.js +7 -0
- package/shared/support/normalize.test.js +4 -2
- package/shared/support/shellLayoutTargets.test.js +1 -1
- package/shared/validators/composeSchemaDefinitions.js +53 -0
- package/shared/validators/composeSchemaDefinitions.test.js +156 -0
- package/shared/validators/createCursorListValidator.js +22 -35
- package/shared/validators/createCursorListValidator.test.js +22 -23
- package/shared/validators/cursorPaginationQueryValidator.js +14 -24
- package/shared/validators/cursorPaginationQueryValidator.test.js +18 -8
- package/shared/validators/htmlTimeSchemas.js +6 -4
- package/shared/validators/index.js +15 -7
- package/shared/validators/jsonRestSchemaSupport.js +139 -0
- package/shared/validators/mergeObjectSchemas.js +44 -6
- package/shared/validators/mergeObjectSchemas.test.js +60 -35
- package/shared/validators/recordIdParamsValidator.js +19 -52
- package/shared/validators/recordIdParamsValidator.test.js +13 -8
- package/shared/validators/resourceRequiredMetadata.js +3 -3
- package/shared/validators/resourceRequiredMetadata.test.js +29 -16
- package/shared/validators/schemaDefinitions.js +126 -0
- package/shared/validators/schemaDefinitions.test.js +51 -0
- package/shared/validators/schemaPayloadValidation.js +65 -0
- package/test/barrelExposure.test.js +30 -0
- package/test/routeInputContractGuard.test.js +10 -6
- package/shared/validators/mergeValidators.js +0 -89
- package/shared/validators/mergeValidators.test.js +0 -116
- package/shared/validators/nestValidator.js +0 -53
- package/shared/validators/nestValidator.test.js +0 -60
- package/shared/validators/settingsFieldNormalization.js +0 -40
|
@@ -1,28 +1,137 @@
|
|
|
1
|
-
import { normalizeArray, normalizeObject } from "../../../shared/support/normalize.js";
|
|
1
|
+
import { normalizeArray, normalizeObject, normalizeText } from "../../../shared/support/normalize.js";
|
|
2
|
+
import { AppError } from "../../runtime/errors.js";
|
|
2
3
|
import { defaultApplyRoutePolicy } from "../../support/routePolicyConfig.js";
|
|
3
4
|
import { resolveDefaultSurfaceId } from "../../support/appConfig.js";
|
|
4
5
|
import { defaultMissingHandler } from "../../support/defaultMissingHandler.js";
|
|
6
|
+
import { registerJsonApiContentTypeParser } from "../../runtime/fastifyBootstrap.js";
|
|
5
7
|
import { RouteRegistrationError } from "./errors.js";
|
|
6
8
|
import { executeMiddlewareStack, normalizeRuntimeMiddlewareConfig, resolveRouteMiddlewareHandlers } from "./middlewareRuntime.js";
|
|
7
9
|
import { attachRequestScope } from "./requestScope.js";
|
|
8
10
|
import { attachRequestActionExecutor } from "./requestActionExecutor.js";
|
|
11
|
+
import { normalizeRouteOutputTransform, normalizeRouteTransport } from "./routeTransport.js";
|
|
9
12
|
|
|
10
13
|
const { structuredClone: cloneRouteSchema } = globalThis;
|
|
14
|
+
const UNSAFE_BODY_METHODS = Object.freeze(["POST", "PUT", "PATCH"]);
|
|
15
|
+
const JSON_API_CONTENT_TYPE = "application/vnd.api+json";
|
|
11
16
|
|
|
12
17
|
function toFastifyRouteOptions(route) {
|
|
13
18
|
const sourceRoute = normalizeObject(route);
|
|
14
19
|
const schema = cloneRouteSchema(sourceRoute.schema);
|
|
20
|
+
const existingConfig = normalizeObject(sourceRoute.config);
|
|
21
|
+
const transportKind = normalizeText(sourceRoute?.transport?.kind).toLowerCase();
|
|
22
|
+
const existingTransportConfig =
|
|
23
|
+
existingConfig.transport && typeof existingConfig.transport === "object" && !Array.isArray(existingConfig.transport)
|
|
24
|
+
? normalizeObject(existingConfig.transport)
|
|
25
|
+
: {};
|
|
15
26
|
return {
|
|
16
27
|
method: sourceRoute.method,
|
|
17
28
|
url: sourceRoute.path,
|
|
18
29
|
...(schema ? { schema } : {}),
|
|
19
30
|
...(sourceRoute.bodyLimit ? { bodyLimit: sourceRoute.bodyLimit } : {}),
|
|
20
31
|
config: {
|
|
21
|
-
...
|
|
32
|
+
...existingConfig,
|
|
33
|
+
...(transportKind
|
|
34
|
+
? {
|
|
35
|
+
transport: {
|
|
36
|
+
...existingTransportConfig,
|
|
37
|
+
kind: transportKind,
|
|
38
|
+
runtime: sourceRoute.transport,
|
|
39
|
+
...(normalizeText(sourceRoute?.transport?.contentType)
|
|
40
|
+
? {
|
|
41
|
+
contentType: normalizeText(sourceRoute.transport.contentType)
|
|
42
|
+
}
|
|
43
|
+
: {})
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
: {})
|
|
22
47
|
}
|
|
23
48
|
};
|
|
24
49
|
}
|
|
25
50
|
|
|
51
|
+
function normalizeHeaderValue(value) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return String(value[0] || "").trim();
|
|
54
|
+
}
|
|
55
|
+
return String(value || "").trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizeMediaType(value = "") {
|
|
59
|
+
return normalizeHeaderValue(value)
|
|
60
|
+
.split(";")[0]
|
|
61
|
+
.trim()
|
|
62
|
+
.toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function routeDefinesRequestBody(route = null) {
|
|
66
|
+
if (!route || typeof route !== "object") {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return route.body != null || route.schema?.body != null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function shouldEnforceRequestContentType(method = "", transport = null, route = null) {
|
|
74
|
+
return (
|
|
75
|
+
UNSAFE_BODY_METHODS.includes(String(method || "").toUpperCase()) &&
|
|
76
|
+
normalizeText(transport?.contentType).length > 0 &&
|
|
77
|
+
routeDefinesRequestBody(route)
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function routeRequiresJsonApiContentTypeParser(route = null) {
|
|
82
|
+
return routeDefinesRequestBody(route) && normalizeMediaType(route?.transport?.contentType) === JSON_API_CONTENT_TYPE;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function enforceRequestContentType({ request = null, route = null, transport = null } = {}) {
|
|
86
|
+
if (!shouldEnforceRequestContentType(route?.method, transport, route)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const expectedContentType = normalizeMediaType(transport?.contentType);
|
|
91
|
+
const actualContentType = normalizeMediaType(request?.headers?.["content-type"]);
|
|
92
|
+
if (actualContentType === expectedContentType) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
throw new AppError(415, `Content-Type must be ${transport.contentType}.`, {
|
|
97
|
+
code: "unsupported_media_type"
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function attachRouteTransport(request, transport = null) {
|
|
102
|
+
if (!request || !transport) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Object.defineProperty(request, "routeTransport", {
|
|
107
|
+
value: transport,
|
|
108
|
+
enumerable: false,
|
|
109
|
+
configurable: true,
|
|
110
|
+
writable: false
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function replyHasHeader(reply, name = "") {
|
|
115
|
+
const normalizedName = String(name || "").trim().toLowerCase();
|
|
116
|
+
if (!normalizedName || !reply) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof reply.hasHeader === "function") {
|
|
121
|
+
return reply.hasHeader(normalizedName);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof reply.getHeader === "function") {
|
|
125
|
+
return reply.getHeader(normalizedName) != null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (reply.headers && typeof reply.headers === "object") {
|
|
129
|
+
return Object.keys(reply.headers).some((key) => String(key || "").trim().toLowerCase() === normalizedName);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
26
135
|
function normalizeRouteInputTransforms(route) {
|
|
27
136
|
const routeInput = route?.input;
|
|
28
137
|
if (routeInput == null) {
|
|
@@ -37,7 +146,7 @@ function normalizeRouteInputTransforms(route) {
|
|
|
37
146
|
|
|
38
147
|
const normalized = {};
|
|
39
148
|
for (const key of ["body", "query", "params"]) {
|
|
40
|
-
if (!Object.
|
|
149
|
+
if (!Object.hasOwn(routeInput, key)) {
|
|
41
150
|
continue;
|
|
42
151
|
}
|
|
43
152
|
|
|
@@ -58,12 +167,34 @@ function normalizeRouteInputTransforms(route) {
|
|
|
58
167
|
return Object.freeze(normalized);
|
|
59
168
|
}
|
|
60
169
|
|
|
61
|
-
function buildRequestInput({ request = null, inputTransforms = null } = {}) {
|
|
170
|
+
async function buildRequestInput({ request = null, inputTransforms = null, transportInputTransforms = null } = {}) {
|
|
62
171
|
const transforms = inputTransforms && typeof inputTransforms === "object" ? inputTransforms : {};
|
|
172
|
+
const transportTransforms =
|
|
173
|
+
transportInputTransforms && typeof transportInputTransforms === "object" ? transportInputTransforms : {};
|
|
63
174
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
175
|
+
let body = request?.body;
|
|
176
|
+
if (body !== undefined && typeof transportTransforms.body === "function") {
|
|
177
|
+
body = await transportTransforms.body(body, request);
|
|
178
|
+
}
|
|
179
|
+
if (typeof transforms.body === "function") {
|
|
180
|
+
body = await transforms.body(body, request);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let query = request?.query;
|
|
184
|
+
if (typeof transportTransforms.query === "function") {
|
|
185
|
+
query = await transportTransforms.query(query, request);
|
|
186
|
+
}
|
|
187
|
+
if (typeof transforms.query === "function") {
|
|
188
|
+
query = await transforms.query(query, request);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let params = request?.params;
|
|
192
|
+
if (typeof transportTransforms.params === "function") {
|
|
193
|
+
params = await transportTransforms.params(params, request);
|
|
194
|
+
}
|
|
195
|
+
if (typeof transforms.params === "function") {
|
|
196
|
+
params = await transforms.params(params, request);
|
|
197
|
+
}
|
|
67
198
|
|
|
68
199
|
return Object.freeze({
|
|
69
200
|
body,
|
|
@@ -72,6 +203,62 @@ function buildRequestInput({ request = null, inputTransforms = null } = {}) {
|
|
|
72
203
|
});
|
|
73
204
|
}
|
|
74
205
|
|
|
206
|
+
function wrapReplySend({ reply = null, request = null, route = null, outputTransform = null, transport = null } = {}) {
|
|
207
|
+
if (!reply || typeof reply.send !== "function") {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const originalSend = reply.send.bind(reply);
|
|
212
|
+
reply.send = function transformedSend(payload) {
|
|
213
|
+
let nextPayload = payload;
|
|
214
|
+
if (typeof transport?.response === "function") {
|
|
215
|
+
const transportedPayload = transport.response(payload, {
|
|
216
|
+
request,
|
|
217
|
+
reply,
|
|
218
|
+
route,
|
|
219
|
+
transport,
|
|
220
|
+
statusCode: Number(reply?.statusCode || 200)
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (transportedPayload && typeof transportedPayload.then === "function") {
|
|
224
|
+
throw new RouteRegistrationError(
|
|
225
|
+
`Route ${String(route?.method || "<unknown>")} ${String(route?.path || "<unknown>")} transport.response must return synchronously.`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
nextPayload = transportedPayload === undefined ? nextPayload : transportedPayload;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (typeof outputTransform === "function") {
|
|
233
|
+
const transformedPayload = outputTransform(nextPayload, {
|
|
234
|
+
request,
|
|
235
|
+
reply,
|
|
236
|
+
route,
|
|
237
|
+
transport,
|
|
238
|
+
statusCode: Number(reply?.statusCode || 200)
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
if (transformedPayload && typeof transformedPayload.then === "function") {
|
|
242
|
+
throw new RouteRegistrationError(
|
|
243
|
+
`Route ${String(route?.method || "<unknown>")} ${String(route?.path || "<unknown>")} output transform must return synchronously.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
nextPayload = transformedPayload === undefined ? nextPayload : transformedPayload;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
normalizeText(transport?.contentType) &&
|
|
252
|
+
Number(reply?.statusCode || 200) !== 204 &&
|
|
253
|
+
!replyHasHeader(reply, "content-type")
|
|
254
|
+
) {
|
|
255
|
+
reply.header("Content-Type", transport.contentType);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return originalSend(nextPayload);
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
75
262
|
function registerRoutes(
|
|
76
263
|
fastify,
|
|
77
264
|
{
|
|
@@ -99,12 +286,29 @@ function registerRoutes(
|
|
|
99
286
|
const fallbackHandler = typeof missingHandler === "function" ? missingHandler : defaultMissingHandler;
|
|
100
287
|
const runtimeMiddlewareConfig = normalizeRuntimeMiddlewareConfig(middleware);
|
|
101
288
|
|
|
289
|
+
if (normalizedRoutes.some((route) => routeRequiresJsonApiContentTypeParser(route))) {
|
|
290
|
+
registerJsonApiContentTypeParser(fastify);
|
|
291
|
+
}
|
|
292
|
+
|
|
102
293
|
for (const route of normalizedRoutes) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
294
|
+
const routeTransport = normalizeRouteTransport(route?.transport, {
|
|
295
|
+
context: `Route ${String(route?.method || "<unknown>")} ${String(route?.path || "<unknown>")} transport`,
|
|
296
|
+
ErrorType: RouteRegistrationError
|
|
297
|
+
});
|
|
298
|
+
const routeOutputTransform = normalizeRouteOutputTransform(route?.output, {
|
|
299
|
+
context: `Route ${String(route?.method || "<unknown>")} ${String(route?.path || "<unknown>")} output`,
|
|
300
|
+
ErrorType: RouteRegistrationError
|
|
301
|
+
});
|
|
302
|
+
const normalizedRoute = {
|
|
303
|
+
...route,
|
|
304
|
+
transport: routeTransport,
|
|
305
|
+
output: routeOutputTransform
|
|
306
|
+
};
|
|
307
|
+
const baseOptions = toFastifyRouteOptions(normalizedRoute);
|
|
308
|
+
const routeOptions = policyApplier(baseOptions, normalizedRoute);
|
|
309
|
+
const routeHandler = typeof normalizedRoute?.handler === "function" ? normalizedRoute.handler : fallbackHandler;
|
|
310
|
+
const resolvedMiddlewareHandlers = resolveRouteMiddlewareHandlers(normalizedRoute, runtimeMiddlewareConfig);
|
|
311
|
+
const routeInputTransforms = normalizeRouteInputTransforms(normalizedRoute);
|
|
108
312
|
const routeActionDefaultSurface = resolveDefaultSurfaceId(null, {
|
|
109
313
|
defaultSurfaceId: route?.surface || routeOptions?.config?.surface || requestActionDefaultSurface
|
|
110
314
|
});
|
|
@@ -141,13 +345,30 @@ function registerRoutes(
|
|
|
141
345
|
defaultSurfaceId: routeActionDefaultSurface
|
|
142
346
|
});
|
|
143
347
|
|
|
144
|
-
|
|
145
|
-
|
|
348
|
+
attachRouteTransport(request, routeTransport);
|
|
349
|
+
enforceRequestContentType({
|
|
350
|
+
request,
|
|
351
|
+
route: normalizedRoute,
|
|
352
|
+
transport: routeTransport
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const transportInputTransforms = routeTransport?.request || null;
|
|
356
|
+
if (routeInputTransforms || transportInputTransforms) {
|
|
357
|
+
request.input = await buildRequestInput({
|
|
146
358
|
request,
|
|
147
|
-
inputTransforms: routeInputTransforms
|
|
359
|
+
inputTransforms: routeInputTransforms,
|
|
360
|
+
transportInputTransforms
|
|
148
361
|
});
|
|
149
362
|
}
|
|
150
363
|
|
|
364
|
+
wrapReplySend({
|
|
365
|
+
reply,
|
|
366
|
+
request,
|
|
367
|
+
route: normalizedRoute,
|
|
368
|
+
outputTransform: routeOutputTransform,
|
|
369
|
+
transport: routeTransport
|
|
370
|
+
});
|
|
371
|
+
|
|
151
372
|
await executeMiddlewareStack(resolvedMiddlewareHandlers, request, reply);
|
|
152
373
|
if (reply?.sent) {
|
|
153
374
|
return;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { normalizeObject, normalizeText } from "../../../shared/support/normalize.js";
|
|
2
|
+
|
|
3
|
+
const ROUTE_TRANSPORT_KINDS = Object.freeze([
|
|
4
|
+
"command",
|
|
5
|
+
"jsonapi-resource"
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
function normalizeRouteOutputTransform(value, { context = "route output", ErrorType = Error } = {}) {
|
|
9
|
+
if (value == null) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (typeof value !== "function") {
|
|
14
|
+
throw new ErrorType(`${context} must be a function.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizeRouteTransport(value, { context = "route transport", ErrorType = Error } = {}) {
|
|
21
|
+
if (value == null) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
26
|
+
throw new ErrorType(`${context} must be an object.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const source = normalizeObject(value);
|
|
30
|
+
const unsupportedKeys = Object.keys(source).filter(
|
|
31
|
+
(key) => !["kind", "request", "response", "error", "contentType"].includes(key)
|
|
32
|
+
);
|
|
33
|
+
if (unsupportedKeys.length > 0) {
|
|
34
|
+
throw new ErrorType(`${context}.${unsupportedKeys[0]} is not supported.`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const normalized = {};
|
|
38
|
+
|
|
39
|
+
if (Object.hasOwn(source, "kind")) {
|
|
40
|
+
const kind = normalizeText(source.kind).toLowerCase();
|
|
41
|
+
if (!kind) {
|
|
42
|
+
throw new ErrorType(`${context}.kind must be a non-empty string.`);
|
|
43
|
+
}
|
|
44
|
+
if (!ROUTE_TRANSPORT_KINDS.includes(kind)) {
|
|
45
|
+
throw new ErrorType(
|
|
46
|
+
`${context}.kind must be one of: ${ROUTE_TRANSPORT_KINDS.join(", ")}.`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
normalized.kind = kind;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Object.hasOwn(source, "request")) {
|
|
53
|
+
const request = source.request;
|
|
54
|
+
if (request != null) {
|
|
55
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
56
|
+
throw new ErrorType(`${context}.request must be an object.`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const requestSource = normalizeObject(request);
|
|
60
|
+
const unsupportedRequestKeys = Object.keys(requestSource).filter(
|
|
61
|
+
(key) => !["body", "query", "params"].includes(key)
|
|
62
|
+
);
|
|
63
|
+
if (unsupportedRequestKeys.length > 0) {
|
|
64
|
+
throw new ErrorType(`${context}.request.${unsupportedRequestKeys[0]} is not supported.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const normalizedRequest = {};
|
|
68
|
+
for (const key of ["body", "query", "params"]) {
|
|
69
|
+
if (!Object.hasOwn(requestSource, key)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const transform = requestSource[key];
|
|
74
|
+
if (transform == null) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof transform !== "function") {
|
|
79
|
+
throw new ErrorType(`${context}.request.${key} must be a function.`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
normalizedRequest[key] = transform;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (Object.keys(normalizedRequest).length > 0) {
|
|
86
|
+
normalized.request = Object.freeze(normalizedRequest);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (Object.hasOwn(source, "response")) {
|
|
92
|
+
const responseTransform = source.response;
|
|
93
|
+
if (responseTransform != null && typeof responseTransform !== "function") {
|
|
94
|
+
throw new ErrorType(`${context}.response must be a function.`);
|
|
95
|
+
}
|
|
96
|
+
if (responseTransform) {
|
|
97
|
+
normalized.response = responseTransform;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (Object.hasOwn(source, "error")) {
|
|
102
|
+
const errorTransform = source.error;
|
|
103
|
+
if (errorTransform != null && typeof errorTransform !== "function") {
|
|
104
|
+
throw new ErrorType(`${context}.error must be a function.`);
|
|
105
|
+
}
|
|
106
|
+
if (errorTransform) {
|
|
107
|
+
normalized.error = errorTransform;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (Object.hasOwn(source, "contentType")) {
|
|
112
|
+
const contentType = normalizeText(source.contentType);
|
|
113
|
+
if (!contentType) {
|
|
114
|
+
throw new ErrorType(`${context}.contentType must be a non-empty string.`);
|
|
115
|
+
}
|
|
116
|
+
normalized.contentType = contentType;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Object.freeze(normalized);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export {
|
|
123
|
+
ROUTE_TRANSPORT_KINDS,
|
|
124
|
+
normalizeRouteOutputTransform,
|
|
125
|
+
normalizeRouteTransport
|
|
126
|
+
};
|