@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,42 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "./inputNormalization.js";
|
|
3
|
+
import { normalizeText } from "../support/normalize.js";
|
|
4
|
+
|
|
5
|
+
function createCursorListValidator(itemValidator) {
|
|
6
|
+
if (!itemValidator || typeof itemValidator !== "object" || Array.isArray(itemValidator)) {
|
|
7
|
+
throw new TypeError("createCursorListValidator requires an item validator object.");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!Object.hasOwn(itemValidator, "schema")) {
|
|
11
|
+
throw new TypeError("createCursorListValidator requires itemValidator.schema.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const normalizeItem =
|
|
15
|
+
typeof itemValidator.normalize === "function"
|
|
16
|
+
? itemValidator.normalize
|
|
17
|
+
: function identity(value) {
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
get schema() {
|
|
23
|
+
return Type.Object(
|
|
24
|
+
{
|
|
25
|
+
items: Type.Array(itemValidator.schema),
|
|
26
|
+
nextCursor: Type.Union([Type.String({ minLength: 1 }), Type.Null()])
|
|
27
|
+
},
|
|
28
|
+
{ additionalProperties: false }
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
normalize(payload = {}) {
|
|
32
|
+
const source = normalizeObjectInput(payload);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
items: Array.isArray(source.items) ? source.items.map((entry) => normalizeItem(entry)) : [],
|
|
36
|
+
nextCursor: normalizeText(source.nextCursor) || null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { createCursorListValidator };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { createCursorListValidator } from "./createCursorListValidator.js";
|
|
5
|
+
|
|
6
|
+
test("createCursorListValidator builds a list validator from an item validator", () => {
|
|
7
|
+
const itemValidator = {
|
|
8
|
+
schema: Type.Object(
|
|
9
|
+
{
|
|
10
|
+
id: Type.Integer({ minimum: 1 }),
|
|
11
|
+
label: Type.String({ minLength: 1 })
|
|
12
|
+
},
|
|
13
|
+
{ additionalProperties: false }
|
|
14
|
+
),
|
|
15
|
+
normalize(payload = {}) {
|
|
16
|
+
return {
|
|
17
|
+
id: Number(payload.id),
|
|
18
|
+
label: String(payload.label || "").trim()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const listValidator = createCursorListValidator(itemValidator);
|
|
24
|
+
const normalized = listValidator.normalize({
|
|
25
|
+
items: [{ id: "7", label: " member " }],
|
|
26
|
+
nextCursor: " 8 "
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.deepEqual(normalized, {
|
|
30
|
+
items: [{ id: 7, label: "member" }],
|
|
31
|
+
nextCursor: "8"
|
|
32
|
+
});
|
|
33
|
+
assert.equal(listValidator.schema.properties.items.type, "array");
|
|
34
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "./inputNormalization.js";
|
|
3
|
+
import { positiveIntegerValidator } from "./recordIdParamsValidator.js";
|
|
4
|
+
|
|
5
|
+
function normalizeCursorPaginationQuery(input = {}) {
|
|
6
|
+
const source = normalizeObjectInput(input);
|
|
7
|
+
const normalized = {};
|
|
8
|
+
|
|
9
|
+
if (Object.hasOwn(source, "cursor")) {
|
|
10
|
+
normalized.cursor = positiveIntegerValidator.normalize(source.cursor);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (Object.hasOwn(source, "limit")) {
|
|
14
|
+
normalized.limit = positiveIntegerValidator.normalize(source.limit);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return normalized;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const cursorPaginationQueryValidator = Object.freeze({
|
|
21
|
+
schema: Type.Object(
|
|
22
|
+
{
|
|
23
|
+
cursor: Type.Optional(positiveIntegerValidator.schema),
|
|
24
|
+
limit: Type.Optional(positiveIntegerValidator.schema)
|
|
25
|
+
},
|
|
26
|
+
{ additionalProperties: false }
|
|
27
|
+
),
|
|
28
|
+
normalize: normalizeCursorPaginationQuery
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export { cursorPaginationQueryValidator };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { cursorPaginationQueryValidator } from "./cursorPaginationQueryValidator.js";
|
|
4
|
+
|
|
5
|
+
test("cursorPaginationQueryValidator normalizes numeric strings", () => {
|
|
6
|
+
assert.deepEqual(cursorPaginationQueryValidator.normalize({ cursor: "12", limit: "25" }), {
|
|
7
|
+
cursor: 12,
|
|
8
|
+
limit: 25
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("cursorPaginationQueryValidator normalizes invalid values to 0", () => {
|
|
13
|
+
assert.deepEqual(cursorPaginationQueryValidator.normalize({ cursor: "abc", limit: "-1" }), {
|
|
14
|
+
cursor: 0,
|
|
15
|
+
limit: 0
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("cursorPaginationQueryValidator keeps absent keys absent", () => {
|
|
20
|
+
assert.deepEqual(cursorPaginationQueryValidator.normalize({}), {});
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { normalizeObjectInput } from "./inputNormalization.js";
|
|
2
|
+
export { createCursorListValidator } from "./createCursorListValidator.js";
|
|
3
|
+
export { cursorPaginationQueryValidator } from "./cursorPaginationQueryValidator.js";
|
|
4
|
+
export { mergeObjectSchemas } from "./mergeObjectSchemas.js";
|
|
5
|
+
export { mergeValidators } from "./mergeValidators.js";
|
|
6
|
+
export { nestValidator } from "./nestValidator.js";
|
|
7
|
+
export { recordIdParamsValidator, positiveIntegerValidator } from "./recordIdParamsValidator.js";
|
|
8
|
+
export {
|
|
9
|
+
normalizeRequiredFieldList,
|
|
10
|
+
deriveRequiredFieldsFromSchema,
|
|
11
|
+
deriveResourceRequiredMetadata
|
|
12
|
+
} from "./resourceRequiredMetadata.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
function mergeObjectSchemas(schemas) {
|
|
4
|
+
if (!Array.isArray(schemas)) {
|
|
5
|
+
throw new Error("mergeObjectSchemas requires an array of object schemas.");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const mergedProperties = {};
|
|
9
|
+
|
|
10
|
+
for (const schema of schemas) {
|
|
11
|
+
if (!schema || typeof schema !== "object" || schema.type !== "object" || typeof schema.properties !== "object") {
|
|
12
|
+
throw new Error("mergeObjectSchemas only supports Type.Object schemas.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
for (const [propertyName, propertySchema] of Object.entries(schema.properties)) {
|
|
16
|
+
if (Object.hasOwn(mergedProperties, propertyName) && mergedProperties[propertyName] !== propertySchema) {
|
|
17
|
+
throw new Error(`mergeObjectSchemas cannot merge duplicate property "${propertyName}".`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
mergedProperties[propertyName] = propertySchema;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return Type.Object(mergedProperties, {
|
|
25
|
+
additionalProperties: false
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
mergeObjectSchemas
|
|
31
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { mergeObjectSchemas } from "./mergeObjectSchemas.js";
|
|
5
|
+
|
|
6
|
+
test("mergeObjectSchemas merges disjoint object schemas", () => {
|
|
7
|
+
const mergedSchema = mergeObjectSchemas([
|
|
8
|
+
Type.Object(
|
|
9
|
+
{
|
|
10
|
+
cursor: Type.Optional(Type.String({ minLength: 1 }))
|
|
11
|
+
},
|
|
12
|
+
{ additionalProperties: false }
|
|
13
|
+
),
|
|
14
|
+
Type.Object(
|
|
15
|
+
{
|
|
16
|
+
search: Type.Optional(Type.String({ minLength: 1 }))
|
|
17
|
+
},
|
|
18
|
+
{ additionalProperties: false }
|
|
19
|
+
)
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
assert.equal(mergedSchema.type, "object");
|
|
23
|
+
assert.equal(mergedSchema.additionalProperties, false);
|
|
24
|
+
assert.equal(typeof mergedSchema.properties.cursor, "object");
|
|
25
|
+
assert.equal(typeof mergedSchema.properties.search, "object");
|
|
26
|
+
assert.deepEqual(mergedSchema.required || [], []);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("mergeObjectSchemas preserves required fields through merged property definitions", () => {
|
|
30
|
+
const mergedSchema = mergeObjectSchemas([
|
|
31
|
+
Type.Object(
|
|
32
|
+
{
|
|
33
|
+
workspaceSlug: Type.String({ minLength: 1 })
|
|
34
|
+
},
|
|
35
|
+
{ additionalProperties: false }
|
|
36
|
+
),
|
|
37
|
+
Type.Object(
|
|
38
|
+
{
|
|
39
|
+
inviteId: Type.String({ minLength: 1 })
|
|
40
|
+
},
|
|
41
|
+
{ additionalProperties: false }
|
|
42
|
+
)
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
assert.deepEqual(mergedSchema.required, ["workspaceSlug", "inviteId"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("mergeObjectSchemas rejects duplicate properties with different schema objects", () => {
|
|
49
|
+
assert.throws(
|
|
50
|
+
() =>
|
|
51
|
+
mergeObjectSchemas([
|
|
52
|
+
Type.Object(
|
|
53
|
+
{
|
|
54
|
+
workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
|
|
55
|
+
},
|
|
56
|
+
{ additionalProperties: false }
|
|
57
|
+
),
|
|
58
|
+
Type.Object(
|
|
59
|
+
{
|
|
60
|
+
workspaceSlug: Type.Optional(Type.String({ minLength: 2 }))
|
|
61
|
+
},
|
|
62
|
+
{ additionalProperties: false }
|
|
63
|
+
)
|
|
64
|
+
]),
|
|
65
|
+
/duplicate property "workspaceSlug"/
|
|
66
|
+
);
|
|
67
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mergeObjectSchemas } from "./mergeObjectSchemas.js";
|
|
2
|
+
import { isRecord as isPlainObject } from "../support/normalize.js";
|
|
3
|
+
|
|
4
|
+
function isPromiseLike(value) {
|
|
5
|
+
return Boolean(value) && typeof value.then === "function";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createErrorFactory(createError) {
|
|
9
|
+
if (typeof createError === "function") {
|
|
10
|
+
return createError;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return (message) => new Error(message);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function mergeValidators(validators = [], options = {}) {
|
|
17
|
+
const sourceValidators = Array.isArray(validators) ? validators : [];
|
|
18
|
+
const context = String(options?.context || "validator").trim() || "validator";
|
|
19
|
+
const requireSchema = options?.requireSchema === true;
|
|
20
|
+
const allowAsyncNormalize = options?.allowAsyncNormalize !== false;
|
|
21
|
+
const requiredSchemaMessage = String(options?.requiredSchemaMessage || `${context}.schema is required.`);
|
|
22
|
+
const normalizeResultMessage = String(options?.normalizeResultMessage || `${context}.normalize must return an object.`);
|
|
23
|
+
const makeError = createErrorFactory(options?.createError);
|
|
24
|
+
const schemas = [];
|
|
25
|
+
const normalizers = [];
|
|
26
|
+
|
|
27
|
+
for (const validator of sourceValidators) {
|
|
28
|
+
if (!isPlainObject(validator)) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (Object.prototype.hasOwnProperty.call(validator, "schema")) {
|
|
33
|
+
schemas.push(validator.schema);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof validator.normalize === "function") {
|
|
37
|
+
normalizers.push(validator.normalize);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (requireSchema && schemas.length < 1) {
|
|
42
|
+
throw makeError(requiredSchemaMessage);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const merged = {};
|
|
46
|
+
if (schemas.length === 1) {
|
|
47
|
+
merged.schema = schemas[0];
|
|
48
|
+
} else if (schemas.length > 1) {
|
|
49
|
+
merged.schema = mergeObjectSchemas(schemas);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (normalizers.length === 1) {
|
|
53
|
+
merged.normalize = normalizers[0];
|
|
54
|
+
} else if (normalizers.length > 1) {
|
|
55
|
+
if (allowAsyncNormalize) {
|
|
56
|
+
merged.normalize = async function normalizeMergedValidators(payload, meta) {
|
|
57
|
+
const normalized = {};
|
|
58
|
+
|
|
59
|
+
for (const normalizer of normalizers) {
|
|
60
|
+
const result = await normalizer(payload, meta);
|
|
61
|
+
if (!isPlainObject(result)) {
|
|
62
|
+
throw makeError(normalizeResultMessage);
|
|
63
|
+
}
|
|
64
|
+
Object.assign(normalized, result);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return normalized;
|
|
68
|
+
};
|
|
69
|
+
} else {
|
|
70
|
+
merged.normalize = function normalizeMergedValidators(payload, meta) {
|
|
71
|
+
const normalized = {};
|
|
72
|
+
|
|
73
|
+
for (const normalizer of normalizers) {
|
|
74
|
+
const result = normalizer(payload, meta);
|
|
75
|
+
if (isPromiseLike(result) || !isPlainObject(result)) {
|
|
76
|
+
throw makeError(normalizeResultMessage);
|
|
77
|
+
}
|
|
78
|
+
Object.assign(normalized, result);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return normalized;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return Object.freeze(merged);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { mergeValidators };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { mergeValidators } from "./mergeValidators.js";
|
|
5
|
+
|
|
6
|
+
test("mergeValidators merges schemas and sync normalizers", () => {
|
|
7
|
+
const merged = mergeValidators(
|
|
8
|
+
[
|
|
9
|
+
{
|
|
10
|
+
schema: Type.Object(
|
|
11
|
+
{
|
|
12
|
+
workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
|
|
13
|
+
},
|
|
14
|
+
{ additionalProperties: false }
|
|
15
|
+
),
|
|
16
|
+
normalize(input = {}) {
|
|
17
|
+
return {
|
|
18
|
+
workspaceSlug: String(input.workspaceSlug || "").trim().toLowerCase()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
schema: Type.Object(
|
|
24
|
+
{
|
|
25
|
+
recordId: Type.Optional(Type.Union([Type.Integer({ minimum: 1 }), Type.String({ minLength: 1 })]))
|
|
26
|
+
},
|
|
27
|
+
{ additionalProperties: false }
|
|
28
|
+
),
|
|
29
|
+
normalize(input = {}) {
|
|
30
|
+
const parsed = Number(input.recordId);
|
|
31
|
+
return {
|
|
32
|
+
recordId: Number.isInteger(parsed) && parsed > 0 ? parsed : 0
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
{
|
|
38
|
+
context: "route params",
|
|
39
|
+
allowAsyncNormalize: false
|
|
40
|
+
}
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
assert.equal(typeof merged.schema, "object");
|
|
44
|
+
assert.equal(typeof merged.normalize, "function");
|
|
45
|
+
assert.deepEqual(merged.normalize({ workspaceSlug: " ACME ", recordId: "42" }), {
|
|
46
|
+
workspaceSlug: "acme",
|
|
47
|
+
recordId: 42
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("mergeValidators merges async normalizers for action validators", async () => {
|
|
52
|
+
const merged = mergeValidators(
|
|
53
|
+
[
|
|
54
|
+
{
|
|
55
|
+
schema: Type.Object(
|
|
56
|
+
{
|
|
57
|
+
workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
|
|
58
|
+
},
|
|
59
|
+
{ additionalProperties: false }
|
|
60
|
+
),
|
|
61
|
+
async normalize(input = {}) {
|
|
62
|
+
return {
|
|
63
|
+
workspaceSlug: String(input.workspaceSlug || "").trim().toLowerCase()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
schema: Type.Object(
|
|
69
|
+
{
|
|
70
|
+
invitesEnabled: Type.Optional(Type.Boolean())
|
|
71
|
+
},
|
|
72
|
+
{ additionalProperties: false }
|
|
73
|
+
),
|
|
74
|
+
normalize(input = {}) {
|
|
75
|
+
return {
|
|
76
|
+
invitesEnabled: input.invitesEnabled === true
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
{
|
|
82
|
+
context: "action input"
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
assert.equal(typeof merged.schema, "object");
|
|
87
|
+
assert.equal(typeof merged.normalize, "function");
|
|
88
|
+
const normalized = await merged.normalize({
|
|
89
|
+
workspaceSlug: " ACME ",
|
|
90
|
+
invitesEnabled: true
|
|
91
|
+
});
|
|
92
|
+
assert.deepEqual(normalized, {
|
|
93
|
+
workspaceSlug: "acme",
|
|
94
|
+
invitesEnabled: true
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("mergeValidators enforces schema requirement when requested", () => {
|
|
99
|
+
assert.throws(
|
|
100
|
+
() =>
|
|
101
|
+
mergeValidators(
|
|
102
|
+
[
|
|
103
|
+
{
|
|
104
|
+
normalize() {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
{
|
|
110
|
+
context: "action input",
|
|
111
|
+
requireSchema: true
|
|
112
|
+
}
|
|
113
|
+
),
|
|
114
|
+
/action input\.schema is required/
|
|
115
|
+
);
|
|
116
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "./inputNormalization.js";
|
|
3
|
+
|
|
4
|
+
function normalizeValidator(validator) {
|
|
5
|
+
if (!validator || typeof validator !== "object" || Array.isArray(validator)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!Object.hasOwn(validator, "schema")) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return validator;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nestValidator(key, validator, { required = true } = {}) {
|
|
17
|
+
const normalizedKey = String(key || "").trim();
|
|
18
|
+
if (!normalizedKey) {
|
|
19
|
+
throw new TypeError("nestValidator requires a non-empty key.");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const normalizedValidator = normalizeValidator(validator);
|
|
23
|
+
if (!normalizedValidator) {
|
|
24
|
+
throw new TypeError(`nestValidator(\"${normalizedKey}\") requires a validator object with schema.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const keySchema = required ? normalizedValidator.schema : Type.Optional(normalizedValidator.schema);
|
|
28
|
+
const schema = Type.Object(
|
|
29
|
+
{
|
|
30
|
+
[normalizedKey]: keySchema
|
|
31
|
+
},
|
|
32
|
+
{ additionalProperties: false }
|
|
33
|
+
);
|
|
34
|
+
const normalizeSection = typeof normalizedValidator.normalize === "function" ? normalizedValidator.normalize : null;
|
|
35
|
+
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
schema,
|
|
38
|
+
async normalize(payload, meta) {
|
|
39
|
+
const source = normalizeObjectInput(payload);
|
|
40
|
+
if (!Object.hasOwn(source, normalizedKey)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sectionPayload = source[normalizedKey];
|
|
45
|
+
const normalizedSection = normalizeSection ? await normalizeSection(sectionPayload, meta) : sectionPayload;
|
|
46
|
+
return {
|
|
47
|
+
[normalizedKey]: normalizedSection
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { nestValidator };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Check } from "typebox/value";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { nestValidator } from "./nestValidator.js";
|
|
6
|
+
|
|
7
|
+
test("nestValidator wraps schema + normalize under one key", async () => {
|
|
8
|
+
const baseValidator = Object.freeze({
|
|
9
|
+
schema: Type.Object(
|
|
10
|
+
{
|
|
11
|
+
name: Type.Optional(Type.String({ minLength: 1 }))
|
|
12
|
+
},
|
|
13
|
+
{ additionalProperties: false }
|
|
14
|
+
),
|
|
15
|
+
normalize(payload = {}) {
|
|
16
|
+
return {
|
|
17
|
+
...(Object.hasOwn(payload, "name") ? { name: String(payload.name).trim() } : {})
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
const validator = nestValidator("payload", baseValidator);
|
|
22
|
+
|
|
23
|
+
const normalized = await validator.normalize({
|
|
24
|
+
payload: {
|
|
25
|
+
name: " Acme "
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
assert.deepEqual(normalized, {
|
|
30
|
+
payload: {
|
|
31
|
+
name: "Acme"
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
assert.equal(Check(validator.schema, normalized), true);
|
|
35
|
+
assert.equal(Check(validator.schema, {}), false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("nestValidator can define optional nested key", async () => {
|
|
39
|
+
const validator = nestValidator(
|
|
40
|
+
"patch",
|
|
41
|
+
Object.freeze({
|
|
42
|
+
schema: Type.Object(
|
|
43
|
+
{
|
|
44
|
+
title: Type.Optional(Type.String({ minLength: 1 }))
|
|
45
|
+
},
|
|
46
|
+
{ additionalProperties: false }
|
|
47
|
+
)
|
|
48
|
+
}),
|
|
49
|
+
{ required: false }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
assert.deepEqual(await validator.normalize({}), {});
|
|
53
|
+
assert.equal(Check(validator.schema, {}), true);
|
|
54
|
+
assert.equal(Check(validator.schema, { patch: { title: "X" } }), true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("nestValidator rejects invalid key and validator", () => {
|
|
58
|
+
assert.throws(() => nestValidator("", { schema: Type.Object({}) }), /requires a non-empty key/);
|
|
59
|
+
assert.throws(() => nestValidator("payload", null), /requires a validator object with schema/);
|
|
60
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { normalizeObjectInput } from "./inputNormalization.js";
|
|
3
|
+
import { normalizePositiveInteger, normalizeText } from "../support/normalize.js";
|
|
4
|
+
|
|
5
|
+
function normalizeRecordId(value) {
|
|
6
|
+
return normalizePositiveInteger(normalizeText(value));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const positiveIntegerValidator = Object.freeze({
|
|
10
|
+
schema: Type.Union([
|
|
11
|
+
Type.Integer({ minimum: 1 }),
|
|
12
|
+
Type.String({ minLength: 1, pattern: "^[1-9][0-9]*$" })
|
|
13
|
+
]),
|
|
14
|
+
normalize: normalizeRecordId
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const recordIdParamsValidator = Object.freeze({
|
|
18
|
+
schema: Type.Object(
|
|
19
|
+
{
|
|
20
|
+
recordId: Type.Optional(positiveIntegerValidator.schema)
|
|
21
|
+
},
|
|
22
|
+
{ additionalProperties: false }
|
|
23
|
+
),
|
|
24
|
+
normalize(input = {}) {
|
|
25
|
+
const source = normalizeObjectInput(input);
|
|
26
|
+
const normalized = {};
|
|
27
|
+
|
|
28
|
+
if (Object.hasOwn(source, "recordId")) {
|
|
29
|
+
normalized.recordId = normalizeRecordId(source.recordId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export { recordIdParamsValidator, positiveIntegerValidator };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import { recordIdParamsValidator } from "./recordIdParamsValidator.js";
|
|
5
|
+
|
|
6
|
+
test("recordIdParamsValidator normalizes string id to positive integer", () => {
|
|
7
|
+
assert.deepEqual(recordIdParamsValidator.normalize({ recordId: "42" }), {
|
|
8
|
+
recordId: 42
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("recordIdParamsValidator normalizes invalid id to 0", () => {
|
|
13
|
+
assert.deepEqual(recordIdParamsValidator.normalize({ recordId: "nope" }), {
|
|
14
|
+
recordId: 0
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("recordIdParamsValidator keeps absent key absent", () => {
|
|
19
|
+
assert.deepEqual(recordIdParamsValidator.normalize({}), {});
|
|
20
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function normalizeRequiredFieldList(value) {
|
|
2
|
+
if (!Array.isArray(value)) {
|
|
3
|
+
return Object.freeze([]);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const normalized = value
|
|
7
|
+
.map((entry) => String(entry || "").trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
|
|
10
|
+
return Object.freeze(Array.from(new Set(normalized)));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function deriveRequiredFieldsFromSchema(schema) {
|
|
14
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
|
|
15
|
+
return Object.freeze([]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return normalizeRequiredFieldList(schema.required);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function deriveResourceRequiredMetadata(resourceSchema) {
|
|
22
|
+
const operations = resourceSchema && typeof resourceSchema === "object"
|
|
23
|
+
? resourceSchema.operations
|
|
24
|
+
: null;
|
|
25
|
+
|
|
26
|
+
const createSchema = operations?.create?.bodyValidator?.schema;
|
|
27
|
+
const replaceSchema = operations?.replace?.bodyValidator?.schema;
|
|
28
|
+
const patchSchema = operations?.patch?.bodyValidator?.schema;
|
|
29
|
+
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
create: deriveRequiredFieldsFromSchema(createSchema),
|
|
32
|
+
replace: deriveRequiredFieldsFromSchema(replaceSchema),
|
|
33
|
+
patch: deriveRequiredFieldsFromSchema(patchSchema)
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
normalizeRequiredFieldList,
|
|
39
|
+
deriveRequiredFieldsFromSchema,
|
|
40
|
+
deriveResourceRequiredMetadata
|
|
41
|
+
};
|