@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,65 @@
|
|
|
1
|
+
import { defaultMissingHandler } from "./routeUtils.js";
|
|
2
|
+
import { defaultApplyRoutePolicy, normalizeRoutePolicyConfig } from "../support/routePolicyConfig.js";
|
|
3
|
+
import { isRecord } from "../../shared/support/normalize.js";
|
|
4
|
+
|
|
5
|
+
function buildBaseRouteOptions(route) {
|
|
6
|
+
if (!isRecord(route)) {
|
|
7
|
+
throw new TypeError("Route definition must be an object.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const sourceRoute = route;
|
|
11
|
+
const routeOptions = {
|
|
12
|
+
method: sourceRoute.method,
|
|
13
|
+
url: sourceRoute.path,
|
|
14
|
+
config: {}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (sourceRoute.schema) {
|
|
18
|
+
routeOptions.schema = sourceRoute.schema;
|
|
19
|
+
}
|
|
20
|
+
if (sourceRoute.bodyLimit) {
|
|
21
|
+
routeOptions.bodyLimit = sourceRoute.bodyLimit;
|
|
22
|
+
}
|
|
23
|
+
if (sourceRoute.rateLimit) {
|
|
24
|
+
routeOptions.config.rateLimit = sourceRoute.rateLimit;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return routeOptions;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function registerApiRouteDefinitions(
|
|
31
|
+
fastify,
|
|
32
|
+
{ routes = [], applyRoutePolicy = defaultApplyRoutePolicy, resolveRequestUrl = null, missingHandler } = {}
|
|
33
|
+
) {
|
|
34
|
+
if (!fastify || typeof fastify.route !== "function") {
|
|
35
|
+
throw new TypeError("registerApiRouteDefinitions requires a Fastify instance.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const routeList = Array.isArray(routes) ? routes : [];
|
|
39
|
+
const toRequestUrl = typeof resolveRequestUrl === "function" ? resolveRequestUrl : () => null;
|
|
40
|
+
const routePolicyApplier = typeof applyRoutePolicy === "function" ? applyRoutePolicy : defaultApplyRoutePolicy;
|
|
41
|
+
const fallbackHandler = typeof missingHandler === "function" ? missingHandler : defaultMissingHandler;
|
|
42
|
+
|
|
43
|
+
for (const route of routeList) {
|
|
44
|
+
const routeOptions = routePolicyApplier(buildBaseRouteOptions(route), route);
|
|
45
|
+
let routeHandler = fallbackHandler;
|
|
46
|
+
if (isRecord(route) && typeof route.handler === "function") {
|
|
47
|
+
routeHandler = route.handler;
|
|
48
|
+
}
|
|
49
|
+
fastify.route({
|
|
50
|
+
...routeOptions,
|
|
51
|
+
handler: async (request, reply) => {
|
|
52
|
+
await routeHandler(request, reply, toRequestUrl(request));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const __testables = {
|
|
59
|
+
buildBaseRouteOptions,
|
|
60
|
+
defaultApplyRoutePolicy,
|
|
61
|
+
normalizeRoutePolicyConfig,
|
|
62
|
+
defaultMissingHandler
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export { registerApiRouteDefinitions, __testables };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "../../shared/validators/inputNormalization.js";
|
|
3
|
+
import { AUTH_POLICY_PUBLIC } from "../../shared/support/policies.js";
|
|
4
|
+
import { KERNEL_TOKENS } from "../../shared/support/tokens.js";
|
|
5
|
+
import { resolveBootstrapPayload } from "../registries/bootstrapPayloadContributorRegistry.js";
|
|
6
|
+
|
|
7
|
+
const bootstrapQueryValidator = Object.freeze({
|
|
8
|
+
schema: Type.Object({}, { additionalProperties: true }),
|
|
9
|
+
normalize: normalizeObjectInput
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const bootstrapOutputValidator = Object.freeze({
|
|
13
|
+
schema: Type.Object({}, { additionalProperties: true }),
|
|
14
|
+
normalize: normalizeObjectInput
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function bootBootstrapRoutes(app) {
|
|
18
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
19
|
+
|
|
20
|
+
router.register(
|
|
21
|
+
"GET",
|
|
22
|
+
"/api/bootstrap",
|
|
23
|
+
{
|
|
24
|
+
auth: AUTH_POLICY_PUBLIC,
|
|
25
|
+
meta: {
|
|
26
|
+
tags: ["bootstrap"],
|
|
27
|
+
summary: "Resolve app bootstrap payload from registered contributors"
|
|
28
|
+
},
|
|
29
|
+
queryValidator: bootstrapQueryValidator,
|
|
30
|
+
responseValidators: {
|
|
31
|
+
200: bootstrapOutputValidator
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
async function (request, reply) {
|
|
35
|
+
const payload = await resolveBootstrapPayload(app, {
|
|
36
|
+
request,
|
|
37
|
+
reply,
|
|
38
|
+
query: request.input?.query || {}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
reply.code(200).send(payload);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { bootBootstrapRoutes, bootstrapQueryValidator, bootstrapOutputValidator };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createContainer } from "../container/index.js";
|
|
4
|
+
import { KERNEL_TOKENS } from "../../shared/support/tokens.js";
|
|
5
|
+
import { registerBootstrapPayloadContributor } from "../registries/bootstrapPayloadContributorRegistry.js";
|
|
6
|
+
import { bootBootstrapRoutes, bootstrapQueryValidator } from "./bootBootstrapRoutes.js";
|
|
7
|
+
|
|
8
|
+
function createReplyDouble() {
|
|
9
|
+
return {
|
|
10
|
+
statusCode: 200,
|
|
11
|
+
payload: null,
|
|
12
|
+
code(value) {
|
|
13
|
+
this.statusCode = value;
|
|
14
|
+
return this;
|
|
15
|
+
},
|
|
16
|
+
send(value) {
|
|
17
|
+
this.payload = value;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("bootstrapQueryValidator normalizes generic query payloads", () => {
|
|
24
|
+
assert.deepEqual(bootstrapQueryValidator.normalize({}), {});
|
|
25
|
+
assert.deepEqual(bootstrapQueryValidator.normalize({ workspaceSlug: " AcMe ", page: "1" }), {
|
|
26
|
+
workspaceSlug: " AcMe ",
|
|
27
|
+
page: "1"
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("bootBootstrapRoutes registers GET /api/bootstrap and resolves contributors", async () => {
|
|
32
|
+
const app = createContainer();
|
|
33
|
+
const routes = [];
|
|
34
|
+
const router = {
|
|
35
|
+
register(method, path, route, handler) {
|
|
36
|
+
routes.push({
|
|
37
|
+
method,
|
|
38
|
+
path,
|
|
39
|
+
route,
|
|
40
|
+
handler
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
app.instance(KERNEL_TOKENS.HttpRouter, router);
|
|
46
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.payload", () => ({
|
|
47
|
+
contributorId: "test.bootstrap.payload",
|
|
48
|
+
contribute({ query }) {
|
|
49
|
+
return {
|
|
50
|
+
source: "test",
|
|
51
|
+
workspaceSlug: query.workspaceSlug
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
bootBootstrapRoutes(app);
|
|
57
|
+
|
|
58
|
+
const bootstrapRoute = routes.find((entry) => entry.method === "GET" && entry.path === "/api/bootstrap");
|
|
59
|
+
assert.ok(bootstrapRoute);
|
|
60
|
+
assert.equal(typeof bootstrapRoute.route.queryValidator.normalize, "function");
|
|
61
|
+
|
|
62
|
+
const reply = createReplyDouble();
|
|
63
|
+
await bootstrapRoute.handler(
|
|
64
|
+
{
|
|
65
|
+
input: {
|
|
66
|
+
query: {
|
|
67
|
+
workspaceSlug: "acme"
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
reply
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
assert.equal(reply.statusCode, 200);
|
|
75
|
+
assert.deepEqual(reply.payload, {
|
|
76
|
+
source: "test",
|
|
77
|
+
workspaceSlug: "acme"
|
|
78
|
+
});
|
|
79
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createContainer } from "../container/index.js";
|
|
4
|
+
import {
|
|
5
|
+
registerBootstrapPayloadContributor,
|
|
6
|
+
resolveBootstrapPayloadContributors,
|
|
7
|
+
resolveBootstrapPayload
|
|
8
|
+
} from "../registries/bootstrapPayloadContributorRegistry.js";
|
|
9
|
+
|
|
10
|
+
test("registerBootstrapPayloadContributor + resolveBootstrapPayloadContributors register canonical contributors", () => {
|
|
11
|
+
const app = createContainer();
|
|
12
|
+
|
|
13
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.alpha", () => ({
|
|
14
|
+
contributorId: "alpha",
|
|
15
|
+
contribute() {
|
|
16
|
+
return {
|
|
17
|
+
alpha: true
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const contributors = resolveBootstrapPayloadContributors(app);
|
|
23
|
+
assert.equal(contributors.length, 1);
|
|
24
|
+
assert.equal(contributors[0].contributorId, "alpha");
|
|
25
|
+
assert.equal(typeof contributors[0].contribute, "function");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("resolveBootstrapPayload applies contributors in deterministic token order", async () => {
|
|
29
|
+
const app = createContainer();
|
|
30
|
+
const calls = [];
|
|
31
|
+
|
|
32
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.zeta", () => ({
|
|
33
|
+
contributorId: "zeta",
|
|
34
|
+
contribute({ payload, query }) {
|
|
35
|
+
calls.push({
|
|
36
|
+
contributorId: "zeta",
|
|
37
|
+
payload,
|
|
38
|
+
query
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
last: true
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.alpha", () => ({
|
|
47
|
+
contributorId: "alpha",
|
|
48
|
+
contribute({ payload, query }) {
|
|
49
|
+
calls.push({
|
|
50
|
+
contributorId: "alpha",
|
|
51
|
+
payload,
|
|
52
|
+
query
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
first: true
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const payload = await resolveBootstrapPayload(app, {
|
|
61
|
+
query: {
|
|
62
|
+
workspaceSlug: "acme"
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(calls, [
|
|
67
|
+
{
|
|
68
|
+
contributorId: "alpha",
|
|
69
|
+
payload: {},
|
|
70
|
+
query: {
|
|
71
|
+
workspaceSlug: "acme"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
contributorId: "zeta",
|
|
76
|
+
payload: {
|
|
77
|
+
first: true
|
|
78
|
+
},
|
|
79
|
+
query: {
|
|
80
|
+
workspaceSlug: "acme"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]);
|
|
84
|
+
assert.deepEqual(payload, {
|
|
85
|
+
first: true,
|
|
86
|
+
last: true
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("resolveBootstrapPayload ignores non-object contributions", async () => {
|
|
91
|
+
const app = createContainer();
|
|
92
|
+
|
|
93
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.noop", () => ({
|
|
94
|
+
contributorId: "noop",
|
|
95
|
+
contribute() {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
registerBootstrapPayloadContributor(app, "test.bootstrap.ok", () => ({
|
|
100
|
+
contributorId: "ok",
|
|
101
|
+
contribute() {
|
|
102
|
+
return {
|
|
103
|
+
ok: true
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
|
|
108
|
+
const payload = await resolveBootstrapPayload(app, {
|
|
109
|
+
ignored: true
|
|
110
|
+
});
|
|
111
|
+
assert.deepEqual(payload, {
|
|
112
|
+
ok: true
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
function isPlainObject(value) {
|
|
4
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value) && value.constructor === Object;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sortValue(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return value.map(sortValue);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (isPlainObject(value)) {
|
|
13
|
+
const sorted = {};
|
|
14
|
+
for (const key of Object.keys(value).sort()) {
|
|
15
|
+
const next = value[key];
|
|
16
|
+
if (next === undefined) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
sorted[key] = sortValue(next);
|
|
20
|
+
}
|
|
21
|
+
return sorted;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toCanonicalJson(value) {
|
|
28
|
+
return JSON.stringify(sortValue(value));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toSha256Hex(value) {
|
|
32
|
+
const source = String(value || "");
|
|
33
|
+
return crypto.createHash("sha256").update(source).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toHmacSha256Hex(secret, value) {
|
|
37
|
+
const normalizedSecret = String(secret || "").trim();
|
|
38
|
+
if (!normalizedSecret) {
|
|
39
|
+
throw new Error("HMAC secret is required.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return crypto
|
|
43
|
+
.createHmac("sha256", normalizedSecret)
|
|
44
|
+
.update(String(value || ""))
|
|
45
|
+
.digest("hex");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeParseJson(value, fallback = null) {
|
|
49
|
+
if (value == null) {
|
|
50
|
+
return fallback;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof value === "object") {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const text = String(value || "").trim();
|
|
58
|
+
if (!text) {
|
|
59
|
+
return fallback;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const __testables = {
|
|
70
|
+
isPlainObject,
|
|
71
|
+
sortValue
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export { toCanonicalJson, toSha256Hex, toHmacSha256Hex, safeParseJson, __testables };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
function normalizeRegistryDefinitions(definitions, { registryKind = "registry" } = {}) {
|
|
2
|
+
const source = Array.isArray(definitions) ? definitions : [];
|
|
3
|
+
const normalized = [];
|
|
4
|
+
const seenIds = new Set();
|
|
5
|
+
|
|
6
|
+
for (const entry of source) {
|
|
7
|
+
const id = String(entry?.id || "").trim();
|
|
8
|
+
const create = entry?.create;
|
|
9
|
+
|
|
10
|
+
if (!id) {
|
|
11
|
+
throw new TypeError(`${registryKind} definition id is required.`);
|
|
12
|
+
}
|
|
13
|
+
if (seenIds.has(id)) {
|
|
14
|
+
throw new TypeError(`${registryKind} definition "${id}" is duplicated.`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof create !== "function") {
|
|
17
|
+
throw new TypeError(`${registryKind} definition "${id}" create must be a function.`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
seenIds.add(id);
|
|
21
|
+
normalized.push({
|
|
22
|
+
id,
|
|
23
|
+
create
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return normalized;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createRegistryFromDefinitions(definitions, createArgsFactory, options = {}) {
|
|
31
|
+
const normalizedDefinitions = normalizeRegistryDefinitions(definitions, options);
|
|
32
|
+
const registry = {};
|
|
33
|
+
|
|
34
|
+
for (const definition of normalizedDefinitions) {
|
|
35
|
+
const createArgs =
|
|
36
|
+
typeof createArgsFactory === "function" ? createArgsFactory({ definition, registry }) : Object.freeze({});
|
|
37
|
+
registry[definition.id] = definition.create(createArgs || {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return registry;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createRepositoryRegistry(definitions) {
|
|
44
|
+
return createRegistryFromDefinitions(definitions, () => ({}), {
|
|
45
|
+
registryKind: "repository"
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function createServiceRegistry({ definitions, dependencies = {} } = {}) {
|
|
50
|
+
return createRegistryFromDefinitions(
|
|
51
|
+
definitions,
|
|
52
|
+
({ registry }) => ({
|
|
53
|
+
...(dependencies || {}),
|
|
54
|
+
services: registry
|
|
55
|
+
}),
|
|
56
|
+
{
|
|
57
|
+
registryKind: "service"
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createControllerRegistry({ definitions, services = {}, dependencies = {} } = {}) {
|
|
63
|
+
return createRegistryFromDefinitions(
|
|
64
|
+
definitions,
|
|
65
|
+
({ registry }) => ({
|
|
66
|
+
...(dependencies || {}),
|
|
67
|
+
services,
|
|
68
|
+
controllers: registry
|
|
69
|
+
}),
|
|
70
|
+
{
|
|
71
|
+
registryKind: "controller"
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function selectRuntimeServices(services, selectedIds = []) {
|
|
77
|
+
const source = services && typeof services === "object" ? services : {};
|
|
78
|
+
const ids = Array.isArray(selectedIds) ? selectedIds : [];
|
|
79
|
+
const runtimeServices = {};
|
|
80
|
+
|
|
81
|
+
for (const rawId of ids) {
|
|
82
|
+
const id = String(rawId || "").trim();
|
|
83
|
+
if (!id) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!Object.hasOwn(source, id)) {
|
|
87
|
+
throw new Error(`Runtime service "${id}" is not defined.`);
|
|
88
|
+
}
|
|
89
|
+
runtimeServices[id] = source[id];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return runtimeServices;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createRuntimeComposition({
|
|
96
|
+
repositoryDefinitions = [],
|
|
97
|
+
serviceDefinitions = [],
|
|
98
|
+
controllerDefinitions = [],
|
|
99
|
+
runtimeServiceIds = [],
|
|
100
|
+
repositoryDependencies = {},
|
|
101
|
+
serviceDependencies = {},
|
|
102
|
+
controllerDependencies = {}
|
|
103
|
+
} = {}) {
|
|
104
|
+
const repositories = createRepositoryRegistry(repositoryDefinitions);
|
|
105
|
+
const services = createServiceRegistry({
|
|
106
|
+
definitions: serviceDefinitions,
|
|
107
|
+
dependencies: {
|
|
108
|
+
...(repositoryDependencies || {}),
|
|
109
|
+
...(serviceDependencies || {}),
|
|
110
|
+
repositories
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const controllers = createControllerRegistry({
|
|
114
|
+
definitions: controllerDefinitions,
|
|
115
|
+
services,
|
|
116
|
+
dependencies: {
|
|
117
|
+
...(controllerDependencies || {})
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const runtimeServices = selectRuntimeServices(services, runtimeServiceIds);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
repositories,
|
|
124
|
+
services,
|
|
125
|
+
controllers,
|
|
126
|
+
runtimeServices
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const __testables = {
|
|
131
|
+
normalizeRegistryDefinitions,
|
|
132
|
+
createRegistryFromDefinitions
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
createRepositoryRegistry,
|
|
137
|
+
createServiceRegistry,
|
|
138
|
+
createControllerRegistry,
|
|
139
|
+
selectRuntimeServices,
|
|
140
|
+
createRuntimeComposition,
|
|
141
|
+
__testables
|
|
142
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createContainer } from "../container/index.js";
|
|
4
|
+
import {
|
|
5
|
+
registerDomainEventListener,
|
|
6
|
+
resolveDomainEventListeners,
|
|
7
|
+
createDomainEvents
|
|
8
|
+
} from "../registries/domainEventListenerRegistry.js";
|
|
9
|
+
|
|
10
|
+
test("registerDomainEventListener + resolveDomainEventListeners resolve canonical listeners", () => {
|
|
11
|
+
const app = createContainer();
|
|
12
|
+
|
|
13
|
+
registerDomainEventListener(app, "test.domainEvents.alpha", () => ({
|
|
14
|
+
listenerId: "alpha",
|
|
15
|
+
async handle() {}
|
|
16
|
+
}));
|
|
17
|
+
registerDomainEventListener(app, "test.domainEvents.zeta", () => ({
|
|
18
|
+
listenerId: "zeta",
|
|
19
|
+
async handle() {}
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const listeners = resolveDomainEventListeners(app);
|
|
23
|
+
assert.deepEqual(
|
|
24
|
+
listeners.map((listener) => listener.listenerId),
|
|
25
|
+
["alpha", "zeta"]
|
|
26
|
+
);
|
|
27
|
+
assert.equal(typeof listeners[0].handle, "function");
|
|
28
|
+
assert.equal(typeof listeners[1].handle, "function");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("createDomainEvents.publish dispatches listeners and applies matches filter", async () => {
|
|
32
|
+
const app = createContainer();
|
|
33
|
+
const calls = [];
|
|
34
|
+
|
|
35
|
+
registerDomainEventListener(app, "test.domainEvents.alpha", () => ({
|
|
36
|
+
listenerId: "alpha",
|
|
37
|
+
matches(event) {
|
|
38
|
+
return event.entity === "record";
|
|
39
|
+
},
|
|
40
|
+
async handle(event) {
|
|
41
|
+
calls.push({
|
|
42
|
+
listenerId: "alpha",
|
|
43
|
+
event
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}));
|
|
47
|
+
registerDomainEventListener(app, "test.domainEvents.beta", () => ({
|
|
48
|
+
listenerId: "beta",
|
|
49
|
+
async handle(event) {
|
|
50
|
+
calls.push({
|
|
51
|
+
listenerId: "beta",
|
|
52
|
+
event
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
const domainEvents = createDomainEvents(app);
|
|
58
|
+
await domainEvents.publish({
|
|
59
|
+
entity: "record",
|
|
60
|
+
operation: "created"
|
|
61
|
+
});
|
|
62
|
+
await domainEvents.publish({
|
|
63
|
+
entity: "other",
|
|
64
|
+
operation: "created"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.deepEqual(calls, [
|
|
68
|
+
{
|
|
69
|
+
listenerId: "alpha",
|
|
70
|
+
event: {
|
|
71
|
+
entity: "record",
|
|
72
|
+
operation: "created"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
listenerId: "beta",
|
|
77
|
+
event: {
|
|
78
|
+
entity: "record",
|
|
79
|
+
operation: "created"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
listenerId: "beta",
|
|
84
|
+
event: {
|
|
85
|
+
entity: "other",
|
|
86
|
+
operation: "created"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("createDomainEvents.publish ignores non-listener tag entries", async () => {
|
|
93
|
+
const app = createContainer();
|
|
94
|
+
|
|
95
|
+
registerDomainEventListener(app, "test.domainEvents.invalid", () => ({
|
|
96
|
+
listenerId: "invalid"
|
|
97
|
+
}));
|
|
98
|
+
registerDomainEventListener(app, "test.domainEvents.valid", () => ({
|
|
99
|
+
listenerId: "valid",
|
|
100
|
+
async handle() {}
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
const listeners = resolveDomainEventListeners(app);
|
|
104
|
+
assert.deepEqual(
|
|
105
|
+
listeners.map((listener) => listener.listenerId),
|
|
106
|
+
["valid"]
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const domainEvents = createDomainEvents(app);
|
|
110
|
+
const result = await domainEvents.publish({
|
|
111
|
+
entity: "record"
|
|
112
|
+
});
|
|
113
|
+
assert.equal(result, null);
|
|
114
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { DomainValidationError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
function collectDomainFieldErrors(rules) {
|
|
4
|
+
const fieldErrors = {};
|
|
5
|
+
|
|
6
|
+
for (const rule of Array.isArray(rules) ? rules : []) {
|
|
7
|
+
if (rule?.when && !rule.when()) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const outcome = rule?.check ? rule.check() : null;
|
|
12
|
+
if (!outcome) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof outcome === "string") {
|
|
17
|
+
fieldErrors[rule.field] = outcome;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof outcome === "object") {
|
|
22
|
+
fieldErrors[rule.field] = outcome?.message || "domain rule failed";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return fieldErrors;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function assertNoDomainRuleFailures(
|
|
30
|
+
rules,
|
|
31
|
+
{
|
|
32
|
+
message = "Domain validation failed.",
|
|
33
|
+
code = "domain_validation_failed"
|
|
34
|
+
} = {}
|
|
35
|
+
) {
|
|
36
|
+
const fieldErrors = collectDomainFieldErrors(rules);
|
|
37
|
+
if (Object.keys(fieldErrors).length > 0) {
|
|
38
|
+
throw new DomainValidationError(
|
|
39
|
+
{
|
|
40
|
+
fieldErrors
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
message,
|
|
44
|
+
code
|
|
45
|
+
}
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { collectDomainFieldErrors, assertNoDomainRuleFailures };
|