@mh-gg/schema 0.1.1-alpha.20260613T085325975Z
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/package.json +39 -0
- package/schemas/app-composition.schema.json +684 -0
- package/schemas/example-app.schema.json +64 -0
- package/schemas/example-demo.schema.json +45 -0
- package/schemas/micro-plugin.schema.json +20 -0
- package/src/actions.cjs +31 -0
- package/src/app.d.ts +119 -0
- package/src/builder/app.cjs +157 -0
- package/src/builder/effects.cjs +116 -0
- package/src/builder/guards.cjs +35 -0
- package/src/builder/index.cjs +11 -0
- package/src/builder/matterhornApp/build.cjs +137 -0
- package/src/builder/matterhornApp/bundle.cjs +89 -0
- package/src/builder/matterhornApp/demo.cjs +161 -0
- package/src/builder/matterhornApp/demoAliases.cjs +50 -0
- package/src/builder/matterhornApp/descriptors.cjs +59 -0
- package/src/builder/matterhornApp/exports.cjs +95 -0
- package/src/builder/matterhornApp/frontend.cjs +80 -0
- package/src/builder/matterhornApp/plugins.cjs +95 -0
- package/src/builder/matterhornApp/shared.cjs +105 -0
- package/src/builder/matterhornApp.cjs +7 -0
- package/src/builder/model.cjs +172 -0
- package/src/builder/notifications.cjs +51 -0
- package/src/builder/refs.cjs +33 -0
- package/src/builder/schema.cjs +101 -0
- package/src/builder/streamKey.cjs +27 -0
- package/src/composition.cjs +157 -0
- package/src/configured.cjs +41 -0
- package/src/configured.d.ts +1 -0
- package/src/imports/loader.cjs +86 -0
- package/src/index.cjs +12 -0
- package/src/index.d.ts +168 -0
- package/src/json.cjs +70 -0
- package/src/jsonSchema/validator.cjs +131 -0
- package/src/microPlugin.cjs +112 -0
- package/src/model/collections.cjs +57 -0
- package/src/model/effects.cjs +149 -0
- package/src/model/expressions.cjs +85 -0
- package/src/model/guards.cjs +139 -0
- package/src/model/index.cjs +9 -0
- package/src/model/partitionOperands.cjs +89 -0
- package/src/model/partitionValidator.cjs +95 -0
- package/src/model/payload.cjs +93 -0
- package/src/model/plugin.cjs +146 -0
- package/src/notifications.cjs +41 -0
- package/src/notifications.d.ts +63 -0
- package/src/registry.cjs +63 -0
- package/src/runtime-exports.d.ts +65 -0
- package/src/streamKey.d.ts +15 -0
- package/src/types/actionTypes.cjs +164 -0
- package/src/types/coreActionPayloadSchemas.cjs +35 -0
- package/src/types/coreFeatures.cjs +29 -0
- package/src/types/entities.cjs +131 -0
- package/src/types/entityAliases.cjs +84 -0
- package/src/types/generator.cjs +145 -0
- package/src/types/index.cjs +5 -0
- package/src/types/interfaceBlock.cjs +41 -0
- package/src/types/pluginEntities.cjs +83 -0
- package/src/types/schema.cjs +148 -0
- package/src/types/standardPluginEntityTypes.cjs +135 -0
- package/test/content-lww-contract.test.cjs +104 -0
- package/test/partitionValidator.test.cjs +92 -0
- package/test/schema-array-effects.test.cjs +116 -0
- package/test/schema-builder-types.test.cjs +383 -0
- package/test/schema-composition.test.cjs +144 -0
- package/test/schema-configured-builders.test.cjs +286 -0
- package/test/schema-imports.test.cjs +66 -0
- package/test/schema-model-interpreter.test.cjs +273 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const { hashCanonical } = require("@mh-gg/base");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const { assertJsonSerializable, assertPlainObject, cloneJson, freezeJson, nonEmptyString } = require("./json.cjs");
|
|
4
|
+
const { builtinMicroPluginRegistry } = require("./registry.cjs");
|
|
5
|
+
const { loadJsonWithImports } = require("./imports/loader.cjs");
|
|
6
|
+
const { assertValidJsonSchema } = require("./jsonSchema/validator.cjs");
|
|
7
|
+
const { normalizeNotifications } = require("./notifications.cjs");
|
|
8
|
+
|
|
9
|
+
const APP_COMPOSITION_SCHEMA_KIND = "matterhorn.app-composition.schema";
|
|
10
|
+
const APP_COMPOSITION_SCHEMA_VERSION = 1;
|
|
11
|
+
|
|
12
|
+
function normalizePluginRef(ref, registry = builtinMicroPluginRegistry) {
|
|
13
|
+
if (typeof ref === "string") {
|
|
14
|
+
const record = registry.get(ref);
|
|
15
|
+
return { key: record.schema.key, id: record.schema.id, version: record.schema.version, required: true };
|
|
16
|
+
}
|
|
17
|
+
const value = assertPlainObject(ref, "composition plugin ref");
|
|
18
|
+
const record = registry.get(value.key || value.id);
|
|
19
|
+
if (value.id && value.id !== record.schema.id) throw new Error(`Micro plugin ${value.key || value.id} resolved to ${record.schema.id}, expected ${value.id}`);
|
|
20
|
+
if (value.version && value.version !== record.schema.version) throw new Error(`Micro plugin ${record.schema.id} version mismatch: expected ${value.version}, got ${record.schema.version}`);
|
|
21
|
+
return {
|
|
22
|
+
key: record.schema.key,
|
|
23
|
+
id: record.schema.id,
|
|
24
|
+
version: record.schema.version,
|
|
25
|
+
required: value.required !== false,
|
|
26
|
+
...(value.config === undefined ? {} : { config: value.config })
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizeActionDescriptor(action, name, composition) {
|
|
31
|
+
const value = assertPlainObject(action, `action ${name}`);
|
|
32
|
+
const plugin = value.plugin || value.pluginKey || value.pluginId || "primary";
|
|
33
|
+
const type = nonEmptyString(value.type || value.operation, `action ${name}.type`);
|
|
34
|
+
return {
|
|
35
|
+
name: value.name || name,
|
|
36
|
+
plugin,
|
|
37
|
+
type,
|
|
38
|
+
...(value.label ? { label: String(value.label) } : {}),
|
|
39
|
+
...(value.payloadDefaults === undefined ? {} : { payloadDefaults: value.payloadDefaults }),
|
|
40
|
+
...(value.payloadSchema === undefined ? {} : { payloadSchema: value.payloadSchema }),
|
|
41
|
+
...(value.requiredRole === undefined ? {} : { requiredRole: value.requiredRole })
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeActions(actions = {}, composition) {
|
|
46
|
+
if (Array.isArray(actions)) {
|
|
47
|
+
return Object.freeze(actions.map((action, index) => normalizeActionDescriptor(action, action.name || `action${index + 1}`, composition)));
|
|
48
|
+
}
|
|
49
|
+
assertPlainObject(actions, "composition actions");
|
|
50
|
+
return Object.freeze(Object.entries(actions).map(([name, action]) => normalizeActionDescriptor(action, name, composition)));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeSharedScopes(sharedScopes = {}) {
|
|
54
|
+
assertPlainObject(sharedScopes, "composition sharedScopes");
|
|
55
|
+
return cloneJson(sharedScopes);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createAppCompositionSchema(input = {}, registry = builtinMicroPluginRegistry) {
|
|
59
|
+
const value = assertPlainObject(input, "app composition schema");
|
|
60
|
+
const app = assertPlainObject(value.app || { id: value.appId, version: value.version, name: value.name }, "composition app");
|
|
61
|
+
const primaryPlugin = assertPlainObject(value.primaryPlugin, "composition primaryPlugin");
|
|
62
|
+
const pluginRefs = (value.plugins || []).map((ref) => normalizePluginRef(ref, registry));
|
|
63
|
+
const deduped = [];
|
|
64
|
+
const seen = new Set([primaryPlugin.id]);
|
|
65
|
+
for (const ref of pluginRefs) {
|
|
66
|
+
if (seen.has(ref.id)) continue;
|
|
67
|
+
seen.add(ref.id);
|
|
68
|
+
deduped.push(ref);
|
|
69
|
+
}
|
|
70
|
+
const composition = {
|
|
71
|
+
kind: APP_COMPOSITION_SCHEMA_KIND,
|
|
72
|
+
schemaVersion: APP_COMPOSITION_SCHEMA_VERSION,
|
|
73
|
+
app: {
|
|
74
|
+
id: nonEmptyString(app.id, "composition app.id"),
|
|
75
|
+
version: nonEmptyString(app.version, "composition app.version"),
|
|
76
|
+
name: nonEmptyString(app.name, "composition app.name")
|
|
77
|
+
},
|
|
78
|
+
primaryPlugin: {
|
|
79
|
+
id: nonEmptyString(primaryPlugin.id, "composition primaryPlugin.id"),
|
|
80
|
+
version: nonEmptyString(primaryPlugin.version, "composition primaryPlugin.version"),
|
|
81
|
+
...(primaryPlugin.source === undefined ? {} : { source: primaryPlugin.source }),
|
|
82
|
+
...(primaryPlugin.model === undefined ? {} : { model: cloneJson(primaryPlugin.model) })
|
|
83
|
+
},
|
|
84
|
+
plugins: deduped,
|
|
85
|
+
...(value.sharedScopes === undefined ? {} : { sharedScopes: normalizeSharedScopes(value.sharedScopes) }),
|
|
86
|
+
views: cloneJson(value.views || []),
|
|
87
|
+
actions: normalizeActions(value.actions || {}, value),
|
|
88
|
+
routes: cloneJson(value.routes || []),
|
|
89
|
+
...(value.notifications === undefined ? {} : { notifications: normalizeNotifications(value.notifications) })
|
|
90
|
+
};
|
|
91
|
+
assertJsonSerializable(composition, "app composition schema");
|
|
92
|
+
assertValidJsonSchema("app-composition.schema.json", composition, { label: "app composition schema" });
|
|
93
|
+
return freezeJson({ ...composition, schemaHash: hashCanonical(composition) });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateAppCompositionSchema(schema, options = {}) {
|
|
97
|
+
const value = assertPlainObject(schema, "app composition schema");
|
|
98
|
+
if (value.kind !== APP_COMPOSITION_SCHEMA_KIND) throw new Error("Invalid app composition schema kind");
|
|
99
|
+
if (value.schemaVersion !== APP_COMPOSITION_SCHEMA_VERSION) throw new Error("Invalid app composition schema version");
|
|
100
|
+
if (!Array.isArray(value.plugins) || !Array.isArray(value.actions) || value.plugins.some((plugin) => typeof plugin === "string")) return createAppCompositionSchema(value, options.registry || builtinMicroPluginRegistry);
|
|
101
|
+
assertPlainObject(value.app, "composition app");
|
|
102
|
+
assertPlainObject(value.primaryPlugin, "composition primaryPlugin");
|
|
103
|
+
nonEmptyString(value.app.id, "composition app.id");
|
|
104
|
+
nonEmptyString(value.app.version, "composition app.version");
|
|
105
|
+
nonEmptyString(value.primaryPlugin.id, "composition primaryPlugin.id");
|
|
106
|
+
if (!Array.isArray(value.plugins)) throw new Error("composition plugins must be an array");
|
|
107
|
+
if (!Array.isArray(value.actions)) throw new Error("composition actions must be an array");
|
|
108
|
+
if (!Array.isArray(value.views)) throw new Error("composition views must be an array");
|
|
109
|
+
if (value.sharedScopes !== undefined) assertPlainObject(value.sharedScopes, "composition sharedScopes");
|
|
110
|
+
if (value.notifications !== undefined) assertPlainObject(value.notifications, "composition notifications");
|
|
111
|
+
assertJsonSerializable(value, "app composition schema");
|
|
112
|
+
assertValidJsonSchema("app-composition.schema.json", value, { label: "app composition schema" });
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function loadAppCompositionSchemaFile(filePath, options = {}) {
|
|
117
|
+
const resolved = path.resolve(filePath);
|
|
118
|
+
const raw = loadJsonWithImports(resolved, { baseDir: options.baseDir || path.dirname(resolved) });
|
|
119
|
+
return createAppCompositionSchema(raw, options.registry || builtinMicroPluginRegistry);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function appCompositionSchemaHash(schema) {
|
|
123
|
+
const copy = { ...validateAppCompositionSchema(schema) };
|
|
124
|
+
delete copy.schemaHash;
|
|
125
|
+
return hashCanonical(copy);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveCompositionHostPluginEntries(schema, registry = builtinMicroPluginRegistry) {
|
|
129
|
+
const composition = validateAppCompositionSchema(schema, { registry });
|
|
130
|
+
return composition.plugins.map((ref) => {
|
|
131
|
+
const record = registry.get(ref.key || ref.id);
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
plugin: record.plugin,
|
|
134
|
+
packageName: record.packageName,
|
|
135
|
+
exportName: record.exportName,
|
|
136
|
+
source: record.source,
|
|
137
|
+
schema: record.schema,
|
|
138
|
+
config: ref.config
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compositionPluginIds(schema) {
|
|
144
|
+
const composition = validateAppCompositionSchema(schema);
|
|
145
|
+
return Object.freeze([composition.primaryPlugin.id, ...composition.plugins.map((plugin) => plugin.id)]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
APP_COMPOSITION_SCHEMA_KIND,
|
|
150
|
+
APP_COMPOSITION_SCHEMA_VERSION,
|
|
151
|
+
appCompositionSchemaHash,
|
|
152
|
+
compositionPluginIds,
|
|
153
|
+
loadAppCompositionSchemaFile,
|
|
154
|
+
createAppCompositionSchema,
|
|
155
|
+
resolveCompositionHostPluginEntries,
|
|
156
|
+
validateAppCompositionSchema
|
|
157
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
function createConfiguredSchemaApi(schema, entries = []) {
|
|
2
|
+
const registry = schema.createMicroPluginRegistry(entries);
|
|
3
|
+
const configured = {
|
|
4
|
+
...schema,
|
|
5
|
+
builtinMicroPluginRegistry: registry,
|
|
6
|
+
getMicroPluginSchema(selection) {
|
|
7
|
+
return registry.get(selection).schema;
|
|
8
|
+
},
|
|
9
|
+
listMicroPluginSchemas() {
|
|
10
|
+
return registry.schemas;
|
|
11
|
+
},
|
|
12
|
+
createAppCompositionSchema(input = {}, selectedRegistry = registry) {
|
|
13
|
+
return schema.createAppCompositionSchema(input, selectedRegistry);
|
|
14
|
+
},
|
|
15
|
+
validateAppCompositionSchema(value, options = {}) {
|
|
16
|
+
return schema.validateAppCompositionSchema(value, { ...options, registry: options.registry || registry });
|
|
17
|
+
},
|
|
18
|
+
loadAppCompositionSchemaFile(filePath, options = {}) {
|
|
19
|
+
return schema.loadAppCompositionSchemaFile(filePath, { ...options, registry: options.registry || registry });
|
|
20
|
+
},
|
|
21
|
+
resolveCompositionHostPluginEntries(value, selectedRegistry = registry) {
|
|
22
|
+
return schema.resolveCompositionHostPluginEntries(value, selectedRegistry);
|
|
23
|
+
},
|
|
24
|
+
createActionDispatchersFromComposition(value, options = {}) {
|
|
25
|
+
return schema.createActionDispatchersFromComposition(value, { ...options, registry: options.registry || registry });
|
|
26
|
+
},
|
|
27
|
+
pluginIdForAction(descriptor, composition, selectedRegistry = registry) {
|
|
28
|
+
return schema.pluginIdForAction(descriptor, composition, selectedRegistry);
|
|
29
|
+
},
|
|
30
|
+
defineApp(spec = {}) {
|
|
31
|
+
return schema.defineApp({ ...spec, registry: spec.registry || registry });
|
|
32
|
+
},
|
|
33
|
+
generateAppTypeDeclaration(value, options = {}) {
|
|
34
|
+
return schema.generateAppTypeDeclaration(value, { ...options, registry: options.registry || registry });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
configured.reusableMicroPluginSchemas = configured.listMicroPluginSchemas();
|
|
38
|
+
return Object.freeze(configured);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { createConfiguredSchemaApi };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createConfiguredSchemaApi(schema: Record<string, any>, entries?: any[]): Record<string, any>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { cloneJson } = require('../json.cjs');
|
|
4
|
+
|
|
5
|
+
function readJson(filePath) {
|
|
6
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function assertRelativeJson(ref, fromFile, baseDir) {
|
|
10
|
+
if (typeof ref !== 'string' || ref.length === 0) throw new Error('Import path must be a non-empty string');
|
|
11
|
+
if (/^[a-z]+:/i.test(ref) || path.isAbsolute(ref)) throw new Error(`Schema import ${ref} must be a relative JSON file`);
|
|
12
|
+
const resolved = path.resolve(path.dirname(fromFile), ref);
|
|
13
|
+
if (!resolved.endsWith('.json')) throw new Error(`Schema import ${ref} must point to a .json file`);
|
|
14
|
+
if (baseDir) {
|
|
15
|
+
const root = path.resolve(baseDir);
|
|
16
|
+
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) throw new Error(`Schema import ${ref} escapes ${root}`);
|
|
17
|
+
}
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getPath(root, dotted) {
|
|
22
|
+
if (!dotted) return root;
|
|
23
|
+
return dotted.split('.').reduce((value, part) => (value == null ? undefined : value[part]), root);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setPath(root, dotted, next, merge = 'replace') {
|
|
27
|
+
const parts = dotted.split('.').filter(Boolean);
|
|
28
|
+
if (!parts.length) return mergeValue(root, next, merge);
|
|
29
|
+
const out = Array.isArray(root) ? root.slice() : { ...(root || {}) };
|
|
30
|
+
let cursor = out;
|
|
31
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
32
|
+
const part = parts[index];
|
|
33
|
+
cursor[part] = Array.isArray(cursor[part]) ? cursor[part].slice() : { ...(cursor[part] || {}) };
|
|
34
|
+
cursor = cursor[part];
|
|
35
|
+
}
|
|
36
|
+
const last = parts[parts.length - 1];
|
|
37
|
+
cursor[last] = mergeValue(cursor[last], next, merge);
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function mergeValue(current, next, merge) {
|
|
42
|
+
if (merge === 'append') return [...(Array.isArray(current) ? current : []), ...(Array.isArray(next) ? next : [next])];
|
|
43
|
+
if (merge === 'merge') return { ...(current || {}), ...(next || {}) };
|
|
44
|
+
return cloneJson(next);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function importItems(imports) {
|
|
48
|
+
if (!imports) return [];
|
|
49
|
+
if (Array.isArray(imports)) return imports.map((item) => typeof item === 'string' ? { path: item } : item);
|
|
50
|
+
if (imports && typeof imports === 'object') {
|
|
51
|
+
return Object.entries(imports).flatMap(([into, value]) => {
|
|
52
|
+
const values = Array.isArray(value) ? value : [value];
|
|
53
|
+
return values.map((item) => typeof item === 'string' ? { path: item, into } : { ...item, into: item.into || into });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
throw new Error('$imports must be an array or object');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadJsonWithImports(filePath, options = {}, stack = []) {
|
|
60
|
+
const resolved = path.resolve(filePath);
|
|
61
|
+
const baseDir = options.baseDir || path.dirname(resolved);
|
|
62
|
+
if (stack.includes(resolved)) throw new Error(`Circular schema import: ${[...stack, resolved].join(' -> ')}`);
|
|
63
|
+
const raw = readJson(resolved);
|
|
64
|
+
let document = { ...raw };
|
|
65
|
+
delete document.$imports;
|
|
66
|
+
|
|
67
|
+
for (const item of importItems(raw.$imports)) {
|
|
68
|
+
if (!item || typeof item !== 'object') throw new Error('Schema import entries must be objects');
|
|
69
|
+
const importedPath = assertRelativeJson(item.path, resolved, baseDir);
|
|
70
|
+
const importedDoc = loadJsonWithImports(importedPath, { ...options, baseDir }, [...stack, resolved]);
|
|
71
|
+
const selected = item.select ? getPath(importedDoc, item.select) : importedDoc;
|
|
72
|
+
if (selected === undefined) throw new Error(`Schema import ${item.path} did not contain ${item.select}`);
|
|
73
|
+
if (!item.into) {
|
|
74
|
+
document = mergeValue(document, selected, item.merge || 'merge');
|
|
75
|
+
} else {
|
|
76
|
+
document = setPath(document, item.into, selected, item.merge || 'replace');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return document;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
getPath,
|
|
84
|
+
loadJsonWithImports,
|
|
85
|
+
setPath
|
|
86
|
+
};
|
package/src/index.cjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
...require('./json.cjs'),
|
|
3
|
+
...require('./microPlugin.cjs'),
|
|
4
|
+
...require('./registry.cjs'),
|
|
5
|
+
...require('./composition.cjs'),
|
|
6
|
+
...require('./actions.cjs'),
|
|
7
|
+
...require('./imports/loader.cjs'),
|
|
8
|
+
...require('./jsonSchema/validator.cjs'),
|
|
9
|
+
...require('./model/index.cjs'),
|
|
10
|
+
...require('./types/index.cjs'),
|
|
11
|
+
...require('./builder/index.cjs')
|
|
12
|
+
};
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
export type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
|
|
2
|
+
|
|
3
|
+
export type PayloadSchemaSpec =
|
|
4
|
+
| { type?: "any" | "string" | "number" | "boolean" | "enum" | "array" | "object" | "record" | "literal"; [key: string]: unknown }
|
|
5
|
+
| string;
|
|
6
|
+
|
|
7
|
+
export interface Schema<T> {
|
|
8
|
+
optional(): Schema<T | undefined>;
|
|
9
|
+
nullable(): Schema<T | null>;
|
|
10
|
+
default(value: NonNullable<T>): Schema<NonNullable<T>>;
|
|
11
|
+
clearable(): Schema<T | null>;
|
|
12
|
+
toJSON(): PayloadSchemaSpec;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type InferSchema<S> = S extends Schema<infer T> ? T : unknown;
|
|
16
|
+
type RequiredKeys<S> = { [K in keyof S]: undefined extends InferSchema<S[K]> ? never : K }[keyof S];
|
|
17
|
+
type OptionalKeys<S> = { [K in keyof S]: undefined extends InferSchema<S[K]> ? K : never }[keyof S];
|
|
18
|
+
export type PayloadOf<S> = { [K in RequiredKeys<S>]: Exclude<InferSchema<S[K]>, undefined> } & { [K in OptionalKeys<S>]?: InferSchema<S[K]> };
|
|
19
|
+
export type { AppDef, AppSpec, EmitOptions, EmitResult, MatterhornAppOptions, MatterhornFrontendCommand, MatterhornFrontendIcon, MatterhornFrontendOptions, NotificationDef, NotificationAudience, NotificationScope, NotificationEntity, NotificationPresentation, NotificationLink, NotificationRead, NotificationReadPolicy, NotificationDelivery, NotificationOn, NotificationExpression, NotificationsBlock } from "./app";
|
|
20
|
+
import type { AppDef, AppSpec, EmitOptions, EmitResult, MatterhornAppOptions } from "./app";
|
|
21
|
+
|
|
22
|
+
export const p: {
|
|
23
|
+
any(): Schema<unknown>;
|
|
24
|
+
string(opts?: { max?: number; min?: number; nonEmpty?: boolean; nullable?: boolean; trim?: boolean; truncate?: boolean }): Schema<string>;
|
|
25
|
+
number(opts?: { min?: number; max?: number }): Schema<number>;
|
|
26
|
+
boolean(): Schema<boolean>;
|
|
27
|
+
enum<const V extends readonly string[]>(values: V, opts?: { fallback?: V[number] }): Schema<V[number]>;
|
|
28
|
+
array<T>(items?: Schema<T>, opts?: Record<string, unknown>): Schema<T[]>;
|
|
29
|
+
object<const S extends Record<string, Schema<unknown>>>(shape: S, opts?: { additional?: boolean }): Schema<PayloadOf<S>>;
|
|
30
|
+
record<T>(values?: Schema<T>, opts?: Record<string, unknown>): Schema<Record<string, T>>;
|
|
31
|
+
literal<const T extends Json>(value: T): Schema<T>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type Ref<T = unknown> = string & { readonly __ref?: T };
|
|
35
|
+
export type PayloadRefs<P> = {
|
|
36
|
+
payload<K extends keyof P & string>(key: K): Ref<P[K]>;
|
|
37
|
+
payload<K extends keyof P & string, F extends Json>(key: K, fallback: F): { $expr: Ref<P[K]>; fallback: F };
|
|
38
|
+
actor(field?: "memberId" | "displayName" | string): Ref<string>;
|
|
39
|
+
app(field?: "id" | string): Ref<string>;
|
|
40
|
+
room(field?: "id" | string): Ref<string>;
|
|
41
|
+
profile(field?: "id" | string): Ref<string>;
|
|
42
|
+
now(): Ref<number>;
|
|
43
|
+
operation(field?: "id" | "type" | string): Ref<string>;
|
|
44
|
+
newId(prefix: string): Ref<string>;
|
|
45
|
+
literal(value: Json): { $literal: Json };
|
|
46
|
+
expr(value: string): { $expr: string };
|
|
47
|
+
};
|
|
48
|
+
export const ref: PayloadRefs<Record<string, never>>;
|
|
49
|
+
|
|
50
|
+
export type { ReadClass, IntegrityClass, ShapeSpec } from "./streamKey";
|
|
51
|
+
|
|
52
|
+
export interface Collection<T = unknown> {
|
|
53
|
+
readonly name: string;
|
|
54
|
+
readonly storage: "array" | "map";
|
|
55
|
+
readonly readClass: import("./streamKey").ReadClass;
|
|
56
|
+
readonly integrityClass: import("./streamKey").IntegrityClass;
|
|
57
|
+
}
|
|
58
|
+
export type Effect = { kind: string; [key: string]: unknown };
|
|
59
|
+
export type Guard = { kind: string; [key: string]: unknown };
|
|
60
|
+
export type GuardOperand = { kind: string; [key: string]: unknown };
|
|
61
|
+
|
|
62
|
+
export const fx: {
|
|
63
|
+
noop(): Effect;
|
|
64
|
+
create<T>(collection: Collection<T> | string, spec: { id?: Ref<string>; idPrefix?: string; fields: Partial<Record<keyof T & string, unknown>> | Record<string, unknown>; activity?: string; recordLabel?: string }): Effect;
|
|
65
|
+
update<T>(collection: Collection<T> | string, spec: { id: Ref<string>; set?: Partial<Record<keyof T & string, unknown>> | Record<string, unknown>; fields?: Record<string, unknown>; activity?: string; recordLabel?: string }): Effect;
|
|
66
|
+
mark<T>(collection: Collection<T> | string, spec: { id: Ref<string>; set?: Partial<Record<keyof T & string, unknown>> | Record<string, unknown>; fields?: Record<string, unknown>; activity?: string; recordLabel?: string }): Effect;
|
|
67
|
+
merge(spec: { path: string; fields?: unknown; deleteNullFields?: boolean; activity?: string }): Effect;
|
|
68
|
+
append(pathOrCollection: Collection | string, item?: unknown): Effect;
|
|
69
|
+
toggleReaction<T>(collection: Collection<T> | string, spec: { id: Ref<string>; emoji: Ref<string>; activity?: string; recordLabel?: string }): Effect;
|
|
70
|
+
insertIntoArray<T>(collection: Collection<T> | string, spec: { id?: Ref<string>; matchId?: Ref<string>; field: keyof T & string; value: unknown; at?: unknown; position?: unknown; activity?: string; recordLabel?: string }): Effect;
|
|
71
|
+
removeFromArray<T>(collection: Collection<T> | string, spec: { id?: Ref<string>; matchId?: Ref<string>; field: keyof T & string; value: unknown; activity?: string; recordLabel?: string }): Effect;
|
|
72
|
+
upsertActor<T>(collection: Collection<T> | string, spec: { fields: Partial<Record<keyof T & string, unknown>> | Record<string, unknown>; activity?: string; recordLabel?: string }): Effect;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const guard: {
|
|
76
|
+
path(path: string): GuardOperand;
|
|
77
|
+
length(path: string): GuardOperand;
|
|
78
|
+
value(value: Json): GuardOperand;
|
|
79
|
+
expr(expr: string): GuardOperand;
|
|
80
|
+
ownerOrRole<T>(collection: Collection<T> | string, spec: { id?: Ref<string>; match?: Ref<string>; ownerField?: keyof T & string; roles?: string[]; message?: string; recordLabel?: string }): Guard;
|
|
81
|
+
flagClear<T>(collection: Collection<T> | string, spec: { id?: Ref<string>; match?: Ref<string>; flag: keyof T & string; message?: string; recordLabel?: string }): Guard;
|
|
82
|
+
eq(left: unknown, right: unknown, message?: string): Guard;
|
|
83
|
+
ne(left: unknown, right: unknown, message?: string): Guard;
|
|
84
|
+
lt(left: unknown, right: unknown, message?: string): Guard;
|
|
85
|
+
lte(left: unknown, right: unknown, message?: string): Guard;
|
|
86
|
+
gt(left: unknown, right: unknown, message?: string): Guard;
|
|
87
|
+
gte(left: unknown, right: unknown, message?: string): Guard;
|
|
88
|
+
exists(path: string, message?: string): Guard;
|
|
89
|
+
and(...guards: Guard[]): Guard;
|
|
90
|
+
or(...guards: Guard[]): Guard;
|
|
91
|
+
not(item: Guard): Guard;
|
|
92
|
+
noop(): Guard;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type OperationDef<P = unknown> = { authorize?: object; payload: object; effects: Effect[]; guards?: Guard[] };
|
|
96
|
+
export function op<const S extends Record<string, Schema<unknown>>, P = PayloadOf<S>>(
|
|
97
|
+
spec: { authorize?: object; payload?: S },
|
|
98
|
+
build: (builders: { ref: PayloadRefs<P>; fx: typeof fx; guard: typeof guard }) => { effects: Effect[]; guards?: Guard[]; guard?: Guard }
|
|
99
|
+
): OperationDef<P>;
|
|
100
|
+
|
|
101
|
+
export interface Model<State = unknown, Operations extends Record<string, OperationDef> = Record<string, OperationDef>> {
|
|
102
|
+
readonly collections: { [K in keyof State & string]: Collection<State[K] extends Array<infer T> ? T : State[K] extends Record<string, infer T> ? T : unknown> };
|
|
103
|
+
withOperations<O extends Record<string, OperationDef>>(ops: O): Model<State, O>;
|
|
104
|
+
operation<K extends string, O extends OperationDef>(name: K, definition: O): Model<State, Operations & Record<K, O>>;
|
|
105
|
+
toJSON(): object;
|
|
106
|
+
}
|
|
107
|
+
export function defineModel<const State extends Record<string, unknown>>(spec: {
|
|
108
|
+
roles?: Record<string, { name?: string; rank?: number; system?: boolean; description?: string; color?: string }>;
|
|
109
|
+
state: State | { initial: State; roomIdPath?: string };
|
|
110
|
+
shapes?: Record<keyof State & string, ShapeSpec>;
|
|
111
|
+
views?: object;
|
|
112
|
+
publicView?: object;
|
|
113
|
+
capabilities?: object;
|
|
114
|
+
}): Model<State>;
|
|
115
|
+
|
|
116
|
+
export interface QueryDef<Result = unknown> { toJSON(): object; }
|
|
117
|
+
export const q: {
|
|
118
|
+
state(): QueryDef<unknown>;
|
|
119
|
+
collection<T>(collection: Collection<T>, options?: { path?: string; includeArchived?: boolean }): QueryDef<T[]>;
|
|
120
|
+
count<T>(collection: Collection<T>, options?: { path?: string }): QueryDef<number>;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export interface ScopeDef {
|
|
124
|
+
readonly key: string;
|
|
125
|
+
readonly view: object;
|
|
126
|
+
readonly edit: object;
|
|
127
|
+
readonly readClass: ReadClass;
|
|
128
|
+
readonly integrityClass: IntegrityClass;
|
|
129
|
+
toJSON(): object;
|
|
130
|
+
}
|
|
131
|
+
export function defineScope(spec: {
|
|
132
|
+
key?: string;
|
|
133
|
+
name?: string;
|
|
134
|
+
type?: string;
|
|
135
|
+
scopeType?: string;
|
|
136
|
+
id?: string;
|
|
137
|
+
scopeId?: string;
|
|
138
|
+
identity?: string;
|
|
139
|
+
participants?: string;
|
|
140
|
+
collections: Array<Collection | string>;
|
|
141
|
+
description?: string;
|
|
142
|
+
readClass?: ReadClass;
|
|
143
|
+
integrityClass?: IntegrityClass;
|
|
144
|
+
}): ScopeDef;
|
|
145
|
+
|
|
146
|
+
export { streamKey } from "./streamKey";
|
|
147
|
+
|
|
148
|
+
export function defineApp(spec: AppSpec): AppDef;
|
|
149
|
+
|
|
150
|
+
export function defineNotification(spec: NotificationDef): NotificationDef;
|
|
151
|
+
export function defineNotificationsBlock(spec: { schemaVersion?: number; definitions: Record<string, NotificationDef | (() => NotificationDef)> }): NotificationsBlock;
|
|
152
|
+
|
|
153
|
+
export function emitAppArtifacts(app: AppDef, options?: EmitOptions): EmitResult;
|
|
154
|
+
export function splitCompositionForFiles(composition: object, options?: object): { composition: object; model: object; actions: object };
|
|
155
|
+
export function buildMatterhornApp(app: AppDef, options?: MatterhornAppOptions): object;
|
|
156
|
+
export function createMatterhornAppBundle(app: AppDef, options?: MatterhornAppOptions): Record<string, unknown>;
|
|
157
|
+
export function createMatterhornAppDescriptor(app: AppDef, options?: MatterhornAppOptions): object;
|
|
158
|
+
export function createMatterhornAppExports(app: AppDef, options?: MatterhornAppOptions): Record<string, unknown>;
|
|
159
|
+
export function emitMatterhornAppBundle(app: AppDef, options?: MatterhornAppOptions & { outDir?: string; fileName?: string }): { bundlePath: string; bundle: Record<string, unknown> };
|
|
160
|
+
export function operationsFromDemoDefinition(demo: object | undefined, app: object, roomId?: string): object[];
|
|
161
|
+
export function genericSummary(state: object, pluginId: string): object;
|
|
162
|
+
|
|
163
|
+
export function generateAppTypeDeclaration(schema: object, options?: { namespace?: string }): string;
|
|
164
|
+
export function namespaceNameForApp(schemaOrApp: object, fallback?: string): string;
|
|
165
|
+
export function inlinePayloadType(schema: object): string;
|
|
166
|
+
export function typeFromSchema(schema: PayloadSchemaSpec): string;
|
|
167
|
+
|
|
168
|
+
export * from "./runtime-exports";
|
package/src/json.cjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
function assertPlainObject(value, name) {
|
|
2
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${name} must be an object`);
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function cloneJson(value) {
|
|
7
|
+
if (value === undefined) return undefined;
|
|
8
|
+
return JSON.parse(JSON.stringify(value));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function assertJsonSerializable(value, name = "value", path = name) {
|
|
12
|
+
if (value === undefined) return;
|
|
13
|
+
if (value === null) return;
|
|
14
|
+
const type = typeof value;
|
|
15
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
16
|
+
if (type === "number" && !Number.isFinite(value)) throw new Error(`${path} must be finite`);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (type === "function" || type === "symbol" || type === "bigint") throw new Error(`${path} must be JSON serializable`);
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
value.forEach((item, index) => assertJsonSerializable(item, name, `${path}[${index}]`));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (type === "object") {
|
|
25
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) throw new Error(`${path} must be a plain JSON object`);
|
|
26
|
+
for (const [key, item] of Object.entries(value)) {
|
|
27
|
+
if (item !== undefined) assertJsonSerializable(item, name, `${path}.${key}`);
|
|
28
|
+
}
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`${path} must be JSON serializable`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deepFreeze(value) {
|
|
35
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
36
|
+
Object.freeze(value);
|
|
37
|
+
for (const item of Object.values(value)) deepFreeze(item);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function freezeJson(value) {
|
|
42
|
+
assertJsonSerializable(value);
|
|
43
|
+
return deepFreeze(cloneJson(value));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function jsonArray(value, name) {
|
|
47
|
+
if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function nonEmptyString(value, name) {
|
|
52
|
+
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${name} must be a non-empty string`);
|
|
53
|
+
return value.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function optionalString(value, name) {
|
|
57
|
+
if (value === undefined || value === null) return undefined;
|
|
58
|
+
return nonEmptyString(value, name);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
assertJsonSerializable,
|
|
63
|
+
assertPlainObject,
|
|
64
|
+
cloneJson,
|
|
65
|
+
deepFreeze,
|
|
66
|
+
freezeJson,
|
|
67
|
+
jsonArray,
|
|
68
|
+
nonEmptyString,
|
|
69
|
+
optionalString
|
|
70
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const SCHEMA_ROOT = path.resolve(__dirname, '../../schemas');
|
|
5
|
+
|
|
6
|
+
function clone(value) {
|
|
7
|
+
return value === undefined ? undefined : JSON.parse(JSON.stringify(value));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function readSchema(name) {
|
|
11
|
+
const file = path.join(SCHEMA_ROOT, name);
|
|
12
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function valueType(value) {
|
|
16
|
+
if (value === null) return 'null';
|
|
17
|
+
if (Array.isArray(value)) return 'array';
|
|
18
|
+
if (Number.isInteger(value)) return 'integer';
|
|
19
|
+
return typeof value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isType(value, expected) {
|
|
23
|
+
if (expected === 'integer') return Number.isInteger(value);
|
|
24
|
+
if (expected === 'number') return typeof value === 'number' && Number.isFinite(value);
|
|
25
|
+
if (expected === 'object') return value && typeof value === 'object' && !Array.isArray(value);
|
|
26
|
+
if (expected === 'array') return Array.isArray(value);
|
|
27
|
+
if (expected === 'string') return typeof value === 'string';
|
|
28
|
+
if (expected === 'boolean') return typeof value === 'boolean';
|
|
29
|
+
if (expected === 'null') return value === null;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function refTarget(schema, ref) {
|
|
34
|
+
if (!ref.startsWith('#/')) throw new Error(`Only local JSON Schema refs are supported: ${ref}`);
|
|
35
|
+
return ref.slice(2).split('/').reduce((node, part) => node?.[part.replace(/~1/g, '/').replace(/~0/g, '~')], schema);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateNode(rootSchema, schema, value, pathName, errors) {
|
|
39
|
+
if (!schema || typeof schema !== 'object') return;
|
|
40
|
+
if (schema.$ref) return validateNode(rootSchema, refTarget(rootSchema, schema.$ref), value, pathName, errors);
|
|
41
|
+
|
|
42
|
+
if (schema.const !== undefined && JSON.stringify(value) !== JSON.stringify(schema.const)) {
|
|
43
|
+
errors.push(`${pathName} must equal ${JSON.stringify(schema.const)}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (schema.enum && !schema.enum.some((item) => JSON.stringify(item) === JSON.stringify(value))) errors.push(`${pathName} must be one of ${schema.enum.join(', ')}`);
|
|
47
|
+
if (schema.oneOf) {
|
|
48
|
+
const matches = schema.oneOf.filter((candidate) => {
|
|
49
|
+
const nested = [];
|
|
50
|
+
validateNode(rootSchema, candidate, value, pathName, nested);
|
|
51
|
+
return nested.length === 0;
|
|
52
|
+
});
|
|
53
|
+
if (matches.length !== 1) errors.push(`${pathName} must match exactly one schema variant`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (schema.anyOf) {
|
|
57
|
+
const ok = schema.anyOf.some((candidate) => {
|
|
58
|
+
const nested = [];
|
|
59
|
+
validateNode(rootSchema, candidate, value, pathName, nested);
|
|
60
|
+
return nested.length === 0;
|
|
61
|
+
});
|
|
62
|
+
if (!ok) errors.push(`${pathName} must match one schema variant`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (schema.allOf) {
|
|
66
|
+
for (const candidate of schema.allOf) validateNode(rootSchema, candidate, value, pathName, errors);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (schema.type !== undefined) {
|
|
70
|
+
const types = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
71
|
+
if (!types.some((type) => isType(value, type))) {
|
|
72
|
+
errors.push(`${pathName} must be ${types.join(' or ')}, got ${valueType(value)}`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (typeof value === 'string') {
|
|
78
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) errors.push(`${pathName} must have length >= ${schema.minLength}`);
|
|
79
|
+
if (schema.pattern && !(new RegExp(schema.pattern).test(value))) errors.push(`${pathName} must match ${schema.pattern}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof value === 'number') {
|
|
83
|
+
if (schema.minimum !== undefined && value < schema.minimum) errors.push(`${pathName} must be >= ${schema.minimum}`);
|
|
84
|
+
if (schema.maximum !== undefined && value > schema.maximum) errors.push(`${pathName} must be <= ${schema.maximum}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Array.isArray(value)) {
|
|
88
|
+
if (schema.minItems !== undefined && value.length < schema.minItems) errors.push(`${pathName} must have at least ${schema.minItems} items`);
|
|
89
|
+
if (schema.items) value.forEach((item, index) => validateNode(rootSchema, schema.items, item, `${pathName}[${index}]`, errors));
|
|
90
|
+
if (schema.uniqueItems) {
|
|
91
|
+
const seen = new Set(value.map((item) => JSON.stringify(item)));
|
|
92
|
+
if (seen.size !== value.length) errors.push(`${pathName} must contain unique items`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
97
|
+
for (const key of schema.required || []) {
|
|
98
|
+
if (value[key] === undefined) errors.push(`${pathName}.${key} is required`);
|
|
99
|
+
}
|
|
100
|
+
const properties = schema.properties || {};
|
|
101
|
+
for (const [key, propertyValue] of Object.entries(value)) {
|
|
102
|
+
if (properties[key]) validateNode(rootSchema, properties[key], propertyValue, `${pathName}.${key}`, errors);
|
|
103
|
+
else if (schema.additionalProperties && typeof schema.additionalProperties === 'object') validateNode(rootSchema, schema.additionalProperties, propertyValue, `${pathName}.${key}`, errors);
|
|
104
|
+
else if (schema.additionalProperties === false) errors.push(`${pathName}.${key} is not allowed`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateJsonSchema(schema, value, options = {}) {
|
|
110
|
+
const errors = [];
|
|
111
|
+
validateNode(schema, schema, value, options.name || '$', errors);
|
|
112
|
+
return errors.length ? { ok: false, errors } : { ok: true, value: clone(value) };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function assertValidJsonSchema(name, value, options = {}) {
|
|
116
|
+
const schema = typeof name === 'string' ? readSchema(name) : name;
|
|
117
|
+
const result = validateJsonSchema(schema, value, { name: options.name || '$' });
|
|
118
|
+
if (!result.ok) {
|
|
119
|
+
const error = new Error(`${options.label || 'JSON schema validation'} failed: ${result.errors.slice(0, 6).join('; ')}`);
|
|
120
|
+
error.errors = result.errors;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
SCHEMA_ROOT,
|
|
128
|
+
assertValidJsonSchema,
|
|
129
|
+
readSchema,
|
|
130
|
+
validateJsonSchema
|
|
131
|
+
};
|