@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.
Files changed (185) hide show
  1. package/README.md +24 -0
  2. package/_testable/index.js +4 -0
  3. package/client/appConfig.js +33 -0
  4. package/client/componentInteraction.js +51 -0
  5. package/client/componentInteraction.test.js +111 -0
  6. package/client/descriptorSections.js +75 -0
  7. package/client/index.d.ts +70 -0
  8. package/client/index.js +3 -0
  9. package/client/logging.js +38 -0
  10. package/client/moduleBootstrap.js +670 -0
  11. package/client/moduleBootstrap.test.js +403 -0
  12. package/client/shellBootstrap.js +233 -0
  13. package/client/shellBootstrap.test.js +185 -0
  14. package/client/shellRouting.js +321 -0
  15. package/client/shellRouting.test.js +113 -0
  16. package/client/vite/clientBootstrapPlugin.js +259 -0
  17. package/client/vite/clientBootstrapPlugin.test.js +563 -0
  18. package/client/vite/index.js +3 -0
  19. package/internal/node/fileSystem.js +21 -0
  20. package/internal/node/installedPackageDescriptor.js +104 -0
  21. package/package.json +43 -0
  22. package/server/actions/ActionRuntimeServiceProvider.js +309 -0
  23. package/server/actions/ActionRuntimeServiceProvider.test.js +551 -0
  24. package/server/actions/index.js +8 -0
  25. package/server/container/ContainerCoreServiceProvider.js +27 -0
  26. package/server/container/index.js +10 -0
  27. package/server/exportPolicy.test.js +68 -0
  28. package/server/http/HttpFastifyServiceProvider.js +25 -0
  29. package/server/http/_testable/index.js +2 -0
  30. package/server/http/index.js +1 -0
  31. package/server/http/lib/controller.js +183 -0
  32. package/server/http/lib/controller.test.js +143 -0
  33. package/server/http/lib/errors.js +12 -0
  34. package/server/http/lib/httpRuntime.js +82 -0
  35. package/server/http/lib/index.js +18 -0
  36. package/server/http/lib/kernel.js +15 -0
  37. package/server/http/lib/kernel.test.js +880 -0
  38. package/server/http/lib/middlewareRuntime.js +149 -0
  39. package/server/http/lib/requestActionExecutor.js +258 -0
  40. package/server/http/lib/requestScope.js +59 -0
  41. package/server/http/lib/routeRegistration.js +165 -0
  42. package/server/http/lib/routeSupport.js +45 -0
  43. package/server/http/lib/routeValidator.js +469 -0
  44. package/server/http/lib/routeValidator.test.js +474 -0
  45. package/server/http/lib/router.js +206 -0
  46. package/server/kernel/KernelCoreServiceProvider.js +27 -0
  47. package/server/kernel/index.js +10 -0
  48. package/server/platform/PlatformServerRuntimeServiceProvider.js +30 -0
  49. package/server/platform/index.js +5 -0
  50. package/server/platform/providerRuntime/descriptorCatalog.js +170 -0
  51. package/server/platform/providerRuntime/helpers.js +45 -0
  52. package/server/platform/providerRuntime/lockfile.js +27 -0
  53. package/server/platform/providerRuntime/providerLoader.js +283 -0
  54. package/server/platform/providerRuntime.js +142 -0
  55. package/server/platform/providerRuntime.test.js +217 -0
  56. package/server/platform/runtime.js +40 -0
  57. package/server/platform/surfaceRuntime.js +150 -0
  58. package/server/platform/surfaceRuntime.test.js +136 -0
  59. package/server/registries/actionSurfaceSourceRegistry.js +150 -0
  60. package/server/registries/bootstrapPayloadContributorRegistry.js +41 -0
  61. package/server/registries/domainEventListenerRegistry.js +61 -0
  62. package/server/registries/index.js +36 -0
  63. package/server/registries/primitives.js +63 -0
  64. package/server/registries/routeVisibilityResolverRegistry.js +87 -0
  65. package/server/registries/serviceRegistrationRegistry.js +431 -0
  66. package/server/runtime/ServerRuntimeCoreServiceProvider.js +65 -0
  67. package/server/runtime/ServerRuntimeCoreServiceProvider.test.js +53 -0
  68. package/server/runtime/apiRoutePolicyParity.test.js +109 -0
  69. package/server/runtime/apiRouteRegistration.js +65 -0
  70. package/server/runtime/bootBootstrapRoutes.js +46 -0
  71. package/server/runtime/bootBootstrapRoutes.test.js +79 -0
  72. package/server/runtime/bootstrapContributors.test.js +114 -0
  73. package/server/runtime/canonicalJson.js +74 -0
  74. package/server/runtime/composition.js +142 -0
  75. package/server/runtime/domainEvents.test.js +114 -0
  76. package/server/runtime/domainRules.js +50 -0
  77. package/server/runtime/domainRules.test.js +87 -0
  78. package/server/runtime/entityChangeEvents.js +182 -0
  79. package/server/runtime/entityChangeEvents.test.js +211 -0
  80. package/server/runtime/errors.js +68 -0
  81. package/server/runtime/errors.test.js +73 -0
  82. package/server/runtime/fastifyBootstrap.js +372 -0
  83. package/server/runtime/fastifyBootstrap.test.js +194 -0
  84. package/server/runtime/index.js +6 -0
  85. package/server/runtime/integers.js +13 -0
  86. package/server/runtime/moduleConfig.js +269 -0
  87. package/server/runtime/moduleConfig.test.js +141 -0
  88. package/server/runtime/pagination.js +13 -0
  89. package/server/runtime/realtimeNormalization.js +21 -0
  90. package/server/runtime/requestUrl.js +38 -0
  91. package/server/runtime/routeUtils.js +20 -0
  92. package/server/runtime/runtimeAssembly.js +113 -0
  93. package/server/runtime/runtimeKernel.js +55 -0
  94. package/server/runtime/securityAudit.js +269 -0
  95. package/server/runtime/securityAudit.test.js +41 -0
  96. package/server/runtime/serviceAuthorization.js +113 -0
  97. package/server/runtime/serviceAuthorization.test.js +100 -0
  98. package/server/runtime/serviceRegistration.test.js +197 -0
  99. package/server/support/SupportCoreServiceProvider.js +25 -0
  100. package/server/support/appConfig.js +37 -0
  101. package/server/support/appConfig.test.js +94 -0
  102. package/server/support/defaultMissingHandler.js +7 -0
  103. package/server/support/index.js +2 -0
  104. package/server/support/routePolicyConfig.js +51 -0
  105. package/server/support/symlinkSafeRequire.js +78 -0
  106. package/server/support/symlinkSafeRequire.test.js +27 -0
  107. package/server/surface/SurfaceRoutingServiceProvider.js +27 -0
  108. package/server/surface/index.js +19 -0
  109. package/shared/actions/actionContributorHelpers.js +34 -0
  110. package/shared/actions/actionContributorHelpers.test.js +16 -0
  111. package/shared/actions/actionDefinitions.js +488 -0
  112. package/shared/actions/actionDefinitions.test.js +212 -0
  113. package/shared/actions/audit.js +7 -0
  114. package/shared/actions/executionContext.js +97 -0
  115. package/shared/actions/executionContext.test.js +66 -0
  116. package/shared/actions/idempotency.js +62 -0
  117. package/shared/actions/index.js +2 -0
  118. package/shared/actions/observability.js +10 -0
  119. package/shared/actions/pipeline.js +287 -0
  120. package/shared/actions/policies.js +342 -0
  121. package/shared/actions/policies.test.js +233 -0
  122. package/shared/actions/registry.js +187 -0
  123. package/shared/actions/registry.test.js +381 -0
  124. package/shared/actions/requestMeta.js +36 -0
  125. package/shared/actions/textNormalization.js +3 -0
  126. package/shared/actions/withActionDefaults.js +34 -0
  127. package/shared/index.js +2 -0
  128. package/shared/runtime/application.js +323 -0
  129. package/shared/runtime/container.js +261 -0
  130. package/shared/runtime/containerErrors.js +22 -0
  131. package/shared/runtime/index.js +18 -0
  132. package/shared/runtime/kernelErrors.js +20 -0
  133. package/shared/runtime/serviceProvider.js +13 -0
  134. package/shared/support/formatDateTime.js +10 -0
  135. package/shared/support/formatDateTime.test.js +15 -0
  136. package/shared/support/index.js +14 -0
  137. package/shared/support/linkPath.js +67 -0
  138. package/shared/support/linkPath.test.js +35 -0
  139. package/shared/support/normalize.js +116 -0
  140. package/shared/support/normalize.test.js +48 -0
  141. package/shared/support/packageDescriptor.test.js +121 -0
  142. package/shared/support/permissions.js +50 -0
  143. package/shared/support/pickOwnProperties.js +17 -0
  144. package/shared/support/pickOwnProperties.test.js +25 -0
  145. package/shared/support/policies.js +11 -0
  146. package/shared/support/queryPath.js +33 -0
  147. package/shared/support/queryPath.test.js +19 -0
  148. package/shared/support/queryResilience.js +34 -0
  149. package/shared/support/queryResilience.test.js +33 -0
  150. package/shared/support/returnToPath.js +153 -0
  151. package/shared/support/returnToPath.test.js +123 -0
  152. package/shared/support/sorting.js +15 -0
  153. package/shared/support/tokens.js +23 -0
  154. package/shared/support/tokens.test.js +17 -0
  155. package/shared/support/visibility.js +56 -0
  156. package/shared/support/visibility.test.js +45 -0
  157. package/shared/surface/apiPaths.js +84 -0
  158. package/shared/surface/escapeRegExp.js +5 -0
  159. package/shared/surface/index.js +6 -0
  160. package/shared/surface/paths.js +273 -0
  161. package/shared/surface/registry.js +135 -0
  162. package/shared/surface/registry.test.js +44 -0
  163. package/shared/surface/runtime.js +357 -0
  164. package/shared/surface/runtime.test.js +319 -0
  165. package/shared/validators/createCursorListValidator.js +42 -0
  166. package/shared/validators/createCursorListValidator.test.js +34 -0
  167. package/shared/validators/cursorPaginationQueryValidator.js +31 -0
  168. package/shared/validators/cursorPaginationQueryValidator.test.js +21 -0
  169. package/shared/validators/index.js +12 -0
  170. package/shared/validators/inputNormalization.js +13 -0
  171. package/shared/validators/mergeObjectSchemas.js +31 -0
  172. package/shared/validators/mergeObjectSchemas.test.js +67 -0
  173. package/shared/validators/mergeValidators.js +89 -0
  174. package/shared/validators/mergeValidators.test.js +116 -0
  175. package/shared/validators/nestValidator.js +53 -0
  176. package/shared/validators/nestValidator.test.js +60 -0
  177. package/shared/validators/recordIdParamsValidator.js +36 -0
  178. package/shared/validators/recordIdParamsValidator.test.js +20 -0
  179. package/shared/validators/resourceRequiredMetadata.js +41 -0
  180. package/shared/validators/resourceRequiredMetadata.test.js +49 -0
  181. package/test/barrelExposure.test.js +106 -0
  182. package/test/dynamicImportPolicy.test.js +89 -0
  183. package/test/exportsContract.test.js +168 -0
  184. package/test/routeInputContractGuard.test.js +78 -0
  185. 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 };