@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,431 @@
|
|
|
1
|
+
import { normalizeObject, normalizePositiveInteger, normalizeText } from "../../shared/support/normalize.js";
|
|
2
|
+
import { isContainerToken } from "../../shared/support/tokens.js";
|
|
3
|
+
import { createEntityChangePublisher } from "../runtime/entityChangeEvents.js";
|
|
4
|
+
import {
|
|
5
|
+
assertTaggableApp,
|
|
6
|
+
normalizeNestedEntries,
|
|
7
|
+
registerTaggedSingleton,
|
|
8
|
+
resolveTaggedEntries
|
|
9
|
+
} from "./primitives.js";
|
|
10
|
+
|
|
11
|
+
const SERVICE_REGISTRATION_TAG = Symbol.for("jskit.runtime.services.registrations");
|
|
12
|
+
const ENTITY_CHANGED_EVENT_TYPE = "entity.changed";
|
|
13
|
+
const DEFAULT_REALTIME_AUDIENCE = "event_scope";
|
|
14
|
+
let SERVICE_REGISTRATION_INDEX = 0;
|
|
15
|
+
|
|
16
|
+
function normalizeMethodName(value, { context = "service method" } = {}) {
|
|
17
|
+
const methodName = String(value || "").trim();
|
|
18
|
+
if (!methodName) {
|
|
19
|
+
throw new TypeError(`${context} must be a non-empty string.`);
|
|
20
|
+
}
|
|
21
|
+
return methodName;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createServiceRegistrationToken() {
|
|
25
|
+
SERVICE_REGISTRATION_INDEX += 1;
|
|
26
|
+
return Symbol(`jskit.runtime.services.registration.${SERVICE_REGISTRATION_INDEX}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeServiceEventType(value, { context = "service event" } = {}) {
|
|
30
|
+
const normalizedType = normalizeText(value).toLowerCase();
|
|
31
|
+
if (normalizedType !== ENTITY_CHANGED_EVENT_TYPE) {
|
|
32
|
+
throw new TypeError(`${context}.type must be "${ENTITY_CHANGED_EVENT_TYPE}".`);
|
|
33
|
+
}
|
|
34
|
+
return normalizedType;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeServiceEventOperation(value, { context = "service event" } = {}) {
|
|
38
|
+
if (typeof value === "function") {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const normalizedOperation = normalizeText(value).toLowerCase();
|
|
43
|
+
if (!normalizedOperation) {
|
|
44
|
+
throw new TypeError(`${context}.operation is required.`);
|
|
45
|
+
}
|
|
46
|
+
if (normalizedOperation !== "created" && normalizedOperation !== "updated" && normalizedOperation !== "deleted") {
|
|
47
|
+
throw new TypeError(`${context}.operation must be one of: created, updated, deleted.`);
|
|
48
|
+
}
|
|
49
|
+
return normalizedOperation;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeServiceEventEntityId(value) {
|
|
53
|
+
if (typeof value === "function" || typeof value === "string") {
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parsed = normalizePositiveInteger(value);
|
|
58
|
+
if (parsed > 0) {
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeRealtimeDispatch(value, { context = "service event.realtime" } = {}) {
|
|
66
|
+
const source = normalizeObject(value);
|
|
67
|
+
if (Object.keys(source).length < 1) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const event = normalizeText(source.event);
|
|
72
|
+
if (!event) {
|
|
73
|
+
throw new TypeError(`${context}.event is required.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const payload = typeof source.payload === "function" ? source.payload : null;
|
|
77
|
+
const audience = normalizeRealtimeAudience(source.audience, {
|
|
78
|
+
context: `${context}.audience`
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
event,
|
|
83
|
+
payload,
|
|
84
|
+
audience
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeRealtimeAudience(value, { context = "service event.realtime.audience" } = {}) {
|
|
89
|
+
if (typeof value === "undefined") {
|
|
90
|
+
return DEFAULT_REALTIME_AUDIENCE;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof value === "function") {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof value === "string") {
|
|
98
|
+
const preset = normalizeText(value).toLowerCase();
|
|
99
|
+
if (!preset) {
|
|
100
|
+
throw new TypeError(`${context} must be a non-empty string, function, or object.`);
|
|
101
|
+
}
|
|
102
|
+
return preset;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
...value
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new TypeError(`${context} must be a string, function, or object.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function normalizeServiceEventSpec(entry, { context = "service event" } = {}) {
|
|
115
|
+
const source = normalizeObject(entry);
|
|
116
|
+
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
type: normalizeServiceEventType(source.type, { context }),
|
|
119
|
+
source: normalizeText(source.source),
|
|
120
|
+
entity: normalizeText(source.entity),
|
|
121
|
+
operation: normalizeServiceEventOperation(source.operation, { context }),
|
|
122
|
+
entityId: normalizeServiceEventEntityId(source.entityId),
|
|
123
|
+
realtime: normalizeRealtimeDispatch(source.realtime, { context: `${context}.realtime` })
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizeServiceMetadata(value = {}) {
|
|
128
|
+
const source = normalizeObject(value);
|
|
129
|
+
if (Object.hasOwn(source, "permissions")) {
|
|
130
|
+
throw new TypeError("service metadata.permissions is no longer supported. Define permissions on actions.");
|
|
131
|
+
}
|
|
132
|
+
const eventsSource = normalizeObject(source.events);
|
|
133
|
+
const events = {};
|
|
134
|
+
|
|
135
|
+
for (const [methodName, eventEntries] of Object.entries(eventsSource)) {
|
|
136
|
+
const normalizedMethodName = normalizeMethodName(methodName, {
|
|
137
|
+
context: "service metadata.events method"
|
|
138
|
+
});
|
|
139
|
+
const normalizedEventEntries = normalizeNestedEntries(eventEntries).map((entry, index) =>
|
|
140
|
+
normalizeServiceEventSpec(entry, {
|
|
141
|
+
context: `service metadata.events.${normalizedMethodName}[${index}]`
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
events[normalizedMethodName] = Object.freeze(normalizedEventEntries);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return Object.freeze({
|
|
148
|
+
events: Object.freeze(events)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeServiceEventsForDefinition(serviceDefinition, serviceMetadata) {
|
|
153
|
+
const service = normalizeObject(serviceDefinition);
|
|
154
|
+
const methodNameSet = new Set(
|
|
155
|
+
Object.entries(service)
|
|
156
|
+
.filter(([, value]) => typeof value === "function")
|
|
157
|
+
.map(([name]) => name)
|
|
158
|
+
);
|
|
159
|
+
const declaredEvents = normalizeObject(serviceMetadata.events);
|
|
160
|
+
const normalizedEvents = {};
|
|
161
|
+
|
|
162
|
+
for (const [methodName, events] of Object.entries(declaredEvents)) {
|
|
163
|
+
if (!methodNameSet.has(methodName)) {
|
|
164
|
+
throw new TypeError(`service metadata.events.${methodName} does not match a service method.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const normalizedEntries = normalizeNestedEntries(events).map((entry, index) =>
|
|
168
|
+
normalizeServiceEventSpec(entry, {
|
|
169
|
+
context: `service metadata.events.${methodName}[${index}]`
|
|
170
|
+
})
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
for (const [index, entry] of normalizedEntries.entries()) {
|
|
174
|
+
if (!entry.source || !entry.entity) {
|
|
175
|
+
throw new TypeError(
|
|
176
|
+
`service metadata.events.${methodName}[${index}] requires source and entity for "${ENTITY_CHANGED_EVENT_TYPE}".`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
normalizedEvents[methodName] = Object.freeze(normalizedEntries);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return Object.freeze(normalizedEvents);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resolveMethodOptions(args = []) {
|
|
188
|
+
if (!Array.isArray(args) || args.length < 1) {
|
|
189
|
+
return {};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const candidate = args[args.length - 1];
|
|
193
|
+
if (candidate && typeof candidate === "object" && !Array.isArray(candidate)) {
|
|
194
|
+
return candidate;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolveEventOperation(spec, state) {
|
|
201
|
+
if (typeof spec.operation === "function") {
|
|
202
|
+
return spec.operation({
|
|
203
|
+
result: state.result,
|
|
204
|
+
args: state.args,
|
|
205
|
+
options: state.options,
|
|
206
|
+
methodName: state.methodName,
|
|
207
|
+
serviceToken: state.serviceToken
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return spec.operation;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function resolveEventEntityId(spec, state) {
|
|
214
|
+
if (typeof spec.entityId === "function") {
|
|
215
|
+
return spec.entityId({
|
|
216
|
+
result: state.result,
|
|
217
|
+
args: state.args,
|
|
218
|
+
options: state.options,
|
|
219
|
+
methodName: state.methodName,
|
|
220
|
+
serviceToken: state.serviceToken
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (typeof spec.entityId === "string") {
|
|
225
|
+
return state?.result?.[spec.entityId];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (Number.isInteger(spec.entityId) && spec.entityId > 0) {
|
|
229
|
+
return spec.entityId;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return state?.result?.id;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveEventMeta(spec, state) {
|
|
236
|
+
const meta = {
|
|
237
|
+
service: Object.freeze({
|
|
238
|
+
token: state.serviceToken,
|
|
239
|
+
method: state.methodName
|
|
240
|
+
})
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (spec.realtime) {
|
|
244
|
+
meta.realtime = spec.realtime.payload
|
|
245
|
+
? Object.freeze({
|
|
246
|
+
event: spec.realtime.event,
|
|
247
|
+
payload: spec.realtime.payload({
|
|
248
|
+
event: state.event,
|
|
249
|
+
result: state.result,
|
|
250
|
+
args: state.args,
|
|
251
|
+
options: state.options,
|
|
252
|
+
methodName: state.methodName,
|
|
253
|
+
serviceToken: state.serviceToken
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
: Object.freeze({
|
|
257
|
+
event: spec.realtime.event
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return Object.freeze(meta);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function createServiceMethodEventPublisher(scope, serviceToken, methodName, specs = []) {
|
|
265
|
+
if (!scope || typeof scope.make !== "function" || typeof scope.has !== "function") {
|
|
266
|
+
throw new TypeError("service event publisher requires a scope with has()/make().");
|
|
267
|
+
}
|
|
268
|
+
if (specs.length < 1) {
|
|
269
|
+
return async function publishNoop() {
|
|
270
|
+
return null;
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (!scope.has("domainEvents")) {
|
|
274
|
+
throw new Error(`app.service(${String(serviceToken)}) requires "domainEvents" binding to emit service events.`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const domainEvents = scope.make("domainEvents");
|
|
278
|
+
const publishers = specs.map((spec) =>
|
|
279
|
+
createEntityChangePublisher({
|
|
280
|
+
domainEvents,
|
|
281
|
+
source: spec.source,
|
|
282
|
+
entity: spec.entity
|
|
283
|
+
})
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return async function publishServiceMethodEvents(state) {
|
|
287
|
+
for (const [index, spec] of specs.entries()) {
|
|
288
|
+
const publisher = publishers[index];
|
|
289
|
+
const operation = resolveEventOperation(spec, state);
|
|
290
|
+
const entityId = resolveEventEntityId(spec, state);
|
|
291
|
+
const eventMeta = resolveEventMeta(spec, {
|
|
292
|
+
...state,
|
|
293
|
+
event: spec
|
|
294
|
+
});
|
|
295
|
+
await publisher(operation, entityId, state.options, eventMeta);
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function materializeServiceRegistration(scope, registrationSpec) {
|
|
302
|
+
const service = registrationSpec.factory(scope);
|
|
303
|
+
const normalizedService = normalizeObject(service);
|
|
304
|
+
const events = normalizeServiceEventsForDefinition(normalizedService, registrationSpec.metadata);
|
|
305
|
+
const wrappedService = {};
|
|
306
|
+
|
|
307
|
+
for (const [methodName, method] of Object.entries(normalizedService)) {
|
|
308
|
+
if (typeof method !== "function") {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const methodEvents = events[methodName] || [];
|
|
313
|
+
const eventPublisher = createServiceMethodEventPublisher(
|
|
314
|
+
scope,
|
|
315
|
+
registrationSpec.serviceToken,
|
|
316
|
+
methodName,
|
|
317
|
+
methodEvents
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
wrappedService[methodName] = function registeredServiceMethod(...args) {
|
|
321
|
+
if (methodEvents.length < 1) {
|
|
322
|
+
return method(...args);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const options = resolveMethodOptions(args);
|
|
326
|
+
const result = method(...args);
|
|
327
|
+
const publish = (resolvedResult) =>
|
|
328
|
+
eventPublisher({
|
|
329
|
+
result: resolvedResult,
|
|
330
|
+
args,
|
|
331
|
+
options,
|
|
332
|
+
methodName,
|
|
333
|
+
serviceToken: registrationSpec.serviceToken
|
|
334
|
+
}).then(() => resolvedResult);
|
|
335
|
+
|
|
336
|
+
if (result && typeof result.then === "function") {
|
|
337
|
+
return result.then((resolvedResult) => publish(resolvedResult));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return publish(result);
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
Object.defineProperty(wrappedService, "serviceEvents", {
|
|
345
|
+
enumerable: false,
|
|
346
|
+
configurable: false,
|
|
347
|
+
writable: false,
|
|
348
|
+
value: events
|
|
349
|
+
});
|
|
350
|
+
Object.defineProperty(wrappedService, "serviceToken", {
|
|
351
|
+
enumerable: false,
|
|
352
|
+
configurable: false,
|
|
353
|
+
writable: false,
|
|
354
|
+
value: registrationSpec.serviceToken
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return Object.freeze(wrappedService);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeServiceRegistration(value = {}) {
|
|
361
|
+
const source = normalizeObject(value);
|
|
362
|
+
if (!isContainerToken(source.serviceToken)) {
|
|
363
|
+
throw new TypeError("app.service requires a valid service token.");
|
|
364
|
+
}
|
|
365
|
+
if (typeof source.factory !== "function") {
|
|
366
|
+
throw new TypeError("app.service requires a factory function.");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return Object.freeze({
|
|
370
|
+
serviceToken: source.serviceToken,
|
|
371
|
+
factory: source.factory,
|
|
372
|
+
metadata: normalizeServiceMetadata(source.metadata)
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function registerServiceRegistration(app, token, factory) {
|
|
377
|
+
registerTaggedSingleton(app, token, factory, SERVICE_REGISTRATION_TAG, {
|
|
378
|
+
context: "registerServiceRegistration"
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function resolveServiceRegistrations(scope) {
|
|
383
|
+
return resolveTaggedEntries(scope, SERVICE_REGISTRATION_TAG)
|
|
384
|
+
.map((entry) => normalizeObject(entry))
|
|
385
|
+
.filter((entry) => Object.keys(entry).length > 0)
|
|
386
|
+
.sort((left, right) => String(left.serviceToken || "").localeCompare(String(right.serviceToken || "")));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function installServiceRegistrationApi(app) {
|
|
390
|
+
assertTaggableApp(app, {
|
|
391
|
+
context: "installServiceRegistrationApi"
|
|
392
|
+
});
|
|
393
|
+
if (typeof app.service === "function") {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const registerService = function registerService(serviceToken, factory, metadata = {}) {
|
|
398
|
+
const registration = normalizeServiceRegistration({
|
|
399
|
+
serviceToken,
|
|
400
|
+
factory,
|
|
401
|
+
metadata
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
this.singleton(registration.serviceToken, (scope) => materializeServiceRegistration(scope, registration));
|
|
405
|
+
|
|
406
|
+
const registrationToken = createServiceRegistrationToken();
|
|
407
|
+
registerServiceRegistration(this, registrationToken, () =>
|
|
408
|
+
Object.freeze({
|
|
409
|
+
serviceToken: registration.serviceToken,
|
|
410
|
+
metadata: registration.metadata
|
|
411
|
+
})
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
return this;
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
Object.defineProperty(app, "service", {
|
|
418
|
+
configurable: true,
|
|
419
|
+
writable: true,
|
|
420
|
+
value: registerService
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export {
|
|
425
|
+
SERVICE_REGISTRATION_TAG,
|
|
426
|
+
normalizeServiceRegistration,
|
|
427
|
+
materializeServiceRegistration,
|
|
428
|
+
registerServiceRegistration,
|
|
429
|
+
resolveServiceRegistrations,
|
|
430
|
+
installServiceRegistrationApi
|
|
431
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import * as apiRouteRegistration from "./apiRouteRegistration.js";
|
|
2
|
+
import * as bootstrapContributors from "../registries/bootstrapPayloadContributorRegistry.js";
|
|
3
|
+
import * as bootstrapRoutes from "./bootBootstrapRoutes.js";
|
|
4
|
+
import * as canonicalJson from "./canonicalJson.js";
|
|
5
|
+
import * as composition from "./composition.js";
|
|
6
|
+
import * as domainEvents from "../registries/domainEventListenerRegistry.js";
|
|
7
|
+
import * as errors from "./errors.js";
|
|
8
|
+
import * as fastifyBootstrap from "./fastifyBootstrap.js";
|
|
9
|
+
import * as integers from "./integers.js";
|
|
10
|
+
import * as pagination from "./pagination.js";
|
|
11
|
+
import * as realtimeNormalization from "./realtimeNormalization.js";
|
|
12
|
+
import * as requestUrl from "./requestUrl.js";
|
|
13
|
+
import * as routeUtils from "./routeUtils.js";
|
|
14
|
+
import * as runtimeAssembly from "./runtimeAssembly.js";
|
|
15
|
+
import * as runtimeKernel from "./runtimeKernel.js";
|
|
16
|
+
import * as serviceAuthorization from "./serviceAuthorization.js";
|
|
17
|
+
import * as serviceRegistration from "../registries/serviceRegistrationRegistry.js";
|
|
18
|
+
import * as entityChangeEvents from "./entityChangeEvents.js";
|
|
19
|
+
import * as securityAudit from "./securityAudit.js";
|
|
20
|
+
import { KERNEL_TOKENS } from "../../shared/support/tokens.js";
|
|
21
|
+
|
|
22
|
+
const SERVER_RUNTIME_CORE_API = Object.freeze({
|
|
23
|
+
apiRouteRegistration: Object.freeze({ ...apiRouteRegistration }),
|
|
24
|
+
bootstrapContributors: Object.freeze({ ...bootstrapContributors }),
|
|
25
|
+
bootstrapRoutes: Object.freeze({ ...bootstrapRoutes }),
|
|
26
|
+
canonicalJson: Object.freeze({ ...canonicalJson }),
|
|
27
|
+
composition: Object.freeze({ ...composition }),
|
|
28
|
+
domainEvents: Object.freeze({ ...domainEvents }),
|
|
29
|
+
errors: Object.freeze({ ...errors }),
|
|
30
|
+
fastifyBootstrap: Object.freeze({ ...fastifyBootstrap }),
|
|
31
|
+
integers: Object.freeze({ ...integers }),
|
|
32
|
+
pagination: Object.freeze({ ...pagination }),
|
|
33
|
+
realtimeNormalization: Object.freeze({ ...realtimeNormalization }),
|
|
34
|
+
requestUrl: Object.freeze({ ...requestUrl }),
|
|
35
|
+
routeUtils: Object.freeze({ ...routeUtils }),
|
|
36
|
+
runtimeAssembly: Object.freeze({ ...runtimeAssembly }),
|
|
37
|
+
runtimeKernel: Object.freeze({ ...runtimeKernel }),
|
|
38
|
+
serviceAuthorization: Object.freeze({ ...serviceAuthorization }),
|
|
39
|
+
serviceRegistration: Object.freeze({ ...serviceRegistration }),
|
|
40
|
+
entityChangeEvents: Object.freeze({ ...entityChangeEvents }),
|
|
41
|
+
securityAudit: Object.freeze({ ...securityAudit })
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
class ServerRuntimeCoreServiceProvider {
|
|
45
|
+
static id = "runtime.server";
|
|
46
|
+
|
|
47
|
+
register(app) {
|
|
48
|
+
if (!app || typeof app.singleton !== "function" || typeof app.has !== "function") {
|
|
49
|
+
throw new Error("ServerRuntimeCoreServiceProvider requires application singleton()/has().");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
serviceRegistration.installServiceRegistrationApi(app);
|
|
53
|
+
|
|
54
|
+
app.singleton("runtime.server", () => SERVER_RUNTIME_CORE_API);
|
|
55
|
+
app.singleton("domainEvents", (scope) => domainEvents.createDomainEvents(scope));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
boot(app) {
|
|
59
|
+
if (app.has(KERNEL_TOKENS.HttpRouter)) {
|
|
60
|
+
bootstrapRoutes.bootBootstrapRoutes(app);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export { ServerRuntimeCoreServiceProvider };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createContainer } from "../container/index.js";
|
|
4
|
+
import { registerDomainEventListener } from "../registries/domainEventListenerRegistry.js";
|
|
5
|
+
import { ServerRuntimeCoreServiceProvider } from "./ServerRuntimeCoreServiceProvider.js";
|
|
6
|
+
|
|
7
|
+
test("ServerRuntimeCoreServiceProvider registers runtime.server and default domainEvents", () => {
|
|
8
|
+
const app = createContainer();
|
|
9
|
+
const provider = new ServerRuntimeCoreServiceProvider();
|
|
10
|
+
provider.register(app);
|
|
11
|
+
|
|
12
|
+
assert.equal(typeof app.service, "function");
|
|
13
|
+
|
|
14
|
+
const runtimeServer = app.make("runtime.server");
|
|
15
|
+
assert.equal(typeof runtimeServer, "object");
|
|
16
|
+
|
|
17
|
+
const domainEvents = app.make("domainEvents");
|
|
18
|
+
assert.equal(typeof domainEvents.publish, "function");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("ServerRuntimeCoreServiceProvider default domainEvents dispatches registered listeners", async () => {
|
|
22
|
+
const app = createContainer();
|
|
23
|
+
const provider = new ServerRuntimeCoreServiceProvider();
|
|
24
|
+
provider.register(app);
|
|
25
|
+
|
|
26
|
+
const received = [];
|
|
27
|
+
registerDomainEventListener(app, "test.domainEvents.alpha", () => ({
|
|
28
|
+
listenerId: "alpha",
|
|
29
|
+
async handle(payload) {
|
|
30
|
+
received.push(payload);
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const domainEvents = app.make("domainEvents");
|
|
35
|
+
const eventPayload = {
|
|
36
|
+
source: "test"
|
|
37
|
+
};
|
|
38
|
+
const result = await domainEvents.publish(eventPayload);
|
|
39
|
+
|
|
40
|
+
assert.equal(result, null);
|
|
41
|
+
assert.deepEqual(received, [eventPayload]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("ServerRuntimeCoreServiceProvider owns domainEvents binding", async () => {
|
|
45
|
+
const app = createContainer();
|
|
46
|
+
app.singleton("domainEvents", () => ({ publish: async () => "existing" }));
|
|
47
|
+
|
|
48
|
+
const provider = new ServerRuntimeCoreServiceProvider();
|
|
49
|
+
assert.throws(
|
|
50
|
+
() => provider.register(app),
|
|
51
|
+
/Token "domainEvents" is already bound\./
|
|
52
|
+
);
|
|
53
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
defaultApplyRoutePolicy as applyHttpRoutePolicy,
|
|
5
|
+
normalizeRoutePolicyConfig as normalizeHttpRoutePolicyConfig
|
|
6
|
+
} from "../http/lib/kernel.js";
|
|
7
|
+
import { __testables as runtimeRouteRegistrationTestables } from "./apiRouteRegistration.js";
|
|
8
|
+
import {
|
|
9
|
+
defaultApplyRoutePolicy as applyCanonicalRoutePolicy,
|
|
10
|
+
normalizeRoutePolicyConfig as normalizeCanonicalRoutePolicyConfig
|
|
11
|
+
} from "../support/routePolicyConfig.js";
|
|
12
|
+
|
|
13
|
+
test("route policy mapping is sourced from one canonical mapper in both registration paths", () => {
|
|
14
|
+
assert.equal(applyHttpRoutePolicy, applyCanonicalRoutePolicy);
|
|
15
|
+
assert.equal(runtimeRouteRegistrationTestables.defaultApplyRoutePolicy, applyCanonicalRoutePolicy);
|
|
16
|
+
assert.equal(normalizeHttpRoutePolicyConfig, normalizeCanonicalRoutePolicyConfig);
|
|
17
|
+
assert.equal(runtimeRouteRegistrationTestables.normalizeRoutePolicyConfig, normalizeCanonicalRoutePolicyConfig);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("route policy parity matrix stays aligned across http/runtime registration", () => {
|
|
21
|
+
const ownerResolver = () => 42;
|
|
22
|
+
|
|
23
|
+
const matrix = [
|
|
24
|
+
{
|
|
25
|
+
label: "auth maps to authPolicy",
|
|
26
|
+
route: { auth: "required" },
|
|
27
|
+
expectedConfig: { seed: "x", authPolicy: "required" }
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: "contextPolicy maps through",
|
|
31
|
+
route: { contextPolicy: "required" },
|
|
32
|
+
expectedConfig: { seed: "x", contextPolicy: "required" }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
label: "surface maps through",
|
|
36
|
+
route: { surface: "admin" },
|
|
37
|
+
expectedConfig: { seed: "x", surface: "admin" }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "visibility is normalized",
|
|
41
|
+
route: { visibility: "USER" },
|
|
42
|
+
expectedConfig: { seed: "x", visibility: "user" }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: "permission maps through",
|
|
46
|
+
route: { permission: "workspace.settings.read" },
|
|
47
|
+
expectedConfig: { seed: "x", permission: "workspace.settings.read" }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "ownerParam maps through",
|
|
51
|
+
route: { ownerParam: "userId" },
|
|
52
|
+
expectedConfig: { seed: "x", ownerParam: "userId" }
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: "userField maps through",
|
|
56
|
+
route: { userField: "accountId" },
|
|
57
|
+
expectedConfig: { seed: "x", userField: "accountId" }
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
label: "ownerResolver maps through",
|
|
61
|
+
route: { ownerResolver },
|
|
62
|
+
expectedConfig: { seed: "x", ownerResolver }
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "csrfProtection maps through",
|
|
66
|
+
route: { csrfProtection: false },
|
|
67
|
+
expectedConfig: { seed: "x", csrfProtection: false }
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "route fields override existing config values",
|
|
71
|
+
routeOptionsConfig: { authPolicy: "public", visibility: "public", seed: "x" },
|
|
72
|
+
route: { auth: "own", visibility: "WORKSPACE" },
|
|
73
|
+
expectedConfig: { seed: "x", authPolicy: "own", visibility: "workspace" }
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: "unmapped route fields do not alter config",
|
|
77
|
+
route: { rateLimit: { max: 1, timeWindow: "1 second" } },
|
|
78
|
+
expectedConfig: { seed: "x" }
|
|
79
|
+
}
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
for (const testCase of matrix) {
|
|
83
|
+
const routeOptions = {
|
|
84
|
+
method: "GET",
|
|
85
|
+
url: "/demo",
|
|
86
|
+
config: {
|
|
87
|
+
seed: "x",
|
|
88
|
+
...(testCase.routeOptionsConfig || {})
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const route = {
|
|
92
|
+
method: "GET",
|
|
93
|
+
path: "/demo",
|
|
94
|
+
...(testCase.route || {})
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const canonical = applyCanonicalRoutePolicy(routeOptions, route);
|
|
98
|
+
const fromHttp = applyHttpRoutePolicy(routeOptions, route);
|
|
99
|
+
const fromRuntime = runtimeRouteRegistrationTestables.defaultApplyRoutePolicy(routeOptions, route);
|
|
100
|
+
|
|
101
|
+
assert.deepEqual(
|
|
102
|
+
canonical.config,
|
|
103
|
+
testCase.expectedConfig,
|
|
104
|
+
`canonical mapping failed for case: ${testCase.label}`
|
|
105
|
+
);
|
|
106
|
+
assert.deepEqual(fromHttp, canonical, `http mapping drifted for case: ${testCase.label}`);
|
|
107
|
+
assert.deepEqual(fromRuntime, canonical, `runtime mapping drifted for case: ${testCase.label}`);
|
|
108
|
+
}
|
|
109
|
+
});
|