@jskit-ai/kernel 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 +24 -0
- package/_testable/index.js +4 -0
- package/client/appConfig.js +33 -0
- package/client/componentInteraction.js +51 -0
- package/client/componentInteraction.test.js +111 -0
- package/client/descriptorSections.js +75 -0
- package/client/index.d.ts +70 -0
- package/client/index.js +3 -0
- package/client/logging.js +38 -0
- package/client/moduleBootstrap.js +670 -0
- package/client/moduleBootstrap.test.js +403 -0
- package/client/shellBootstrap.js +233 -0
- package/client/shellBootstrap.test.js +185 -0
- package/client/shellRouting.js +321 -0
- package/client/shellRouting.test.js +113 -0
- package/client/vite/clientBootstrapPlugin.js +259 -0
- package/client/vite/clientBootstrapPlugin.test.js +563 -0
- package/client/vite/index.js +3 -0
- package/internal/node/fileSystem.js +21 -0
- package/internal/node/installedPackageDescriptor.js +104 -0
- package/package.json +43 -0
- package/server/actions/ActionRuntimeServiceProvider.js +309 -0
- package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
- package/server/actions/index.js +8 -0
- package/server/container/ContainerCoreServiceProvider.js +27 -0
- package/server/container/index.js +10 -0
- package/server/exportPolicy.test.js +68 -0
- package/server/http/HttpFastifyServiceProvider.js +25 -0
- package/server/http/_testable/index.js +2 -0
- package/server/http/index.js +1 -0
- package/server/http/lib/controller.js +183 -0
- package/server/http/lib/controller.test.js +143 -0
- package/server/http/lib/errors.js +12 -0
- package/server/http/lib/httpRuntime.js +82 -0
- package/server/http/lib/index.js +18 -0
- package/server/http/lib/kernel.js +15 -0
- package/server/http/lib/kernel.test.js +880 -0
- package/server/http/lib/middlewareRuntime.js +149 -0
- package/server/http/lib/requestActionExecutor.js +258 -0
- package/server/http/lib/requestScope.js +59 -0
- package/server/http/lib/routeRegistration.js +165 -0
- package/server/http/lib/routeSupport.js +45 -0
- package/server/http/lib/routeValidator.js +469 -0
- package/server/http/lib/routeValidator.test.js +474 -0
- package/server/http/lib/router.js +206 -0
- package/server/kernel/KernelCoreServiceProvider.js +27 -0
- package/server/kernel/index.js +10 -0
- package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
- package/server/platform/index.js +5 -0
- package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
- package/server/platform/providerRuntime/helpers.js +45 -0
- package/server/platform/providerRuntime/lockfile.js +27 -0
- package/server/platform/providerRuntime/providerLoader.js +283 -0
- package/server/platform/providerRuntime.js +142 -0
- package/server/platform/providerRuntime.test.js +217 -0
- package/server/platform/runtime.js +40 -0
- package/server/platform/surfaceRuntime.js +150 -0
- package/server/platform/surfaceRuntime.test.js +136 -0
- package/server/registries/actionSurfaceSourceRegistry.js +150 -0
- package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
- package/server/registries/domainEventListenerRegistry.js +61 -0
- package/server/registries/index.js +36 -0
- package/server/registries/primitives.js +63 -0
- package/server/registries/routeVisibilityResolverRegistry.js +87 -0
- package/server/registries/serviceRegistrationRegistry.js +431 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
- package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
- package/server/runtime/apiRoutePolicyParity.test.js +109 -0
- package/server/runtime/apiRouteRegistration.js +65 -0
- package/server/runtime/bootBootstrapRoutes.js +46 -0
- package/server/runtime/bootBootstrapRoutes.test.js +79 -0
- package/server/runtime/bootstrapContributors.test.js +114 -0
- package/server/runtime/canonicalJson.js +74 -0
- package/server/runtime/composition.js +142 -0
- package/server/runtime/domainEvents.test.js +114 -0
- package/server/runtime/domainRules.js +50 -0
- package/server/runtime/domainRules.test.js +87 -0
- package/server/runtime/entityChangeEvents.js +182 -0
- package/server/runtime/entityChangeEvents.test.js +211 -0
- package/server/runtime/errors.js +68 -0
- package/server/runtime/errors.test.js +73 -0
- package/server/runtime/fastifyBootstrap.js +372 -0
- package/server/runtime/fastifyBootstrap.test.js +194 -0
- package/server/runtime/index.js +6 -0
- package/server/runtime/integers.js +13 -0
- package/server/runtime/moduleConfig.js +269 -0
- package/server/runtime/moduleConfig.test.js +141 -0
- package/server/runtime/pagination.js +13 -0
- package/server/runtime/realtimeNormalization.js +21 -0
- package/server/runtime/requestUrl.js +38 -0
- package/server/runtime/routeUtils.js +20 -0
- package/server/runtime/runtimeAssembly.js +113 -0
- package/server/runtime/runtimeKernel.js +55 -0
- package/server/runtime/securityAudit.js +269 -0
- package/server/runtime/securityAudit.test.js +41 -0
- package/server/runtime/serviceAuthorization.js +113 -0
- package/server/runtime/serviceAuthorization.test.js +100 -0
- package/server/runtime/serviceRegistration.test.js +197 -0
- package/server/support/SupportCoreServiceProvider.js +25 -0
- package/server/support/appConfig.js +37 -0
- package/server/support/appConfig.test.js +94 -0
- package/server/support/defaultMissingHandler.js +7 -0
- package/server/support/index.js +2 -0
- package/server/support/routePolicyConfig.js +51 -0
- package/server/support/symlinkSafeRequire.js +78 -0
- package/server/support/symlinkSafeRequire.test.js +27 -0
- package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
- package/server/surface/index.js +19 -0
- package/shared/actions/actionContributorHelpers.js +34 -0
- package/shared/actions/actionContributorHelpers.test.js +16 -0
- package/shared/actions/actionDefinitions.js +488 -0
- package/shared/actions/actionDefinitions.test.js +212 -0
- package/shared/actions/audit.js +7 -0
- package/shared/actions/executionContext.js +97 -0
- package/shared/actions/executionContext.test.js +66 -0
- package/shared/actions/idempotency.js +62 -0
- package/shared/actions/index.js +2 -0
- package/shared/actions/observability.js +10 -0
- package/shared/actions/pipeline.js +287 -0
- package/shared/actions/policies.js +342 -0
- package/shared/actions/policies.test.js +233 -0
- package/shared/actions/registry.js +187 -0
- package/shared/actions/registry.test.js +381 -0
- package/shared/actions/requestMeta.js +36 -0
- package/shared/actions/textNormalization.js +3 -0
- package/shared/actions/withActionDefaults.js +34 -0
- package/shared/index.js +2 -0
- package/shared/runtime/application.js +323 -0
- package/shared/runtime/container.js +261 -0
- package/shared/runtime/containerErrors.js +22 -0
- package/shared/runtime/index.js +18 -0
- package/shared/runtime/kernelErrors.js +20 -0
- package/shared/runtime/serviceProvider.js +13 -0
- package/shared/support/formatDateTime.js +10 -0
- package/shared/support/formatDateTime.test.js +15 -0
- package/shared/support/index.js +14 -0
- package/shared/support/linkPath.js +67 -0
- package/shared/support/linkPath.test.js +35 -0
- package/shared/support/normalize.js +116 -0
- package/shared/support/normalize.test.js +48 -0
- package/shared/support/packageDescriptor.test.js +121 -0
- package/shared/support/permissions.js +50 -0
- package/shared/support/pickOwnProperties.js +17 -0
- package/shared/support/pickOwnProperties.test.js +25 -0
- package/shared/support/policies.js +11 -0
- package/shared/support/queryPath.js +33 -0
- package/shared/support/queryPath.test.js +19 -0
- package/shared/support/queryResilience.js +34 -0
- package/shared/support/queryResilience.test.js +33 -0
- package/shared/support/returnToPath.js +153 -0
- package/shared/support/returnToPath.test.js +123 -0
- package/shared/support/sorting.js +15 -0
- package/shared/support/tokens.js +23 -0
- package/shared/support/tokens.test.js +17 -0
- package/shared/support/visibility.js +56 -0
- package/shared/support/visibility.test.js +45 -0
- package/shared/surface/apiPaths.js +84 -0
- package/shared/surface/escapeRegExp.js +5 -0
- package/shared/surface/index.js +6 -0
- package/shared/surface/paths.js +273 -0
- package/shared/surface/registry.js +135 -0
- package/shared/surface/registry.test.js +44 -0
- package/shared/surface/runtime.js +357 -0
- package/shared/surface/runtime.test.js +319 -0
- package/shared/validators/createCursorListValidator.js +42 -0
- package/shared/validators/createCursorListValidator.test.js +34 -0
- package/shared/validators/cursorPaginationQueryValidator.js +31 -0
- package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
- package/shared/validators/index.js +12 -0
- package/shared/validators/inputNormalization.js +13 -0
- package/shared/validators/mergeObjectSchemas.js +31 -0
- package/shared/validators/mergeObjectSchemas.test.js +67 -0
- package/shared/validators/mergeValidators.js +89 -0
- package/shared/validators/mergeValidators.test.js +116 -0
- package/shared/validators/nestValidator.js +53 -0
- package/shared/validators/nestValidator.test.js +60 -0
- package/shared/validators/recordIdParamsValidator.js +36 -0
- package/shared/validators/recordIdParamsValidator.test.js +20 -0
- package/shared/validators/resourceRequiredMetadata.js +41 -0
- package/shared/validators/resourceRequiredMetadata.test.js +49 -0
- package/test/barrelExposure.test.js +106 -0
- package/test/dynamicImportPolicy.test.js +89 -0
- package/test/exportsContract.test.js +168 -0
- package/test/routeInputContractGuard.test.js +78 -0
- package/test/surfaceIndependence.test.js +109 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
|
|
4
|
+
import { createRouter } from "./router.js";
|
|
5
|
+
import { compileRouteValidator, defineRouteValidator, resolveRouteValidatorOptions } from "./routeValidator.js";
|
|
6
|
+
|
|
7
|
+
test("defineRouteValidator compiles body/query/params and maps query schema to querystring", () => {
|
|
8
|
+
const bodySchema = {
|
|
9
|
+
type: "object"
|
|
10
|
+
};
|
|
11
|
+
const querySchema = {
|
|
12
|
+
type: "object"
|
|
13
|
+
};
|
|
14
|
+
const paramsSchema = {
|
|
15
|
+
type: "object"
|
|
16
|
+
};
|
|
17
|
+
const responseSchema = {
|
|
18
|
+
200: {
|
|
19
|
+
schema: {
|
|
20
|
+
type: "object"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const headersSchema = {
|
|
25
|
+
type: "object"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const normalizeBody = (body) => body;
|
|
29
|
+
const normalizeQuery = (query) => query;
|
|
30
|
+
const normalizeParams = (params) => params;
|
|
31
|
+
|
|
32
|
+
const validator = defineRouteValidator({
|
|
33
|
+
meta: {
|
|
34
|
+
tags: ["contacts", "intake"],
|
|
35
|
+
summary: "Create contact intake"
|
|
36
|
+
},
|
|
37
|
+
bodyValidator: {
|
|
38
|
+
schema: bodySchema,
|
|
39
|
+
normalize: normalizeBody
|
|
40
|
+
},
|
|
41
|
+
queryValidator: {
|
|
42
|
+
schema: querySchema,
|
|
43
|
+
normalize: normalizeQuery
|
|
44
|
+
},
|
|
45
|
+
paramsValidator: {
|
|
46
|
+
schema: paramsSchema
|
|
47
|
+
},
|
|
48
|
+
responseValidators: responseSchema,
|
|
49
|
+
advanced: {
|
|
50
|
+
fastifySchema: {
|
|
51
|
+
headers: headersSchema
|
|
52
|
+
},
|
|
53
|
+
jskitInput: {
|
|
54
|
+
params: normalizeParams
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const compiled = validator.toRouteOptions();
|
|
60
|
+
|
|
61
|
+
assert.deepEqual(compiled.schema, {
|
|
62
|
+
tags: ["contacts", "intake"],
|
|
63
|
+
summary: "Create contact intake",
|
|
64
|
+
body: bodySchema,
|
|
65
|
+
querystring: querySchema,
|
|
66
|
+
params: paramsSchema,
|
|
67
|
+
response: {
|
|
68
|
+
200: {
|
|
69
|
+
type: "object"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
headers: headersSchema
|
|
73
|
+
});
|
|
74
|
+
assert.equal(compiled.input.body, normalizeBody);
|
|
75
|
+
assert.equal(compiled.input.query, normalizeQuery);
|
|
76
|
+
assert.equal(compiled.input.params, normalizeParams);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("compileRouteValidator accepts plain validator objects", () => {
|
|
80
|
+
const querySchema = {
|
|
81
|
+
type: "object"
|
|
82
|
+
};
|
|
83
|
+
const normalizeQuery = (query) => ({
|
|
84
|
+
dryRun: Boolean(query?.dryRun)
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const compiled = compileRouteValidator({
|
|
88
|
+
queryValidator: {
|
|
89
|
+
schema: querySchema,
|
|
90
|
+
normalize: normalizeQuery
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
assert.deepEqual(compiled.schema, {
|
|
95
|
+
querystring: querySchema
|
|
96
|
+
});
|
|
97
|
+
assert.equal(compiled.input.query, normalizeQuery);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("compileRouteValidator creates pass-through request.input transforms for schema-only params and query", () => {
|
|
101
|
+
const querySchema = {
|
|
102
|
+
type: "object"
|
|
103
|
+
};
|
|
104
|
+
const paramsSchema = {
|
|
105
|
+
type: "object"
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const compiled = compileRouteValidator({
|
|
109
|
+
queryValidator: {
|
|
110
|
+
schema: querySchema
|
|
111
|
+
},
|
|
112
|
+
paramsValidator: {
|
|
113
|
+
schema: paramsSchema
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.deepEqual(compiled.schema, {
|
|
118
|
+
querystring: querySchema,
|
|
119
|
+
params: paramsSchema
|
|
120
|
+
});
|
|
121
|
+
assert.equal(typeof compiled.input.query, "function");
|
|
122
|
+
assert.equal(typeof compiled.input.params, "function");
|
|
123
|
+
assert.deepEqual(compiled.input.query({ workspaceSlug: "acme" }), { workspaceSlug: "acme" });
|
|
124
|
+
assert.deepEqual(compiled.input.params({ workspaceSlug: "acme" }), { workspaceSlug: "acme" });
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("compileRouteValidator accepts response validator objects and extracts only response schemas", () => {
|
|
128
|
+
const responseBodySchema = {
|
|
129
|
+
type: "object"
|
|
130
|
+
};
|
|
131
|
+
const normalizeOutput = (payload) => ({
|
|
132
|
+
...payload,
|
|
133
|
+
normalized: true
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const compiled = compileRouteValidator({
|
|
137
|
+
responseValidators: {
|
|
138
|
+
200: {
|
|
139
|
+
schema: responseBodySchema,
|
|
140
|
+
normalize: normalizeOutput
|
|
141
|
+
},
|
|
142
|
+
400: {
|
|
143
|
+
schema: {
|
|
144
|
+
type: "object"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
assert.deepEqual(compiled.schema, {
|
|
151
|
+
response: {
|
|
152
|
+
200: responseBodySchema,
|
|
153
|
+
400: {
|
|
154
|
+
type: "object"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
assert.equal(Object.prototype.hasOwnProperty.call(compiled, "output"), false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("compileRouteValidator merges query validator arrays automatically", () => {
|
|
162
|
+
const paginationQuery = {
|
|
163
|
+
schema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
properties: {
|
|
166
|
+
cursor: {
|
|
167
|
+
type: "string"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
additionalProperties: false
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const searchQuery = {
|
|
174
|
+
schema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
search: {
|
|
178
|
+
type: "string"
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
additionalProperties: false
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const compiled = compileRouteValidator({
|
|
186
|
+
queryValidator: [paginationQuery, searchQuery]
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
assert.deepEqual(compiled.schema, {
|
|
190
|
+
querystring: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
cursor: {
|
|
194
|
+
type: "string"
|
|
195
|
+
},
|
|
196
|
+
search: {
|
|
197
|
+
type: "string"
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
required: ["cursor", "search"],
|
|
201
|
+
additionalProperties: false
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("compileRouteValidator merges params validator arrays automatically", () => {
|
|
207
|
+
const workspaceSlugParams = {
|
|
208
|
+
schema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
properties: {
|
|
211
|
+
workspaceSlug: {
|
|
212
|
+
type: "string"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
required: ["workspaceSlug"],
|
|
216
|
+
additionalProperties: false
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
const inviteIdParams = {
|
|
220
|
+
schema: {
|
|
221
|
+
type: "object",
|
|
222
|
+
properties: {
|
|
223
|
+
inviteId: {
|
|
224
|
+
type: "string"
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
required: ["inviteId"],
|
|
228
|
+
additionalProperties: false
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const compiled = compileRouteValidator({
|
|
233
|
+
paramsValidator: [workspaceSlugParams, inviteIdParams]
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
assert.deepEqual(compiled.schema, {
|
|
237
|
+
params: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
workspaceSlug: {
|
|
241
|
+
type: "string"
|
|
242
|
+
},
|
|
243
|
+
inviteId: {
|
|
244
|
+
type: "string"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
required: ["workspaceSlug", "inviteId"],
|
|
248
|
+
additionalProperties: false
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("compileRouteValidator composes multiple query normalizers in validator arrays", () => {
|
|
254
|
+
const compiled = compileRouteValidator({
|
|
255
|
+
queryValidator: [
|
|
256
|
+
{
|
|
257
|
+
schema: {
|
|
258
|
+
type: "object",
|
|
259
|
+
properties: {
|
|
260
|
+
cursor: {
|
|
261
|
+
type: "string"
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
additionalProperties: false
|
|
265
|
+
},
|
|
266
|
+
normalize(query = {}) {
|
|
267
|
+
return {
|
|
268
|
+
cursor: String(query.cursor || "").trim()
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
schema: {
|
|
274
|
+
type: "object",
|
|
275
|
+
properties: {
|
|
276
|
+
search: {
|
|
277
|
+
type: "string"
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
additionalProperties: false
|
|
281
|
+
},
|
|
282
|
+
normalize(query = {}) {
|
|
283
|
+
return {
|
|
284
|
+
search: String(query.search || "").trim().toLowerCase()
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
assert.deepEqual(compiled.input.query({ cursor: " 100 ", search: " ACME " }), {
|
|
292
|
+
cursor: "100",
|
|
293
|
+
search: "acme"
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("resolveRouteValidatorOptions ignores legacy schema/input definitions", () => {
|
|
298
|
+
const resolved = resolveRouteValidatorOptions({
|
|
299
|
+
method: "POST",
|
|
300
|
+
path: "/contacts",
|
|
301
|
+
options: {
|
|
302
|
+
schema: {
|
|
303
|
+
bodyValidator: {}
|
|
304
|
+
},
|
|
305
|
+
input: {
|
|
306
|
+
body: () => ({})
|
|
307
|
+
},
|
|
308
|
+
middleware: ["api"]
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
assert.equal(Object.prototype.hasOwnProperty.call(resolved, "schema"), false);
|
|
313
|
+
assert.equal(Object.prototype.hasOwnProperty.call(resolved, "input"), false);
|
|
314
|
+
assert.deepEqual(resolved.middleware, ["api"]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("resolveRouteValidatorOptions supports inline validator shape without wrapper", () => {
|
|
318
|
+
const bodySchema = {
|
|
319
|
+
type: "object"
|
|
320
|
+
};
|
|
321
|
+
const normalizeBody = (body) => ({
|
|
322
|
+
name: String(body?.name || "").trim()
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const resolved = resolveRouteValidatorOptions({
|
|
326
|
+
method: "POST",
|
|
327
|
+
path: "/contacts",
|
|
328
|
+
options: {
|
|
329
|
+
meta: {
|
|
330
|
+
tags: ["contacts"],
|
|
331
|
+
summary: "Create contact"
|
|
332
|
+
},
|
|
333
|
+
bodyValidator: {
|
|
334
|
+
schema: bodySchema,
|
|
335
|
+
normalize: normalizeBody
|
|
336
|
+
},
|
|
337
|
+
middleware: ["api"]
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
assert.deepEqual(resolved.schema, {
|
|
342
|
+
tags: ["contacts"],
|
|
343
|
+
summary: "Create contact",
|
|
344
|
+
body: bodySchema
|
|
345
|
+
});
|
|
346
|
+
assert.equal(resolved.input.body, normalizeBody);
|
|
347
|
+
assert.deepEqual(resolved.middleware, ["api"]);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("resolveRouteValidatorOptions ignores validator wrapper", () => {
|
|
351
|
+
const resolved = resolveRouteValidatorOptions({
|
|
352
|
+
method: "POST",
|
|
353
|
+
path: "/contacts",
|
|
354
|
+
options: {
|
|
355
|
+
validator: defineRouteValidator({}),
|
|
356
|
+
middleware: ["api"]
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
assert.equal(Object.prototype.hasOwnProperty.call(resolved, "validator"), false);
|
|
361
|
+
assert.deepEqual(resolved.middleware, ["api"]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("defineRouteValidator rejects unsupported advanced.jskitInput keys", () => {
|
|
365
|
+
assert.throws(
|
|
366
|
+
() =>
|
|
367
|
+
defineRouteValidator({
|
|
368
|
+
advanced: {
|
|
369
|
+
jskitInput: {
|
|
370
|
+
headers: () => ({})
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}),
|
|
374
|
+
/advanced\.jskitInput\.headers is not supported/
|
|
375
|
+
);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("defineRouteValidator validates meta fields", () => {
|
|
379
|
+
assert.throws(
|
|
380
|
+
() =>
|
|
381
|
+
defineRouteValidator({
|
|
382
|
+
meta: {
|
|
383
|
+
tags: ["ok", ""]
|
|
384
|
+
}
|
|
385
|
+
}),
|
|
386
|
+
/meta\.tags\[1\] must be a non-empty string/
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
assert.throws(
|
|
390
|
+
() =>
|
|
391
|
+
defineRouteValidator({
|
|
392
|
+
meta: {
|
|
393
|
+
summary: ""
|
|
394
|
+
}
|
|
395
|
+
}),
|
|
396
|
+
/meta\.summary must be a non-empty string/
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("HttpRouter.register ignores validator wrapper options", () => {
|
|
401
|
+
const router = createRouter();
|
|
402
|
+
router.register(
|
|
403
|
+
"POST",
|
|
404
|
+
"/contacts",
|
|
405
|
+
{
|
|
406
|
+
validator: defineRouteValidator({})
|
|
407
|
+
},
|
|
408
|
+
async () => {}
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const [route] = router.list();
|
|
412
|
+
assert.equal(route.schema, undefined);
|
|
413
|
+
assert.equal(route.input, null);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
test("HttpRouter.register ignores compiled legacy-style route options", () => {
|
|
417
|
+
const router = createRouter();
|
|
418
|
+
const querySchema = {
|
|
419
|
+
type: "object"
|
|
420
|
+
};
|
|
421
|
+
const normalizeQuery = (query) => ({
|
|
422
|
+
dryRun: query?.dryRun === true
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const validator = defineRouteValidator({
|
|
426
|
+
queryValidator: {
|
|
427
|
+
schema: querySchema,
|
|
428
|
+
normalize: normalizeQuery
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
router.get(
|
|
433
|
+
"/contacts",
|
|
434
|
+
validator.toRouteOptions(),
|
|
435
|
+
async () => {}
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const [route] = router.list();
|
|
439
|
+
assert.equal(route.schema, undefined);
|
|
440
|
+
assert.equal(route.input, null);
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("HttpRouter.register accepts inline validator shape directly", () => {
|
|
444
|
+
const router = createRouter();
|
|
445
|
+
const querySchema = {
|
|
446
|
+
type: "object"
|
|
447
|
+
};
|
|
448
|
+
const normalizeQuery = (query) => ({
|
|
449
|
+
dryRun: query?.dryRun === true
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
router.get(
|
|
453
|
+
"/contacts",
|
|
454
|
+
{
|
|
455
|
+
meta: {
|
|
456
|
+
tags: ["contacts"],
|
|
457
|
+
summary: "List contacts"
|
|
458
|
+
},
|
|
459
|
+
queryValidator: {
|
|
460
|
+
schema: querySchema,
|
|
461
|
+
normalize: normalizeQuery
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
async () => {}
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
const [route] = router.list();
|
|
468
|
+
assert.deepEqual(route.schema, {
|
|
469
|
+
tags: ["contacts"],
|
|
470
|
+
summary: "List contacts",
|
|
471
|
+
querystring: querySchema
|
|
472
|
+
});
|
|
473
|
+
assert.equal(route.input.query, normalizeQuery);
|
|
474
|
+
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { ensureNonEmptyText, normalizeArray, normalizeObject, normalizeText } from "../../../shared/support/normalize.js";
|
|
2
|
+
import { RouteDefinitionError } from "./errors.js";
|
|
3
|
+
import { resolveRouteValidatorOptions } from "./routeValidator.js";
|
|
4
|
+
import { normalizeMiddlewareStack as normalizeSharedMiddlewareStack } from "./routeSupport.js";
|
|
5
|
+
|
|
6
|
+
function normalizeMethod(method) {
|
|
7
|
+
return ensureNonEmptyText(method, "route method").toUpperCase();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function normalizePath(pathname) {
|
|
11
|
+
const value = normalizeText(pathname);
|
|
12
|
+
if (!value.startsWith("/")) {
|
|
13
|
+
throw new RouteDefinitionError(`Route path must start with '/': ${value || "<empty>"}`);
|
|
14
|
+
}
|
|
15
|
+
return value.replace(/\/+/g, "/");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function joinPath(left, right) {
|
|
19
|
+
const leftPath = normalizeText(left);
|
|
20
|
+
const rightPath = normalizeText(right);
|
|
21
|
+
|
|
22
|
+
const normalizedLeft = leftPath ? `/${leftPath.replace(/^\/+|\/+$/g, "")}` : "";
|
|
23
|
+
const normalizedRight = rightPath ? `/${rightPath.replace(/^\/+|\/+$/g, "")}` : "";
|
|
24
|
+
const joined = `${normalizedLeft}${normalizedRight}`.replace(/\/+/g, "/");
|
|
25
|
+
return joined || "/";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeRouterMiddlewareStack(value, { context = "middleware" } = {}) {
|
|
29
|
+
return normalizeSharedMiddlewareStack(value, {
|
|
30
|
+
context,
|
|
31
|
+
ErrorType: RouteDefinitionError,
|
|
32
|
+
entryLabel: "entries",
|
|
33
|
+
includeIndex: false
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeRouteInput(method, path, optionsOrHandler, maybeHandler) {
|
|
38
|
+
const options =
|
|
39
|
+
typeof optionsOrHandler === "function"
|
|
40
|
+
? {}
|
|
41
|
+
: normalizeObject(optionsOrHandler, {
|
|
42
|
+
fallback: {}
|
|
43
|
+
});
|
|
44
|
+
const handler =
|
|
45
|
+
typeof optionsOrHandler === "function"
|
|
46
|
+
? optionsOrHandler
|
|
47
|
+
: typeof maybeHandler === "function"
|
|
48
|
+
? maybeHandler
|
|
49
|
+
: null;
|
|
50
|
+
|
|
51
|
+
if (typeof handler !== "function") {
|
|
52
|
+
throw new RouteDefinitionError(`Route ${method} ${path} requires a handler function.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
method: normalizeMethod(method),
|
|
57
|
+
path: normalizePath(path),
|
|
58
|
+
options,
|
|
59
|
+
handler
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
class HttpRouter {
|
|
64
|
+
constructor({ routes = null, prefix = "", middleware = [] } = {}) {
|
|
65
|
+
this._routes = Array.isArray(routes) ? routes : [];
|
|
66
|
+
this._prefix = normalizeText(prefix);
|
|
67
|
+
this._middleware = normalizeRouterMiddlewareStack(middleware, {
|
|
68
|
+
context: "router middleware"
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
register(method, path, optionsOrHandler, maybeHandler) {
|
|
73
|
+
const input = normalizeRouteInput(method, path, optionsOrHandler, maybeHandler);
|
|
74
|
+
const resolvedOptions = resolveRouteValidatorOptions({
|
|
75
|
+
method: input.method,
|
|
76
|
+
path: input.path,
|
|
77
|
+
options: input.options
|
|
78
|
+
});
|
|
79
|
+
const routeMiddleware = normalizeRouterMiddlewareStack(resolvedOptions.middleware, {
|
|
80
|
+
context: `Route ${input.method} ${input.path} middleware`
|
|
81
|
+
});
|
|
82
|
+
const routeInput = Object.prototype.hasOwnProperty.call(resolvedOptions, "input") ? resolvedOptions.input : null;
|
|
83
|
+
const routeOutput = Object.prototype.hasOwnProperty.call(resolvedOptions, "output") ? resolvedOptions.output : null;
|
|
84
|
+
|
|
85
|
+
const route = Object.freeze({
|
|
86
|
+
id: normalizeText(resolvedOptions.id),
|
|
87
|
+
method: input.method,
|
|
88
|
+
path: joinPath(this._prefix, input.path),
|
|
89
|
+
schema: resolvedOptions.schema,
|
|
90
|
+
input: routeInput,
|
|
91
|
+
output: routeOutput,
|
|
92
|
+
config: normalizeObject(resolvedOptions.config),
|
|
93
|
+
auth: resolvedOptions.auth,
|
|
94
|
+
contextPolicy: resolvedOptions.contextPolicy,
|
|
95
|
+
surface: resolvedOptions.surface,
|
|
96
|
+
visibility: resolvedOptions.visibility,
|
|
97
|
+
permission: resolvedOptions.permission,
|
|
98
|
+
ownerParam: resolvedOptions.ownerParam,
|
|
99
|
+
userField: resolvedOptions.userField,
|
|
100
|
+
ownerResolver: resolvedOptions.ownerResolver,
|
|
101
|
+
csrfProtection: resolvedOptions.csrfProtection,
|
|
102
|
+
bodyLimit: resolvedOptions.bodyLimit,
|
|
103
|
+
middleware: Object.freeze([...this._middleware, ...routeMiddleware]),
|
|
104
|
+
handler: input.handler
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
this._routes.push(route);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
get(path, optionsOrHandler, maybeHandler) {
|
|
112
|
+
return this.register("GET", path, optionsOrHandler, maybeHandler);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
post(path, optionsOrHandler, maybeHandler) {
|
|
116
|
+
return this.register("POST", path, optionsOrHandler, maybeHandler);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
put(path, optionsOrHandler, maybeHandler) {
|
|
120
|
+
return this.register("PUT", path, optionsOrHandler, maybeHandler);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
patch(path, optionsOrHandler, maybeHandler) {
|
|
124
|
+
return this.register("PATCH", path, optionsOrHandler, maybeHandler);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
delete(path, optionsOrHandler, maybeHandler) {
|
|
128
|
+
return this.register("DELETE", path, optionsOrHandler, maybeHandler);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
group({ prefix = "", middleware = [] } = {}, defineRoutes = null) {
|
|
132
|
+
if (typeof defineRoutes !== "function") {
|
|
133
|
+
throw new RouteDefinitionError("group() requires a callback.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const nestedRouter = new HttpRouter({
|
|
137
|
+
routes: this._routes,
|
|
138
|
+
prefix: joinPath(this._prefix, prefix || ""),
|
|
139
|
+
middleware: [
|
|
140
|
+
...this._middleware,
|
|
141
|
+
...normalizeRouterMiddlewareStack(middleware, {
|
|
142
|
+
context: "group middleware"
|
|
143
|
+
})
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
defineRoutes(nestedRouter);
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
resource(name, controller, options = {}) {
|
|
152
|
+
this._resource(name, controller, {
|
|
153
|
+
...normalizeObject(options),
|
|
154
|
+
apiOnly: false
|
|
155
|
+
});
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
apiResource(name, controller, options = {}) {
|
|
160
|
+
this._resource(name, controller, {
|
|
161
|
+
...normalizeObject(options),
|
|
162
|
+
apiOnly: true
|
|
163
|
+
});
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_resource(name, controller, options = {}) {
|
|
168
|
+
const resourceName = ensureNonEmptyText(name, "resource name");
|
|
169
|
+
const idParam = normalizeText(options.idParam, { fallback: "id" });
|
|
170
|
+
const basePath = `/${resourceName}`;
|
|
171
|
+
const itemPath = `/${resourceName}/:${idParam}`;
|
|
172
|
+
|
|
173
|
+
const methods = normalizeObject(controller);
|
|
174
|
+
const middleware = normalizeRouterMiddlewareStack(options.middleware, {
|
|
175
|
+
context: `resource ${resourceName} middleware`
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const requireMethod = (methodName) => {
|
|
179
|
+
const handler = methods[methodName];
|
|
180
|
+
if (typeof handler !== "function") {
|
|
181
|
+
throw new RouteDefinitionError(`resource controller for ${resourceName} is missing method ${methodName}().`);
|
|
182
|
+
}
|
|
183
|
+
return handler;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
this.get(basePath, { middleware }, requireMethod("index"));
|
|
187
|
+
if (!options.apiOnly) {
|
|
188
|
+
this.get(`${basePath}/create`, { middleware }, requireMethod("create"));
|
|
189
|
+
this.get(`${itemPath}/edit`, { middleware }, requireMethod("edit"));
|
|
190
|
+
}
|
|
191
|
+
this.post(basePath, { middleware }, requireMethod("store"));
|
|
192
|
+
this.get(itemPath, { middleware }, requireMethod("show"));
|
|
193
|
+
this.put(itemPath, { middleware }, requireMethod("update"));
|
|
194
|
+
this.delete(itemPath, { middleware }, requireMethod("destroy"));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
list() {
|
|
198
|
+
return Object.freeze([...this._routes]);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function createRouter(options = {}) {
|
|
203
|
+
return new HttpRouter(options);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export { HttpRouter, createRouter, joinPath };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Application, createApplication, createProviderClass } from "../../shared/runtime/application.js";
|
|
2
|
+
import { ServiceProvider } from "../../shared/runtime/serviceProvider.js";
|
|
3
|
+
import * as errors from "../../shared/runtime/kernelErrors.js";
|
|
4
|
+
|
|
5
|
+
const KERNEL_CORE_API = Object.freeze({
|
|
6
|
+
Application,
|
|
7
|
+
createApplication,
|
|
8
|
+
createProviderClass,
|
|
9
|
+
ServiceProvider,
|
|
10
|
+
errors: Object.freeze({ ...errors })
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
class KernelCoreServiceProvider {
|
|
14
|
+
static id = "runtime.kernel";
|
|
15
|
+
|
|
16
|
+
register(app) {
|
|
17
|
+
if (!app || typeof app.singleton !== "function") {
|
|
18
|
+
throw new Error("KernelCoreServiceProvider requires application singleton().");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
app.singleton("runtime.kernel", () => KERNEL_CORE_API);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
boot() {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { KernelCoreServiceProvider };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Application, createApplication, createProviderClass } from "../../shared/runtime/application.js";
|
|
2
|
+
export { ServiceProvider } from "../../shared/runtime/serviceProvider.js";
|
|
3
|
+
export {
|
|
4
|
+
KernelError,
|
|
5
|
+
ProviderNormalizationError,
|
|
6
|
+
DuplicateProviderError,
|
|
7
|
+
ProviderDependencyError,
|
|
8
|
+
ProviderLifecycleError
|
|
9
|
+
} from "../../shared/runtime/kernelErrors.js";
|
|
10
|
+
export { KernelCoreServiceProvider } from "./KernelCoreServiceProvider.js";
|