@genetik/content 0.0.0
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/index.cjs +268 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +128 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +128 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +265 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eugene Michasiw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# @genetik/content
|
|
2
|
+
|
|
3
|
+
Types, validation, and normalization for the Genetik content model: flat node map, entry id, validation against a schema, and inline → flat + id generation.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @genetik/content
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
- **Types**: `GenetikContent`, `ContentNode`, `SlotValue`, `GenetikContentInput`, `InlineNode` — canonical and input shapes.
|
|
14
|
+
- **Validation**: `validateContent(schema, content)` — validates content against a schema (block types, config, slots, link integrity).
|
|
15
|
+
- **Normalization**: `normalizeContent(schema, input, options?)` — flattens inline nodes (when schema allows) and assigns ids.
|
|
16
|
+
|
|
17
|
+
See the [docs](/docs/packages/content) for full API and examples.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
let _genetik_schema = require("@genetik/schema");
|
|
3
|
+
let nanoid = require("nanoid");
|
|
4
|
+
|
|
5
|
+
//#region src/normalize.ts
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes content that may contain inline nodes in slots (when schema allows)
|
|
8
|
+
* to canonical flat form. Inline nodes are flattened, assigned ids, and slot
|
|
9
|
+
* values become id references. Id-only slots are left unchanged.
|
|
10
|
+
*/
|
|
11
|
+
function normalizeContent(schema, input, options) {
|
|
12
|
+
const generateId = options?.generateId ?? nanoid.nanoid;
|
|
13
|
+
const resultNodes = {};
|
|
14
|
+
for (const [nodeId, inputNode] of Object.entries(input.nodes)) {
|
|
15
|
+
const { node, extraNodes } = normalizeNode(schema, inputNode, nodeId, generateId);
|
|
16
|
+
resultNodes[nodeId] = node;
|
|
17
|
+
Object.assign(resultNodes, extraNodes);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
entryId: input.entryId,
|
|
21
|
+
nodes: resultNodes
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function normalizeNode(schema, input, nodeId, generateId) {
|
|
25
|
+
const block = input.block;
|
|
26
|
+
const blockType = (0, _genetik_schema.getBlockType)(schema, block);
|
|
27
|
+
const config = input.config ?? {};
|
|
28
|
+
if (!blockType) {
|
|
29
|
+
const node = {
|
|
30
|
+
id: nodeId,
|
|
31
|
+
block,
|
|
32
|
+
config
|
|
33
|
+
};
|
|
34
|
+
for (const k of Object.keys(input)) {
|
|
35
|
+
if (k === "id" || k === "block" || k === "config") continue;
|
|
36
|
+
const v = input[k];
|
|
37
|
+
if (typeof v === "string" || Array.isArray(v) && v.every((x) => typeof x === "string")) node[k] = v;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
node,
|
|
41
|
+
extraNodes: {}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const node = {
|
|
45
|
+
id: nodeId,
|
|
46
|
+
block,
|
|
47
|
+
config
|
|
48
|
+
};
|
|
49
|
+
let extraNodes = {};
|
|
50
|
+
for (const slotDef of blockType.slots) {
|
|
51
|
+
const value = input[slotDef.name];
|
|
52
|
+
if (value === void 0 || value === null) continue;
|
|
53
|
+
if (slotDef.referenceMode === "id") {
|
|
54
|
+
node[slotDef.name] = value;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (slotDef.multiple) {
|
|
58
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
59
|
+
const ids = [];
|
|
60
|
+
for (const item of arr) if (typeof item === "string") ids.push(item);
|
|
61
|
+
else if (isInlineNode(item)) {
|
|
62
|
+
const childId = generateId();
|
|
63
|
+
const { node: childNode, extraNodes: childExtras } = normalizeNode(schema, item, childId, generateId);
|
|
64
|
+
ids.push(childId);
|
|
65
|
+
extraNodes = {
|
|
66
|
+
...extraNodes,
|
|
67
|
+
...childExtras,
|
|
68
|
+
[childId]: childNode
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
node[slotDef.name] = ids;
|
|
72
|
+
} else if (typeof value === "string") node[slotDef.name] = value;
|
|
73
|
+
else if (isInlineNode(value)) {
|
|
74
|
+
const childId = generateId();
|
|
75
|
+
const { node: childNode, extraNodes: childExtras } = normalizeNode(schema, value, childId, generateId);
|
|
76
|
+
node[slotDef.name] = childId;
|
|
77
|
+
extraNodes = {
|
|
78
|
+
...extraNodes,
|
|
79
|
+
...childExtras,
|
|
80
|
+
[childId]: childNode
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
node,
|
|
86
|
+
extraNodes
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function isInlineNode(value) {
|
|
90
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && "block" in value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/parse.ts
|
|
95
|
+
/**
|
|
96
|
+
* Parses a JSON string into content and checks the minimal shape (entryId and nodes).
|
|
97
|
+
* Does not validate against a schema or normalize; use validateContent and
|
|
98
|
+
* normalizeContent for that.
|
|
99
|
+
*
|
|
100
|
+
* @param raw - JSON string (e.g. from user input or API)
|
|
101
|
+
* @returns ParseContentResult: either { ok: true, content } or { ok: false, error }
|
|
102
|
+
*/
|
|
103
|
+
function parseContentJson(raw) {
|
|
104
|
+
let parsed;
|
|
105
|
+
try {
|
|
106
|
+
parsed = JSON.parse(raw);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: e instanceof Error ? e.message : "Invalid JSON"
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (parsed === null || typeof parsed !== "object") return {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: "Content must be an object"
|
|
116
|
+
};
|
|
117
|
+
const obj = parsed;
|
|
118
|
+
if (typeof obj.entryId !== "string") return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: "Content must have entryId (string)"
|
|
121
|
+
};
|
|
122
|
+
if (obj.nodes === null || typeof obj.nodes !== "object") return {
|
|
123
|
+
ok: false,
|
|
124
|
+
error: "Content must have nodes (object)"
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
content: obj
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/validate.ts
|
|
134
|
+
/**
|
|
135
|
+
* Validates content against a schema. Checks:
|
|
136
|
+
* - entryId is present and exists in nodes
|
|
137
|
+
* - Every node has id, block, config; block type exists in schema
|
|
138
|
+
* - Each node's config is valid against the block's JSON Schema
|
|
139
|
+
* - Slot values are id or id[] per slot definition; no extra slot keys
|
|
140
|
+
* - All referenced ids exist in the node map (link integrity)
|
|
141
|
+
*/
|
|
142
|
+
function validateContent(schema, content) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
if (content === null || typeof content !== "object" || Array.isArray(content)) return {
|
|
145
|
+
valid: false,
|
|
146
|
+
errors: [{
|
|
147
|
+
path: "",
|
|
148
|
+
message: "Content must be an object"
|
|
149
|
+
}]
|
|
150
|
+
};
|
|
151
|
+
const c = content;
|
|
152
|
+
const entryId = c.entryId;
|
|
153
|
+
const nodes = c.nodes;
|
|
154
|
+
if (typeof entryId !== "string" || entryId === "") errors.push({
|
|
155
|
+
path: "entryId",
|
|
156
|
+
message: "entryId must be a non-empty string"
|
|
157
|
+
});
|
|
158
|
+
if (nodes === null || typeof nodes !== "object" || Array.isArray(nodes)) errors.push({
|
|
159
|
+
path: "nodes",
|
|
160
|
+
message: "nodes must be an object (map of id to node)"
|
|
161
|
+
});
|
|
162
|
+
const nodeMap = nodes;
|
|
163
|
+
const allIds = new Set(nodeMap ? Object.keys(nodeMap) : []);
|
|
164
|
+
if (nodeMap) for (const [key, val] of Object.entries(nodeMap)) {
|
|
165
|
+
const nodeError = validateNode(schema, key, val, "nodes");
|
|
166
|
+
if (nodeError) errors.push(nodeError);
|
|
167
|
+
}
|
|
168
|
+
if (typeof entryId === "string" && entryId !== "" && nodeMap && !(entryId in nodeMap)) errors.push({
|
|
169
|
+
path: "entryId",
|
|
170
|
+
message: `entryId "${entryId}" is not in nodes`
|
|
171
|
+
});
|
|
172
|
+
if (nodeMap) for (const [nodeKey, val] of Object.entries(nodeMap)) {
|
|
173
|
+
const refErrors = collectSlotReferenceErrors(nodeMap, val, `nodes.${nodeKey}`, allIds);
|
|
174
|
+
errors.push(...refErrors);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
valid: errors.length === 0,
|
|
178
|
+
errors
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function validateNode(schema, mapKey, value, path) {
|
|
182
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return {
|
|
183
|
+
path: `${path}.${mapKey}`,
|
|
184
|
+
message: "Node must be an object"
|
|
185
|
+
};
|
|
186
|
+
const node = value;
|
|
187
|
+
const id = node.id;
|
|
188
|
+
const block = node.block;
|
|
189
|
+
const config = node.config;
|
|
190
|
+
if (typeof id !== "string" || id === "") return {
|
|
191
|
+
path: `${path}.${mapKey}.id`,
|
|
192
|
+
message: "Node id must be a non-empty string"
|
|
193
|
+
};
|
|
194
|
+
if (id !== mapKey) return {
|
|
195
|
+
path: `${path}.${mapKey}.id`,
|
|
196
|
+
message: `Node id "${id}" must match map key "${mapKey}"`
|
|
197
|
+
};
|
|
198
|
+
if (typeof block !== "string" || block === "") return {
|
|
199
|
+
path: `${path}.${mapKey}.block`,
|
|
200
|
+
message: "Node block must be a non-empty string"
|
|
201
|
+
};
|
|
202
|
+
const blockType = block ? (0, _genetik_schema.getBlockType)(schema, block) : void 0;
|
|
203
|
+
if (!blockType) return {
|
|
204
|
+
path: `${path}.${mapKey}.block`,
|
|
205
|
+
message: `Unknown block type: ${block}`
|
|
206
|
+
};
|
|
207
|
+
if (config !== void 0 && (config === null || typeof config !== "object" || Array.isArray(config))) return {
|
|
208
|
+
path: `${path}.${mapKey}.config`,
|
|
209
|
+
message: "Node config must be an object"
|
|
210
|
+
};
|
|
211
|
+
const configResult = (0, _genetik_schema.validateConfigAgainstDefinition)(blockType, config ?? {});
|
|
212
|
+
if (!configResult.valid && configResult.errors?.length) {
|
|
213
|
+
const first = configResult.errors[0];
|
|
214
|
+
return {
|
|
215
|
+
path: `${path}.${mapKey}.config`,
|
|
216
|
+
message: first.message ?? "Invalid config"
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const slotNames = new Set(blockType.slots.map((s) => s.name));
|
|
220
|
+
for (const key of Object.keys(node)) {
|
|
221
|
+
if (key === "id" || key === "block" || key === "config") continue;
|
|
222
|
+
if (!slotNames.has(key)) return {
|
|
223
|
+
path: `${path}.${mapKey}`,
|
|
224
|
+
message: `Unknown slot "${key}" for block type "${block}"`
|
|
225
|
+
};
|
|
226
|
+
const slotDef = blockType.slots.find((s) => s.name === key);
|
|
227
|
+
if (!slotDef) continue;
|
|
228
|
+
const slotValue = node[key];
|
|
229
|
+
if (slotValue === void 0 || slotValue === null) continue;
|
|
230
|
+
if (slotDef.multiple) {
|
|
231
|
+
if (!Array.isArray(slotValue)) return {
|
|
232
|
+
path: `${path}.${mapKey}.${key}`,
|
|
233
|
+
message: `Slot "${key}" must be an array of ids (multiple: true)`
|
|
234
|
+
};
|
|
235
|
+
if (slotValue.some((v) => typeof v !== "string")) return {
|
|
236
|
+
path: `${path}.${mapKey}.${key}`,
|
|
237
|
+
message: `Slot "${key}" must contain only string ids`
|
|
238
|
+
};
|
|
239
|
+
} else if (typeof slotValue !== "string") return {
|
|
240
|
+
path: `${path}.${mapKey}.${key}`,
|
|
241
|
+
message: `Slot "${key}" must be a single id string (multiple: false)`
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function collectSlotReferenceErrors(nodeMap, value, path, allIds) {
|
|
247
|
+
const errors = [];
|
|
248
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return errors;
|
|
249
|
+
const node = value;
|
|
250
|
+
for (const [key, slotValue] of Object.entries(node)) {
|
|
251
|
+
if (key === "id" || key === "block" || key === "config") continue;
|
|
252
|
+
const ids = Array.isArray(slotValue) ? slotValue : slotValue === void 0 || slotValue === null ? [] : [slotValue];
|
|
253
|
+
for (const id of ids) {
|
|
254
|
+
if (typeof id !== "string") continue;
|
|
255
|
+
if (!allIds.has(id)) errors.push({
|
|
256
|
+
path: `${path}.${key}`,
|
|
257
|
+
message: `Referenced id "${id}" is not in nodes (dangling reference)`
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return errors;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//#endregion
|
|
265
|
+
exports.normalizeContent = normalizeContent;
|
|
266
|
+
exports.parseContentJson = parseContentJson;
|
|
267
|
+
exports.validateContent = validateContent;
|
|
268
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../src/normalize.ts","../src/parse.ts","../src/validate.ts"],"sourcesContent":["import type { GenetikSchema } from \"@genetik/schema\";\nimport { getBlockType } from \"@genetik/schema\";\nimport { nanoid } from \"nanoid\";\nimport type { ContentNode, GenetikContent, GenetikContentInput } from \"./types.js\";\n\nexport interface NormalizeOptions {\n /** Id generator for new nodes from inline. Defaults to nanoid. */\n generateId?: () => string;\n}\n\n/**\n * Normalizes content that may contain inline nodes in slots (when schema allows)\n * to canonical flat form. Inline nodes are flattened, assigned ids, and slot\n * values become id references. Id-only slots are left unchanged.\n */\nexport function normalizeContent(\n schema: GenetikSchema,\n input: GenetikContentInput,\n options?: NormalizeOptions\n): GenetikContent {\n const generateId = options?.generateId ?? nanoid;\n const resultNodes: Record<string, ContentNode> = {};\n\n for (const [nodeId, inputNode] of Object.entries(input.nodes)) {\n const { node, extraNodes } = normalizeNode(schema, inputNode as Record<string, unknown>, nodeId, generateId);\n resultNodes[nodeId] = node;\n Object.assign(resultNodes, extraNodes);\n }\n\n return {\n entryId: input.entryId,\n nodes: resultNodes,\n };\n}\n\nfunction normalizeNode(\n schema: GenetikSchema,\n input: Record<string, unknown>,\n nodeId: string,\n generateId: () => string\n): { node: ContentNode; extraNodes: Record<string, ContentNode> } {\n const block = input.block as string;\n const blockType = getBlockType(schema, block);\n const config = (input.config as Record<string, unknown>) ?? {};\n\n if (!blockType) {\n const node: ContentNode = { id: nodeId, block, config };\n for (const k of Object.keys(input)) {\n if (k === \"id\" || k === \"block\" || k === \"config\") continue;\n const v = input[k];\n if (typeof v === \"string\" || (Array.isArray(v) && v.every((x) => typeof x === \"string\"))) node[k] = v;\n }\n return { node, extraNodes: {} };\n }\n\n const node: ContentNode = { id: nodeId, block, config };\n let extraNodes: Record<string, ContentNode> = {};\n\n for (const slotDef of blockType.slots) {\n const value = input[slotDef.name];\n if (value === undefined || value === null) continue;\n\n if (slotDef.referenceMode === \"id\") {\n node[slotDef.name] = value;\n continue;\n }\n\n if (slotDef.multiple) {\n const arr = Array.isArray(value) ? value : [value];\n const ids: string[] = [];\n for (const item of arr) {\n if (typeof item === \"string\") {\n ids.push(item);\n } else if (isInlineNode(item)) {\n const childId = generateId();\n const { node: childNode, extraNodes: childExtras } = normalizeNode(\n schema,\n item as Record<string, unknown>,\n childId,\n generateId\n );\n ids.push(childId);\n extraNodes = { ...extraNodes, ...childExtras, [childId]: childNode };\n }\n }\n node[slotDef.name] = ids;\n } else {\n if (typeof value === \"string\") {\n node[slotDef.name] = value;\n } else if (isInlineNode(value)) {\n const childId = generateId();\n const { node: childNode, extraNodes: childExtras } = normalizeNode(\n schema,\n value as Record<string, unknown>,\n childId,\n generateId\n );\n node[slotDef.name] = childId;\n extraNodes = { ...extraNodes, ...childExtras, [childId]: childNode };\n }\n }\n }\n\n return { node, extraNodes };\n}\n\nfunction isInlineNode(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value) && \"block\" in value;\n}\n","import type { GenetikContent } from \"./types.js\";\n\n/**\n * Result of parsing a content JSON string. Use this when parsing user or API input\n * so you can show a clear error instead of throwing.\n */\nexport type ParseContentResult =\n | { ok: true; content: GenetikContent }\n | { ok: false; error: string };\n\n/**\n * Parses a JSON string into content and checks the minimal shape (entryId and nodes).\n * Does not validate against a schema or normalize; use validateContent and\n * normalizeContent for that.\n *\n * @param raw - JSON string (e.g. from user input or API)\n * @returns ParseContentResult: either { ok: true, content } or { ok: false, error }\n */\nexport function parseContentJson(raw: string): ParseContentResult {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n return { ok: false, error: e instanceof Error ? e.message : \"Invalid JSON\" };\n }\n\n if (parsed === null || typeof parsed !== \"object\") {\n return { ok: false, error: \"Content must be an object\" };\n }\n\n const obj = parsed as Record<string, unknown>;\n if (typeof obj.entryId !== \"string\") {\n return { ok: false, error: \"Content must have entryId (string)\" };\n }\n if (obj.nodes === null || typeof obj.nodes !== \"object\") {\n return { ok: false, error: \"Content must have nodes (object)\" };\n }\n\n return { ok: true, content: obj as unknown as GenetikContent };\n}\n","import type { GenetikSchema } from \"@genetik/schema\";\nimport { getBlockType } from \"@genetik/schema\";\nimport { validateConfigAgainstDefinition } from \"@genetik/schema\";\nimport type { ContentNode } from \"./types.js\";\n\nexport interface ContentValidationError {\n /** Path to the problem (e.g. \"nodes.a\", \"nodes.a.config\"). */\n path: string;\n /** Human-readable message. */\n message: string;\n}\n\nexport interface ContentValidationResult {\n valid: boolean;\n errors: ContentValidationError[];\n}\n\n/**\n * Validates content against a schema. Checks:\n * - entryId is present and exists in nodes\n * - Every node has id, block, config; block type exists in schema\n * - Each node's config is valid against the block's JSON Schema\n * - Slot values are id or id[] per slot definition; no extra slot keys\n * - All referenced ids exist in the node map (link integrity)\n */\nexport function validateContent(\n schema: GenetikSchema,\n content: unknown\n): ContentValidationResult {\n const errors: ContentValidationError[] = [];\n\n if (content === null || typeof content !== \"object\" || Array.isArray(content)) {\n return {\n valid: false,\n errors: [{ path: \"\", message: \"Content must be an object\" }],\n };\n }\n\n const c = content as Record<string, unknown>;\n const entryId = c.entryId;\n const nodes = c.nodes;\n\n if (typeof entryId !== \"string\" || entryId === \"\") {\n errors.push({ path: \"entryId\", message: \"entryId must be a non-empty string\" });\n }\n\n if (nodes === null || typeof nodes !== \"object\" || Array.isArray(nodes)) {\n errors.push({ path: \"nodes\", message: \"nodes must be an object (map of id to node)\" });\n }\n\n const nodeMap = nodes as Record<string, unknown> | undefined;\n const allIds = new Set<string>(nodeMap ? Object.keys(nodeMap) : []);\n\n if (nodeMap) {\n for (const [key, val] of Object.entries(nodeMap)) {\n const nodeError = validateNode(schema, key, val, \"nodes\");\n if (nodeError) errors.push(nodeError);\n }\n }\n\n if (typeof entryId === \"string\" && entryId !== \"\" && nodeMap && !(entryId in nodeMap)) {\n errors.push({ path: \"entryId\", message: `entryId \"${entryId}\" is not in nodes` });\n }\n\n if (nodeMap) {\n for (const [nodeKey, val] of Object.entries(nodeMap)) {\n const refErrors = collectSlotReferenceErrors(nodeMap, val, `nodes.${nodeKey}`, allIds);\n errors.push(...refErrors);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\nfunction validateNode(\n schema: GenetikSchema,\n mapKey: string,\n value: unknown,\n path: string\n): ContentValidationError | null {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return { path: `${path}.${mapKey}`, message: \"Node must be an object\" };\n }\n\n const node = value as Record<string, unknown>;\n const id = node.id;\n const block = node.block;\n const config = node.config;\n\n if (typeof id !== \"string\" || id === \"\") {\n return { path: `${path}.${mapKey}.id`, message: \"Node id must be a non-empty string\" };\n }\n if (id !== mapKey) {\n return { path: `${path}.${mapKey}.id`, message: `Node id \"${id}\" must match map key \"${mapKey}\"` };\n }\n if (typeof block !== \"string\" || block === \"\") {\n return { path: `${path}.${mapKey}.block`, message: \"Node block must be a non-empty string\" };\n }\n\n const blockType = block ? getBlockType(schema, block as string) : undefined;\n if (!blockType) {\n return {\n path: `${path}.${mapKey}.block`,\n message: `Unknown block type: ${block}`,\n };\n }\n\n if (config !== undefined && (config === null || typeof config !== \"object\" || Array.isArray(config))) {\n return { path: `${path}.${mapKey}.config`, message: \"Node config must be an object\" };\n }\n\n const configResult = validateConfigAgainstDefinition(blockType, config ?? {});\n if (!configResult.valid && configResult.errors?.length) {\n const first = configResult.errors[0]!;\n return {\n path: `${path}.${mapKey}.config`,\n message: first.message ?? \"Invalid config\",\n };\n }\n\n const slotNames = new Set(blockType.slots.map((s) => s.name));\n for (const key of Object.keys(node)) {\n if (key === \"id\" || key === \"block\" || key === \"config\") continue;\n if (!slotNames.has(key)) {\n return {\n path: `${path}.${mapKey}`,\n message: `Unknown slot \"${key}\" for block type \"${block}\"`,\n };\n }\n const slotDef = blockType.slots.find((s) => s.name === key);\n if (!slotDef) continue;\n const slotValue = node[key];\n if (slotValue === undefined || slotValue === null) continue;\n if (slotDef.multiple) {\n if (!Array.isArray(slotValue)) {\n return {\n path: `${path}.${mapKey}.${key}`,\n message: `Slot \"${key}\" must be an array of ids (multiple: true)`,\n };\n }\n if (slotValue.some((v) => typeof v !== \"string\")) {\n return {\n path: `${path}.${mapKey}.${key}`,\n message: `Slot \"${key}\" must contain only string ids`,\n };\n }\n } else {\n if (typeof slotValue !== \"string\") {\n return {\n path: `${path}.${mapKey}.${key}`,\n message: `Slot \"${key}\" must be a single id string (multiple: false)`,\n };\n }\n }\n }\n\n return null;\n}\n\nfunction collectSlotReferenceErrors(\n nodeMap: Record<string, unknown>,\n value: unknown,\n path: string,\n allIds: Set<string>\n): ContentValidationError[] {\n const errors: ContentValidationError[] = [];\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return errors;\n\n const node = value as ContentNode;\n for (const [key, slotValue] of Object.entries(node)) {\n if (key === \"id\" || key === \"block\" || key === \"config\") continue;\n const ids = Array.isArray(slotValue) ? slotValue : slotValue === undefined || slotValue === null ? [] : [slotValue];\n for (const id of ids) {\n if (typeof id !== \"string\") continue;\n if (!allIds.has(id)) {\n errors.push({\n path: `${path}.${key}`,\n message: `Referenced id \"${id}\" is not in nodes (dangling reference)`,\n });\n }\n }\n }\n return errors;\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { GenetikSchema } from "@genetik/schema";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Canonical content model: flat node map and entry point.
|
|
6
|
+
* Slot values are id references (string or array of ids).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A single node in the content tree. In canonical form, slot values
|
|
10
|
+
* are string (id) or string[] (ordered ids). Keys besides id, block,
|
|
11
|
+
* and config are slot names defined by the block type.
|
|
12
|
+
*/
|
|
13
|
+
interface ContentNode {
|
|
14
|
+
/** Node id; must match the key in the nodes map. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Block type name (must exist in schema). */
|
|
17
|
+
block: string;
|
|
18
|
+
/** Block-specific configuration (validated against block's JSON Schema). */
|
|
19
|
+
config: Record<string, unknown>;
|
|
20
|
+
/** Slot name → id or array of ids. Other keys are not allowed in canonical form. */
|
|
21
|
+
[slotName: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Slot value in canonical form: either a single node id or an ordered list of ids.
|
|
25
|
+
*/
|
|
26
|
+
type SlotValue = string | string[];
|
|
27
|
+
/**
|
|
28
|
+
* Content definition in canonical form: one entry id and a flat map of nodes.
|
|
29
|
+
* No nesting; slots reference children by id.
|
|
30
|
+
*/
|
|
31
|
+
interface GenetikContent {
|
|
32
|
+
/** Root node id (must exist in nodes). */
|
|
33
|
+
entryId: string;
|
|
34
|
+
/** Flat map of node id → node. */
|
|
35
|
+
nodes: Record<string, ContentNode>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Input form for a slot value when the schema allows inline (referenceMode "inline" or "both").
|
|
39
|
+
* Can be an id, array of ids, a single inline node object, or array of id or inline node.
|
|
40
|
+
*/
|
|
41
|
+
type InlineSlotValue = string | string[] | InlineNode | (string | InlineNode)[];
|
|
42
|
+
/**
|
|
43
|
+
* Inline node: can appear in a slot when schema allows. Has block, config, optional id,
|
|
44
|
+
* and slot keys whose values may be id(s) or nested inline. Normalization flattens these
|
|
45
|
+
* and assigns ids.
|
|
46
|
+
*/
|
|
47
|
+
interface InlineNode {
|
|
48
|
+
/** Optional; if missing, normalization will generate one. */
|
|
49
|
+
id?: string;
|
|
50
|
+
block: string;
|
|
51
|
+
config: Record<string, unknown>;
|
|
52
|
+
[slotName: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Content in input form: same shape as canonical but slot values may be inline (when schema allows).
|
|
56
|
+
* Used as input to normalizeContent().
|
|
57
|
+
*/
|
|
58
|
+
interface GenetikContentInput {
|
|
59
|
+
entryId: string;
|
|
60
|
+
nodes: Record<string, ContentNodeInput>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Node in input form: id must match map key; slot values may be id, id[], or inline (per schema).
|
|
64
|
+
*/
|
|
65
|
+
interface ContentNodeInput {
|
|
66
|
+
id: string;
|
|
67
|
+
block: string;
|
|
68
|
+
config: Record<string, unknown>;
|
|
69
|
+
[slotName: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/normalize.d.ts
|
|
73
|
+
interface NormalizeOptions {
|
|
74
|
+
/** Id generator for new nodes from inline. Defaults to nanoid. */
|
|
75
|
+
generateId?: () => string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Normalizes content that may contain inline nodes in slots (when schema allows)
|
|
79
|
+
* to canonical flat form. Inline nodes are flattened, assigned ids, and slot
|
|
80
|
+
* values become id references. Id-only slots are left unchanged.
|
|
81
|
+
*/
|
|
82
|
+
declare function normalizeContent(schema: GenetikSchema, input: GenetikContentInput, options?: NormalizeOptions): GenetikContent;
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/parse.d.ts
|
|
85
|
+
/**
|
|
86
|
+
* Result of parsing a content JSON string. Use this when parsing user or API input
|
|
87
|
+
* so you can show a clear error instead of throwing.
|
|
88
|
+
*/
|
|
89
|
+
type ParseContentResult = {
|
|
90
|
+
ok: true;
|
|
91
|
+
content: GenetikContent;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
error: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Parses a JSON string into content and checks the minimal shape (entryId and nodes).
|
|
98
|
+
* Does not validate against a schema or normalize; use validateContent and
|
|
99
|
+
* normalizeContent for that.
|
|
100
|
+
*
|
|
101
|
+
* @param raw - JSON string (e.g. from user input or API)
|
|
102
|
+
* @returns ParseContentResult: either { ok: true, content } or { ok: false, error }
|
|
103
|
+
*/
|
|
104
|
+
declare function parseContentJson(raw: string): ParseContentResult;
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/validate.d.ts
|
|
107
|
+
interface ContentValidationError {
|
|
108
|
+
/** Path to the problem (e.g. "nodes.a", "nodes.a.config"). */
|
|
109
|
+
path: string;
|
|
110
|
+
/** Human-readable message. */
|
|
111
|
+
message: string;
|
|
112
|
+
}
|
|
113
|
+
interface ContentValidationResult {
|
|
114
|
+
valid: boolean;
|
|
115
|
+
errors: ContentValidationError[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validates content against a schema. Checks:
|
|
119
|
+
* - entryId is present and exists in nodes
|
|
120
|
+
* - Every node has id, block, config; block type exists in schema
|
|
121
|
+
* - Each node's config is valid against the block's JSON Schema
|
|
122
|
+
* - Slot values are id or id[] per slot definition; no extra slot keys
|
|
123
|
+
* - All referenced ids exist in the node map (link integrity)
|
|
124
|
+
*/
|
|
125
|
+
declare function validateContent(schema: GenetikSchema, content: unknown): ContentValidationResult;
|
|
126
|
+
//#endregion
|
|
127
|
+
export { type ContentNode, type ContentNodeInput, type ContentValidationError, type ContentValidationResult, type GenetikContent, type GenetikContentInput, type InlineNode, type InlineSlotValue, type NormalizeOptions, type ParseContentResult, type SlotValue, normalizeContent, parseContentJson, validateContent };
|
|
128
|
+
//# sourceMappingURL=index.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/types.ts","../src/normalize.ts","../src/parse.ts","../src/validate.ts"],"mappings":";;;;;;AAUA;;;;;;UAAiB,WAAA;EAMP;EAJR,EAAA;EAMiB;EAJjB,KAAA;EAUU;EARV,MAAA,EAAQ,MAAA;;GAEP,QAAA;AAAA;AAYH;;;AAAA,KANY,SAAA;;;;;UAMK,cAAA;EAIkB;EAFjC,OAAA;EASyB;EAPzB,KAAA,EAAO,MAAA,SAAe,WAAA;AAAA;;AAcxB;;;KAPY,eAAA,uBAAsC,UAAA,aAAuB,UAAA;;;;;;UAOxD,UAAA;EAKE;EAHjB,EAAA;EACA,KAAA;EACA,MAAA,EAAQ,MAAA;EAAA,CACP,QAAA;AAAA;;;;;UAOc,mBAAA;EACf,OAAA;EACA,KAAA,EAAO,MAAA,SAAe,gBAAA;AAAA;;;;UAMP,gBAAA;EACf,EAAA;EACA,KAAA;EACA,MAAA,EAAQ,MAAA;EAAA,CACP,QAAA;AAAA;;;UCnEc,gBAAA;EDKA;ECHf,UAAA;AAAA;;;;;;iBAQc,gBAAA,CACd,MAAA,EAAQ,aAAA,EACR,KAAA,EAAO,mBAAA,EACP,OAAA,GAAU,gBAAA,GACT,cAAA;;;;;ADTH;;KEJY,kBAAA;EACN,EAAA;EAAU,OAAA,EAAS,cAAA;AAAA;EACnB,EAAA;EAAW,KAAA;AAAA;;;AFgBjB;;;;;AAMA;iBEZgB,gBAAA,CAAiB,GAAA,WAAc,kBAAA;;;UCb9B,sBAAA;;EAEf,IAAA;EHG0B;EGD1B,OAAA;AAAA;AAAA,UAGe,uBAAA;EACf,KAAA;EACA,MAAA,EAAQ,sBAAA;AAAA;;;;AHUV;;;;;iBGCgB,eAAA,CACd,MAAA,EAAQ,aAAA,EACR,OAAA,YACC,uBAAA"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { GenetikSchema } from "@genetik/schema";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Canonical content model: flat node map and entry point.
|
|
6
|
+
* Slot values are id references (string or array of ids).
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* A single node in the content tree. In canonical form, slot values
|
|
10
|
+
* are string (id) or string[] (ordered ids). Keys besides id, block,
|
|
11
|
+
* and config are slot names defined by the block type.
|
|
12
|
+
*/
|
|
13
|
+
interface ContentNode {
|
|
14
|
+
/** Node id; must match the key in the nodes map. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** Block type name (must exist in schema). */
|
|
17
|
+
block: string;
|
|
18
|
+
/** Block-specific configuration (validated against block's JSON Schema). */
|
|
19
|
+
config: Record<string, unknown>;
|
|
20
|
+
/** Slot name → id or array of ids. Other keys are not allowed in canonical form. */
|
|
21
|
+
[slotName: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Slot value in canonical form: either a single node id or an ordered list of ids.
|
|
25
|
+
*/
|
|
26
|
+
type SlotValue = string | string[];
|
|
27
|
+
/**
|
|
28
|
+
* Content definition in canonical form: one entry id and a flat map of nodes.
|
|
29
|
+
* No nesting; slots reference children by id.
|
|
30
|
+
*/
|
|
31
|
+
interface GenetikContent {
|
|
32
|
+
/** Root node id (must exist in nodes). */
|
|
33
|
+
entryId: string;
|
|
34
|
+
/** Flat map of node id → node. */
|
|
35
|
+
nodes: Record<string, ContentNode>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Input form for a slot value when the schema allows inline (referenceMode "inline" or "both").
|
|
39
|
+
* Can be an id, array of ids, a single inline node object, or array of id or inline node.
|
|
40
|
+
*/
|
|
41
|
+
type InlineSlotValue = string | string[] | InlineNode | (string | InlineNode)[];
|
|
42
|
+
/**
|
|
43
|
+
* Inline node: can appear in a slot when schema allows. Has block, config, optional id,
|
|
44
|
+
* and slot keys whose values may be id(s) or nested inline. Normalization flattens these
|
|
45
|
+
* and assigns ids.
|
|
46
|
+
*/
|
|
47
|
+
interface InlineNode {
|
|
48
|
+
/** Optional; if missing, normalization will generate one. */
|
|
49
|
+
id?: string;
|
|
50
|
+
block: string;
|
|
51
|
+
config: Record<string, unknown>;
|
|
52
|
+
[slotName: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Content in input form: same shape as canonical but slot values may be inline (when schema allows).
|
|
56
|
+
* Used as input to normalizeContent().
|
|
57
|
+
*/
|
|
58
|
+
interface GenetikContentInput {
|
|
59
|
+
entryId: string;
|
|
60
|
+
nodes: Record<string, ContentNodeInput>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Node in input form: id must match map key; slot values may be id, id[], or inline (per schema).
|
|
64
|
+
*/
|
|
65
|
+
interface ContentNodeInput {
|
|
66
|
+
id: string;
|
|
67
|
+
block: string;
|
|
68
|
+
config: Record<string, unknown>;
|
|
69
|
+
[slotName: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region src/normalize.d.ts
|
|
73
|
+
interface NormalizeOptions {
|
|
74
|
+
/** Id generator for new nodes from inline. Defaults to nanoid. */
|
|
75
|
+
generateId?: () => string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Normalizes content that may contain inline nodes in slots (when schema allows)
|
|
79
|
+
* to canonical flat form. Inline nodes are flattened, assigned ids, and slot
|
|
80
|
+
* values become id references. Id-only slots are left unchanged.
|
|
81
|
+
*/
|
|
82
|
+
declare function normalizeContent(schema: GenetikSchema, input: GenetikContentInput, options?: NormalizeOptions): GenetikContent;
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/parse.d.ts
|
|
85
|
+
/**
|
|
86
|
+
* Result of parsing a content JSON string. Use this when parsing user or API input
|
|
87
|
+
* so you can show a clear error instead of throwing.
|
|
88
|
+
*/
|
|
89
|
+
type ParseContentResult = {
|
|
90
|
+
ok: true;
|
|
91
|
+
content: GenetikContent;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
error: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Parses a JSON string into content and checks the minimal shape (entryId and nodes).
|
|
98
|
+
* Does not validate against a schema or normalize; use validateContent and
|
|
99
|
+
* normalizeContent for that.
|
|
100
|
+
*
|
|
101
|
+
* @param raw - JSON string (e.g. from user input or API)
|
|
102
|
+
* @returns ParseContentResult: either { ok: true, content } or { ok: false, error }
|
|
103
|
+
*/
|
|
104
|
+
declare function parseContentJson(raw: string): ParseContentResult;
|
|
105
|
+
//#endregion
|
|
106
|
+
//#region src/validate.d.ts
|
|
107
|
+
interface ContentValidationError {
|
|
108
|
+
/** Path to the problem (e.g. "nodes.a", "nodes.a.config"). */
|
|
109
|
+
path: string;
|
|
110
|
+
/** Human-readable message. */
|
|
111
|
+
message: string;
|
|
112
|
+
}
|
|
113
|
+
interface ContentValidationResult {
|
|
114
|
+
valid: boolean;
|
|
115
|
+
errors: ContentValidationError[];
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Validates content against a schema. Checks:
|
|
119
|
+
* - entryId is present and exists in nodes
|
|
120
|
+
* - Every node has id, block, config; block type exists in schema
|
|
121
|
+
* - Each node's config is valid against the block's JSON Schema
|
|
122
|
+
* - Slot values are id or id[] per slot definition; no extra slot keys
|
|
123
|
+
* - All referenced ids exist in the node map (link integrity)
|
|
124
|
+
*/
|
|
125
|
+
declare function validateContent(schema: GenetikSchema, content: unknown): ContentValidationResult;
|
|
126
|
+
//#endregion
|
|
127
|
+
export { type ContentNode, type ContentNodeInput, type ContentValidationError, type ContentValidationResult, type GenetikContent, type GenetikContentInput, type InlineNode, type InlineSlotValue, type NormalizeOptions, type ParseContentResult, type SlotValue, normalizeContent, parseContentJson, validateContent };
|
|
128
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/normalize.ts","../src/parse.ts","../src/validate.ts"],"mappings":";;;;;;AAUA;;;;;;UAAiB,WAAA;EAMP;EAJR,EAAA;EAMiB;EAJjB,KAAA;EAUU;EARV,MAAA,EAAQ,MAAA;;GAEP,QAAA;AAAA;AAYH;;;AAAA,KANY,SAAA;;;;;UAMK,cAAA;EAIkB;EAFjC,OAAA;EASyB;EAPzB,KAAA,EAAO,MAAA,SAAe,WAAA;AAAA;;AAcxB;;;KAPY,eAAA,uBAAsC,UAAA,aAAuB,UAAA;;;;;;UAOxD,UAAA;EAKE;EAHjB,EAAA;EACA,KAAA;EACA,MAAA,EAAQ,MAAA;EAAA,CACP,QAAA;AAAA;;;;;UAOc,mBAAA;EACf,OAAA;EACA,KAAA,EAAO,MAAA,SAAe,gBAAA;AAAA;;;;UAMP,gBAAA;EACf,EAAA;EACA,KAAA;EACA,MAAA,EAAQ,MAAA;EAAA,CACP,QAAA;AAAA;;;UCnEc,gBAAA;EDKA;ECHf,UAAA;AAAA;;;;;;iBAQc,gBAAA,CACd,MAAA,EAAQ,aAAA,EACR,KAAA,EAAO,mBAAA,EACP,OAAA,GAAU,gBAAA,GACT,cAAA;;;;;ADTH;;KEJY,kBAAA;EACN,EAAA;EAAU,OAAA,EAAS,cAAA;AAAA;EACnB,EAAA;EAAW,KAAA;AAAA;;;AFgBjB;;;;;AAMA;iBEZgB,gBAAA,CAAiB,GAAA,WAAc,kBAAA;;;UCb9B,sBAAA;;EAEf,IAAA;EHG0B;EGD1B,OAAA;AAAA;AAAA,UAGe,uBAAA;EACf,KAAA;EACA,MAAA,EAAQ,sBAAA;AAAA;;;;AHUV;;;;;iBGCgB,eAAA,CACd,MAAA,EAAQ,aAAA,EACR,OAAA,YACC,uBAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { getBlockType, validateConfigAgainstDefinition } from "@genetik/schema";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
|
|
4
|
+
//#region src/normalize.ts
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes content that may contain inline nodes in slots (when schema allows)
|
|
7
|
+
* to canonical flat form. Inline nodes are flattened, assigned ids, and slot
|
|
8
|
+
* values become id references. Id-only slots are left unchanged.
|
|
9
|
+
*/
|
|
10
|
+
function normalizeContent(schema, input, options) {
|
|
11
|
+
const generateId = options?.generateId ?? nanoid;
|
|
12
|
+
const resultNodes = {};
|
|
13
|
+
for (const [nodeId, inputNode] of Object.entries(input.nodes)) {
|
|
14
|
+
const { node, extraNodes } = normalizeNode(schema, inputNode, nodeId, generateId);
|
|
15
|
+
resultNodes[nodeId] = node;
|
|
16
|
+
Object.assign(resultNodes, extraNodes);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
entryId: input.entryId,
|
|
20
|
+
nodes: resultNodes
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function normalizeNode(schema, input, nodeId, generateId) {
|
|
24
|
+
const block = input.block;
|
|
25
|
+
const blockType = getBlockType(schema, block);
|
|
26
|
+
const config = input.config ?? {};
|
|
27
|
+
if (!blockType) {
|
|
28
|
+
const node = {
|
|
29
|
+
id: nodeId,
|
|
30
|
+
block,
|
|
31
|
+
config
|
|
32
|
+
};
|
|
33
|
+
for (const k of Object.keys(input)) {
|
|
34
|
+
if (k === "id" || k === "block" || k === "config") continue;
|
|
35
|
+
const v = input[k];
|
|
36
|
+
if (typeof v === "string" || Array.isArray(v) && v.every((x) => typeof x === "string")) node[k] = v;
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
node,
|
|
40
|
+
extraNodes: {}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const node = {
|
|
44
|
+
id: nodeId,
|
|
45
|
+
block,
|
|
46
|
+
config
|
|
47
|
+
};
|
|
48
|
+
let extraNodes = {};
|
|
49
|
+
for (const slotDef of blockType.slots) {
|
|
50
|
+
const value = input[slotDef.name];
|
|
51
|
+
if (value === void 0 || value === null) continue;
|
|
52
|
+
if (slotDef.referenceMode === "id") {
|
|
53
|
+
node[slotDef.name] = value;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (slotDef.multiple) {
|
|
57
|
+
const arr = Array.isArray(value) ? value : [value];
|
|
58
|
+
const ids = [];
|
|
59
|
+
for (const item of arr) if (typeof item === "string") ids.push(item);
|
|
60
|
+
else if (isInlineNode(item)) {
|
|
61
|
+
const childId = generateId();
|
|
62
|
+
const { node: childNode, extraNodes: childExtras } = normalizeNode(schema, item, childId, generateId);
|
|
63
|
+
ids.push(childId);
|
|
64
|
+
extraNodes = {
|
|
65
|
+
...extraNodes,
|
|
66
|
+
...childExtras,
|
|
67
|
+
[childId]: childNode
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
node[slotDef.name] = ids;
|
|
71
|
+
} else if (typeof value === "string") node[slotDef.name] = value;
|
|
72
|
+
else if (isInlineNode(value)) {
|
|
73
|
+
const childId = generateId();
|
|
74
|
+
const { node: childNode, extraNodes: childExtras } = normalizeNode(schema, value, childId, generateId);
|
|
75
|
+
node[slotDef.name] = childId;
|
|
76
|
+
extraNodes = {
|
|
77
|
+
...extraNodes,
|
|
78
|
+
...childExtras,
|
|
79
|
+
[childId]: childNode
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
node,
|
|
85
|
+
extraNodes
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function isInlineNode(value) {
|
|
89
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && "block" in value;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region src/parse.ts
|
|
94
|
+
/**
|
|
95
|
+
* Parses a JSON string into content and checks the minimal shape (entryId and nodes).
|
|
96
|
+
* Does not validate against a schema or normalize; use validateContent and
|
|
97
|
+
* normalizeContent for that.
|
|
98
|
+
*
|
|
99
|
+
* @param raw - JSON string (e.g. from user input or API)
|
|
100
|
+
* @returns ParseContentResult: either { ok: true, content } or { ok: false, error }
|
|
101
|
+
*/
|
|
102
|
+
function parseContentJson(raw) {
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(raw);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: e instanceof Error ? e.message : "Invalid JSON"
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (parsed === null || typeof parsed !== "object") return {
|
|
113
|
+
ok: false,
|
|
114
|
+
error: "Content must be an object"
|
|
115
|
+
};
|
|
116
|
+
const obj = parsed;
|
|
117
|
+
if (typeof obj.entryId !== "string") return {
|
|
118
|
+
ok: false,
|
|
119
|
+
error: "Content must have entryId (string)"
|
|
120
|
+
};
|
|
121
|
+
if (obj.nodes === null || typeof obj.nodes !== "object") return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: "Content must have nodes (object)"
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
ok: true,
|
|
127
|
+
content: obj
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/validate.ts
|
|
133
|
+
/**
|
|
134
|
+
* Validates content against a schema. Checks:
|
|
135
|
+
* - entryId is present and exists in nodes
|
|
136
|
+
* - Every node has id, block, config; block type exists in schema
|
|
137
|
+
* - Each node's config is valid against the block's JSON Schema
|
|
138
|
+
* - Slot values are id or id[] per slot definition; no extra slot keys
|
|
139
|
+
* - All referenced ids exist in the node map (link integrity)
|
|
140
|
+
*/
|
|
141
|
+
function validateContent(schema, content) {
|
|
142
|
+
const errors = [];
|
|
143
|
+
if (content === null || typeof content !== "object" || Array.isArray(content)) return {
|
|
144
|
+
valid: false,
|
|
145
|
+
errors: [{
|
|
146
|
+
path: "",
|
|
147
|
+
message: "Content must be an object"
|
|
148
|
+
}]
|
|
149
|
+
};
|
|
150
|
+
const c = content;
|
|
151
|
+
const entryId = c.entryId;
|
|
152
|
+
const nodes = c.nodes;
|
|
153
|
+
if (typeof entryId !== "string" || entryId === "") errors.push({
|
|
154
|
+
path: "entryId",
|
|
155
|
+
message: "entryId must be a non-empty string"
|
|
156
|
+
});
|
|
157
|
+
if (nodes === null || typeof nodes !== "object" || Array.isArray(nodes)) errors.push({
|
|
158
|
+
path: "nodes",
|
|
159
|
+
message: "nodes must be an object (map of id to node)"
|
|
160
|
+
});
|
|
161
|
+
const nodeMap = nodes;
|
|
162
|
+
const allIds = new Set(nodeMap ? Object.keys(nodeMap) : []);
|
|
163
|
+
if (nodeMap) for (const [key, val] of Object.entries(nodeMap)) {
|
|
164
|
+
const nodeError = validateNode(schema, key, val, "nodes");
|
|
165
|
+
if (nodeError) errors.push(nodeError);
|
|
166
|
+
}
|
|
167
|
+
if (typeof entryId === "string" && entryId !== "" && nodeMap && !(entryId in nodeMap)) errors.push({
|
|
168
|
+
path: "entryId",
|
|
169
|
+
message: `entryId "${entryId}" is not in nodes`
|
|
170
|
+
});
|
|
171
|
+
if (nodeMap) for (const [nodeKey, val] of Object.entries(nodeMap)) {
|
|
172
|
+
const refErrors = collectSlotReferenceErrors(nodeMap, val, `nodes.${nodeKey}`, allIds);
|
|
173
|
+
errors.push(...refErrors);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
valid: errors.length === 0,
|
|
177
|
+
errors
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function validateNode(schema, mapKey, value, path) {
|
|
181
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return {
|
|
182
|
+
path: `${path}.${mapKey}`,
|
|
183
|
+
message: "Node must be an object"
|
|
184
|
+
};
|
|
185
|
+
const node = value;
|
|
186
|
+
const id = node.id;
|
|
187
|
+
const block = node.block;
|
|
188
|
+
const config = node.config;
|
|
189
|
+
if (typeof id !== "string" || id === "") return {
|
|
190
|
+
path: `${path}.${mapKey}.id`,
|
|
191
|
+
message: "Node id must be a non-empty string"
|
|
192
|
+
};
|
|
193
|
+
if (id !== mapKey) return {
|
|
194
|
+
path: `${path}.${mapKey}.id`,
|
|
195
|
+
message: `Node id "${id}" must match map key "${mapKey}"`
|
|
196
|
+
};
|
|
197
|
+
if (typeof block !== "string" || block === "") return {
|
|
198
|
+
path: `${path}.${mapKey}.block`,
|
|
199
|
+
message: "Node block must be a non-empty string"
|
|
200
|
+
};
|
|
201
|
+
const blockType = block ? getBlockType(schema, block) : void 0;
|
|
202
|
+
if (!blockType) return {
|
|
203
|
+
path: `${path}.${mapKey}.block`,
|
|
204
|
+
message: `Unknown block type: ${block}`
|
|
205
|
+
};
|
|
206
|
+
if (config !== void 0 && (config === null || typeof config !== "object" || Array.isArray(config))) return {
|
|
207
|
+
path: `${path}.${mapKey}.config`,
|
|
208
|
+
message: "Node config must be an object"
|
|
209
|
+
};
|
|
210
|
+
const configResult = validateConfigAgainstDefinition(blockType, config ?? {});
|
|
211
|
+
if (!configResult.valid && configResult.errors?.length) {
|
|
212
|
+
const first = configResult.errors[0];
|
|
213
|
+
return {
|
|
214
|
+
path: `${path}.${mapKey}.config`,
|
|
215
|
+
message: first.message ?? "Invalid config"
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const slotNames = new Set(blockType.slots.map((s) => s.name));
|
|
219
|
+
for (const key of Object.keys(node)) {
|
|
220
|
+
if (key === "id" || key === "block" || key === "config") continue;
|
|
221
|
+
if (!slotNames.has(key)) return {
|
|
222
|
+
path: `${path}.${mapKey}`,
|
|
223
|
+
message: `Unknown slot "${key}" for block type "${block}"`
|
|
224
|
+
};
|
|
225
|
+
const slotDef = blockType.slots.find((s) => s.name === key);
|
|
226
|
+
if (!slotDef) continue;
|
|
227
|
+
const slotValue = node[key];
|
|
228
|
+
if (slotValue === void 0 || slotValue === null) continue;
|
|
229
|
+
if (slotDef.multiple) {
|
|
230
|
+
if (!Array.isArray(slotValue)) return {
|
|
231
|
+
path: `${path}.${mapKey}.${key}`,
|
|
232
|
+
message: `Slot "${key}" must be an array of ids (multiple: true)`
|
|
233
|
+
};
|
|
234
|
+
if (slotValue.some((v) => typeof v !== "string")) return {
|
|
235
|
+
path: `${path}.${mapKey}.${key}`,
|
|
236
|
+
message: `Slot "${key}" must contain only string ids`
|
|
237
|
+
};
|
|
238
|
+
} else if (typeof slotValue !== "string") return {
|
|
239
|
+
path: `${path}.${mapKey}.${key}`,
|
|
240
|
+
message: `Slot "${key}" must be a single id string (multiple: false)`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
function collectSlotReferenceErrors(nodeMap, value, path, allIds) {
|
|
246
|
+
const errors = [];
|
|
247
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return errors;
|
|
248
|
+
const node = value;
|
|
249
|
+
for (const [key, slotValue] of Object.entries(node)) {
|
|
250
|
+
if (key === "id" || key === "block" || key === "config") continue;
|
|
251
|
+
const ids = Array.isArray(slotValue) ? slotValue : slotValue === void 0 || slotValue === null ? [] : [slotValue];
|
|
252
|
+
for (const id of ids) {
|
|
253
|
+
if (typeof id !== "string") continue;
|
|
254
|
+
if (!allIds.has(id)) errors.push({
|
|
255
|
+
path: `${path}.${key}`,
|
|
256
|
+
message: `Referenced id "${id}" is not in nodes (dangling reference)`
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return errors;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
//#endregion
|
|
264
|
+
export { normalizeContent, parseContentJson, validateContent };
|
|
265
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/normalize.ts","../src/parse.ts","../src/validate.ts"],"sourcesContent":["import type { GenetikSchema } from \"@genetik/schema\";\nimport { getBlockType } from \"@genetik/schema\";\nimport { nanoid } from \"nanoid\";\nimport type { ContentNode, GenetikContent, GenetikContentInput } from \"./types.js\";\n\nexport interface NormalizeOptions {\n /** Id generator for new nodes from inline. Defaults to nanoid. */\n generateId?: () => string;\n}\n\n/**\n * Normalizes content that may contain inline nodes in slots (when schema allows)\n * to canonical flat form. Inline nodes are flattened, assigned ids, and slot\n * values become id references. Id-only slots are left unchanged.\n */\nexport function normalizeContent(\n schema: GenetikSchema,\n input: GenetikContentInput,\n options?: NormalizeOptions\n): GenetikContent {\n const generateId = options?.generateId ?? nanoid;\n const resultNodes: Record<string, ContentNode> = {};\n\n for (const [nodeId, inputNode] of Object.entries(input.nodes)) {\n const { node, extraNodes } = normalizeNode(schema, inputNode as Record<string, unknown>, nodeId, generateId);\n resultNodes[nodeId] = node;\n Object.assign(resultNodes, extraNodes);\n }\n\n return {\n entryId: input.entryId,\n nodes: resultNodes,\n };\n}\n\nfunction normalizeNode(\n schema: GenetikSchema,\n input: Record<string, unknown>,\n nodeId: string,\n generateId: () => string\n): { node: ContentNode; extraNodes: Record<string, ContentNode> } {\n const block = input.block as string;\n const blockType = getBlockType(schema, block);\n const config = (input.config as Record<string, unknown>) ?? {};\n\n if (!blockType) {\n const node: ContentNode = { id: nodeId, block, config };\n for (const k of Object.keys(input)) {\n if (k === \"id\" || k === \"block\" || k === \"config\") continue;\n const v = input[k];\n if (typeof v === \"string\" || (Array.isArray(v) && v.every((x) => typeof x === \"string\"))) node[k] = v;\n }\n return { node, extraNodes: {} };\n }\n\n const node: ContentNode = { id: nodeId, block, config };\n let extraNodes: Record<string, ContentNode> = {};\n\n for (const slotDef of blockType.slots) {\n const value = input[slotDef.name];\n if (value === undefined || value === null) continue;\n\n if (slotDef.referenceMode === \"id\") {\n node[slotDef.name] = value;\n continue;\n }\n\n if (slotDef.multiple) {\n const arr = Array.isArray(value) ? value : [value];\n const ids: string[] = [];\n for (const item of arr) {\n if (typeof item === \"string\") {\n ids.push(item);\n } else if (isInlineNode(item)) {\n const childId = generateId();\n const { node: childNode, extraNodes: childExtras } = normalizeNode(\n schema,\n item as Record<string, unknown>,\n childId,\n generateId\n );\n ids.push(childId);\n extraNodes = { ...extraNodes, ...childExtras, [childId]: childNode };\n }\n }\n node[slotDef.name] = ids;\n } else {\n if (typeof value === \"string\") {\n node[slotDef.name] = value;\n } else if (isInlineNode(value)) {\n const childId = generateId();\n const { node: childNode, extraNodes: childExtras } = normalizeNode(\n schema,\n value as Record<string, unknown>,\n childId,\n generateId\n );\n node[slotDef.name] = childId;\n extraNodes = { ...extraNodes, ...childExtras, [childId]: childNode };\n }\n }\n }\n\n return { node, extraNodes };\n}\n\nfunction isInlineNode(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value) && \"block\" in value;\n}\n","import type { GenetikContent } from \"./types.js\";\n\n/**\n * Result of parsing a content JSON string. Use this when parsing user or API input\n * so you can show a clear error instead of throwing.\n */\nexport type ParseContentResult =\n | { ok: true; content: GenetikContent }\n | { ok: false; error: string };\n\n/**\n * Parses a JSON string into content and checks the minimal shape (entryId and nodes).\n * Does not validate against a schema or normalize; use validateContent and\n * normalizeContent for that.\n *\n * @param raw - JSON string (e.g. from user input or API)\n * @returns ParseContentResult: either { ok: true, content } or { ok: false, error }\n */\nexport function parseContentJson(raw: string): ParseContentResult {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n return { ok: false, error: e instanceof Error ? e.message : \"Invalid JSON\" };\n }\n\n if (parsed === null || typeof parsed !== \"object\") {\n return { ok: false, error: \"Content must be an object\" };\n }\n\n const obj = parsed as Record<string, unknown>;\n if (typeof obj.entryId !== \"string\") {\n return { ok: false, error: \"Content must have entryId (string)\" };\n }\n if (obj.nodes === null || typeof obj.nodes !== \"object\") {\n return { ok: false, error: \"Content must have nodes (object)\" };\n }\n\n return { ok: true, content: obj as unknown as GenetikContent };\n}\n","import type { GenetikSchema } from \"@genetik/schema\";\nimport { getBlockType } from \"@genetik/schema\";\nimport { validateConfigAgainstDefinition } from \"@genetik/schema\";\nimport type { ContentNode } from \"./types.js\";\n\nexport interface ContentValidationError {\n /** Path to the problem (e.g. \"nodes.a\", \"nodes.a.config\"). */\n path: string;\n /** Human-readable message. */\n message: string;\n}\n\nexport interface ContentValidationResult {\n valid: boolean;\n errors: ContentValidationError[];\n}\n\n/**\n * Validates content against a schema. Checks:\n * - entryId is present and exists in nodes\n * - Every node has id, block, config; block type exists in schema\n * - Each node's config is valid against the block's JSON Schema\n * - Slot values are id or id[] per slot definition; no extra slot keys\n * - All referenced ids exist in the node map (link integrity)\n */\nexport function validateContent(\n schema: GenetikSchema,\n content: unknown\n): ContentValidationResult {\n const errors: ContentValidationError[] = [];\n\n if (content === null || typeof content !== \"object\" || Array.isArray(content)) {\n return {\n valid: false,\n errors: [{ path: \"\", message: \"Content must be an object\" }],\n };\n }\n\n const c = content as Record<string, unknown>;\n const entryId = c.entryId;\n const nodes = c.nodes;\n\n if (typeof entryId !== \"string\" || entryId === \"\") {\n errors.push({ path: \"entryId\", message: \"entryId must be a non-empty string\" });\n }\n\n if (nodes === null || typeof nodes !== \"object\" || Array.isArray(nodes)) {\n errors.push({ path: \"nodes\", message: \"nodes must be an object (map of id to node)\" });\n }\n\n const nodeMap = nodes as Record<string, unknown> | undefined;\n const allIds = new Set<string>(nodeMap ? Object.keys(nodeMap) : []);\n\n if (nodeMap) {\n for (const [key, val] of Object.entries(nodeMap)) {\n const nodeError = validateNode(schema, key, val, \"nodes\");\n if (nodeError) errors.push(nodeError);\n }\n }\n\n if (typeof entryId === \"string\" && entryId !== \"\" && nodeMap && !(entryId in nodeMap)) {\n errors.push({ path: \"entryId\", message: `entryId \"${entryId}\" is not in nodes` });\n }\n\n if (nodeMap) {\n for (const [nodeKey, val] of Object.entries(nodeMap)) {\n const refErrors = collectSlotReferenceErrors(nodeMap, val, `nodes.${nodeKey}`, allIds);\n errors.push(...refErrors);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\nfunction validateNode(\n schema: GenetikSchema,\n mapKey: string,\n value: unknown,\n path: string\n): ContentValidationError | null {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return { path: `${path}.${mapKey}`, message: \"Node must be an object\" };\n }\n\n const node = value as Record<string, unknown>;\n const id = node.id;\n const block = node.block;\n const config = node.config;\n\n if (typeof id !== \"string\" || id === \"\") {\n return { path: `${path}.${mapKey}.id`, message: \"Node id must be a non-empty string\" };\n }\n if (id !== mapKey) {\n return { path: `${path}.${mapKey}.id`, message: `Node id \"${id}\" must match map key \"${mapKey}\"` };\n }\n if (typeof block !== \"string\" || block === \"\") {\n return { path: `${path}.${mapKey}.block`, message: \"Node block must be a non-empty string\" };\n }\n\n const blockType = block ? getBlockType(schema, block as string) : undefined;\n if (!blockType) {\n return {\n path: `${path}.${mapKey}.block`,\n message: `Unknown block type: ${block}`,\n };\n }\n\n if (config !== undefined && (config === null || typeof config !== \"object\" || Array.isArray(config))) {\n return { path: `${path}.${mapKey}.config`, message: \"Node config must be an object\" };\n }\n\n const configResult = validateConfigAgainstDefinition(blockType, config ?? {});\n if (!configResult.valid && configResult.errors?.length) {\n const first = configResult.errors[0]!;\n return {\n path: `${path}.${mapKey}.config`,\n message: first.message ?? \"Invalid config\",\n };\n }\n\n const slotNames = new Set(blockType.slots.map((s) => s.name));\n for (const key of Object.keys(node)) {\n if (key === \"id\" || key === \"block\" || key === \"config\") continue;\n if (!slotNames.has(key)) {\n return {\n path: `${path}.${mapKey}`,\n message: `Unknown slot \"${key}\" for block type \"${block}\"`,\n };\n }\n const slotDef = blockType.slots.find((s) => s.name === key);\n if (!slotDef) continue;\n const slotValue = node[key];\n if (slotValue === undefined || slotValue === null) continue;\n if (slotDef.multiple) {\n if (!Array.isArray(slotValue)) {\n return {\n path: `${path}.${mapKey}.${key}`,\n message: `Slot \"${key}\" must be an array of ids (multiple: true)`,\n };\n }\n if (slotValue.some((v) => typeof v !== \"string\")) {\n return {\n path: `${path}.${mapKey}.${key}`,\n message: `Slot \"${key}\" must contain only string ids`,\n };\n }\n } else {\n if (typeof slotValue !== \"string\") {\n return {\n path: `${path}.${mapKey}.${key}`,\n message: `Slot \"${key}\" must be a single id string (multiple: false)`,\n };\n }\n }\n }\n\n return null;\n}\n\nfunction collectSlotReferenceErrors(\n nodeMap: Record<string, unknown>,\n value: unknown,\n path: string,\n allIds: Set<string>\n): ContentValidationError[] {\n const errors: ContentValidationError[] = [];\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return errors;\n\n const node = value as ContentNode;\n for (const [key, slotValue] of Object.entries(node)) {\n if (key === \"id\" || key === \"block\" || key === \"config\") continue;\n const ids = Array.isArray(slotValue) ? slotValue : slotValue === undefined || slotValue === null ? [] : [slotValue];\n for (const id of ids) {\n if (typeof id !== \"string\") continue;\n if (!allIds.has(id)) {\n errors.push({\n path: `${path}.${key}`,\n message: `Referenced id \"${id}\" is not in nodes (dangling reference)`,\n });\n }\n }\n }\n return errors;\n}\n"],"mappings":";;;;;;;;;AAeA,SAAgB,iBACd,QACA,OACA,SACgB;CAChB,MAAM,aAAa,SAAS,cAAc;CAC1C,MAAM,cAA2C,EAAE;AAEnD,MAAK,MAAM,CAAC,QAAQ,cAAc,OAAO,QAAQ,MAAM,MAAM,EAAE;EAC7D,MAAM,EAAE,MAAM,eAAe,cAAc,QAAQ,WAAsC,QAAQ,WAAW;AAC5G,cAAY,UAAU;AACtB,SAAO,OAAO,aAAa,WAAW;;AAGxC,QAAO;EACL,SAAS,MAAM;EACf,OAAO;EACR;;AAGH,SAAS,cACP,QACA,OACA,QACA,YACgE;CAChE,MAAM,QAAQ,MAAM;CACpB,MAAM,YAAY,aAAa,QAAQ,MAAM;CAC7C,MAAM,SAAU,MAAM,UAAsC,EAAE;AAE9D,KAAI,CAAC,WAAW;EACd,MAAM,OAAoB;GAAE,IAAI;GAAQ;GAAO;GAAQ;AACvD,OAAK,MAAM,KAAK,OAAO,KAAK,MAAM,EAAE;AAClC,OAAI,MAAM,QAAQ,MAAM,WAAW,MAAM,SAAU;GACnD,MAAM,IAAI,MAAM;AAChB,OAAI,OAAO,MAAM,YAAa,MAAM,QAAQ,EAAE,IAAI,EAAE,OAAO,MAAM,OAAO,MAAM,SAAS,CAAG,MAAK,KAAK;;AAEtG,SAAO;GAAE;GAAM,YAAY,EAAE;GAAE;;CAGjC,MAAM,OAAoB;EAAE,IAAI;EAAQ;EAAO;EAAQ;CACvD,IAAI,aAA0C,EAAE;AAEhD,MAAK,MAAM,WAAW,UAAU,OAAO;EACrC,MAAM,QAAQ,MAAM,QAAQ;AAC5B,MAAI,UAAU,UAAa,UAAU,KAAM;AAE3C,MAAI,QAAQ,kBAAkB,MAAM;AAClC,QAAK,QAAQ,QAAQ;AACrB;;AAGF,MAAI,QAAQ,UAAU;GACpB,MAAM,MAAM,MAAM,QAAQ,MAAM,GAAG,QAAQ,CAAC,MAAM;GAClD,MAAM,MAAgB,EAAE;AACxB,QAAK,MAAM,QAAQ,IACjB,KAAI,OAAO,SAAS,SAClB,KAAI,KAAK,KAAK;YACL,aAAa,KAAK,EAAE;IAC7B,MAAM,UAAU,YAAY;IAC5B,MAAM,EAAE,MAAM,WAAW,YAAY,gBAAgB,cACnD,QACA,MACA,SACA,WACD;AACD,QAAI,KAAK,QAAQ;AACjB,iBAAa;KAAE,GAAG;KAAY,GAAG;MAAc,UAAU;KAAW;;AAGxE,QAAK,QAAQ,QAAQ;aAEjB,OAAO,UAAU,SACnB,MAAK,QAAQ,QAAQ;WACZ,aAAa,MAAM,EAAE;GAC9B,MAAM,UAAU,YAAY;GAC5B,MAAM,EAAE,MAAM,WAAW,YAAY,gBAAgB,cACnD,QACA,OACA,SACA,WACD;AACD,QAAK,QAAQ,QAAQ;AACrB,gBAAa;IAAE,GAAG;IAAY,GAAG;KAAc,UAAU;IAAW;;;AAK1E,QAAO;EAAE;EAAM;EAAY;;AAG7B,SAAS,aAAa,OAAkD;AACtE,QAAO,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,IAAI,WAAW;;;;;;;;;;;;;ACzF5F,SAAgB,iBAAiB,KAAiC;CAChE,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,GAAG;AACV,SAAO;GAAE,IAAI;GAAO,OAAO,aAAa,QAAQ,EAAE,UAAU;GAAgB;;AAG9E,KAAI,WAAW,QAAQ,OAAO,WAAW,SACvC,QAAO;EAAE,IAAI;EAAO,OAAO;EAA6B;CAG1D,MAAM,MAAM;AACZ,KAAI,OAAO,IAAI,YAAY,SACzB,QAAO;EAAE,IAAI;EAAO,OAAO;EAAsC;AAEnE,KAAI,IAAI,UAAU,QAAQ,OAAO,IAAI,UAAU,SAC7C,QAAO;EAAE,IAAI;EAAO,OAAO;EAAoC;AAGjE,QAAO;EAAE,IAAI;EAAM,SAAS;EAAkC;;;;;;;;;;;;;ACbhE,SAAgB,gBACd,QACA,SACyB;CACzB,MAAM,SAAmC,EAAE;AAE3C,KAAI,YAAY,QAAQ,OAAO,YAAY,YAAY,MAAM,QAAQ,QAAQ,CAC3E,QAAO;EACL,OAAO;EACP,QAAQ,CAAC;GAAE,MAAM;GAAI,SAAS;GAA6B,CAAC;EAC7D;CAGH,MAAM,IAAI;CACV,MAAM,UAAU,EAAE;CAClB,MAAM,QAAQ,EAAE;AAEhB,KAAI,OAAO,YAAY,YAAY,YAAY,GAC7C,QAAO,KAAK;EAAE,MAAM;EAAW,SAAS;EAAsC,CAAC;AAGjF,KAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CACrE,QAAO,KAAK;EAAE,MAAM;EAAS,SAAS;EAA+C,CAAC;CAGxF,MAAM,UAAU;CAChB,MAAM,SAAS,IAAI,IAAY,UAAU,OAAO,KAAK,QAAQ,GAAG,EAAE,CAAC;AAEnE,KAAI,QACF,MAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,QAAQ,EAAE;EAChD,MAAM,YAAY,aAAa,QAAQ,KAAK,KAAK,QAAQ;AACzD,MAAI,UAAW,QAAO,KAAK,UAAU;;AAIzC,KAAI,OAAO,YAAY,YAAY,YAAY,MAAM,WAAW,EAAE,WAAW,SAC3E,QAAO,KAAK;EAAE,MAAM;EAAW,SAAS,YAAY,QAAQ;EAAoB,CAAC;AAGnF,KAAI,QACF,MAAK,MAAM,CAAC,SAAS,QAAQ,OAAO,QAAQ,QAAQ,EAAE;EACpD,MAAM,YAAY,2BAA2B,SAAS,KAAK,SAAS,WAAW,OAAO;AACtF,SAAO,KAAK,GAAG,UAAU;;AAI7B,QAAO;EACL,OAAO,OAAO,WAAW;EACzB;EACD;;AAGH,SAAS,aACP,QACA,QACA,OACA,MAC+B;AAC/B,KAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CACrE,QAAO;EAAE,MAAM,GAAG,KAAK,GAAG;EAAU,SAAS;EAA0B;CAGzE,MAAM,OAAO;CACb,MAAM,KAAK,KAAK;CAChB,MAAM,QAAQ,KAAK;CACnB,MAAM,SAAS,KAAK;AAEpB,KAAI,OAAO,OAAO,YAAY,OAAO,GACnC,QAAO;EAAE,MAAM,GAAG,KAAK,GAAG,OAAO;EAAM,SAAS;EAAsC;AAExF,KAAI,OAAO,OACT,QAAO;EAAE,MAAM,GAAG,KAAK,GAAG,OAAO;EAAM,SAAS,YAAY,GAAG,wBAAwB,OAAO;EAAI;AAEpG,KAAI,OAAO,UAAU,YAAY,UAAU,GACzC,QAAO;EAAE,MAAM,GAAG,KAAK,GAAG,OAAO;EAAS,SAAS;EAAyC;CAG9F,MAAM,YAAY,QAAQ,aAAa,QAAQ,MAAgB,GAAG;AAClE,KAAI,CAAC,UACH,QAAO;EACL,MAAM,GAAG,KAAK,GAAG,OAAO;EACxB,SAAS,uBAAuB;EACjC;AAGH,KAAI,WAAW,WAAc,WAAW,QAAQ,OAAO,WAAW,YAAY,MAAM,QAAQ,OAAO,EACjG,QAAO;EAAE,MAAM,GAAG,KAAK,GAAG,OAAO;EAAU,SAAS;EAAiC;CAGvF,MAAM,eAAe,gCAAgC,WAAW,UAAU,EAAE,CAAC;AAC7E,KAAI,CAAC,aAAa,SAAS,aAAa,QAAQ,QAAQ;EACtD,MAAM,QAAQ,aAAa,OAAO;AAClC,SAAO;GACL,MAAM,GAAG,KAAK,GAAG,OAAO;GACxB,SAAS,MAAM,WAAW;GAC3B;;CAGH,MAAM,YAAY,IAAI,IAAI,UAAU,MAAM,KAAK,MAAM,EAAE,KAAK,CAAC;AAC7D,MAAK,MAAM,OAAO,OAAO,KAAK,KAAK,EAAE;AACnC,MAAI,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,SAAU;AACzD,MAAI,CAAC,UAAU,IAAI,IAAI,CACrB,QAAO;GACL,MAAM,GAAG,KAAK,GAAG;GACjB,SAAS,iBAAiB,IAAI,oBAAoB,MAAM;GACzD;EAEH,MAAM,UAAU,UAAU,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI;AAC3D,MAAI,CAAC,QAAS;EACd,MAAM,YAAY,KAAK;AACvB,MAAI,cAAc,UAAa,cAAc,KAAM;AACnD,MAAI,QAAQ,UAAU;AACpB,OAAI,CAAC,MAAM,QAAQ,UAAU,CAC3B,QAAO;IACL,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG;IAC3B,SAAS,SAAS,IAAI;IACvB;AAEH,OAAI,UAAU,MAAM,MAAM,OAAO,MAAM,SAAS,CAC9C,QAAO;IACL,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG;IAC3B,SAAS,SAAS,IAAI;IACvB;aAGC,OAAO,cAAc,SACvB,QAAO;GACL,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG;GAC3B,SAAS,SAAS,IAAI;GACvB;;AAKP,QAAO;;AAGT,SAAS,2BACP,SACA,OACA,MACA,QAC0B;CAC1B,MAAM,SAAmC,EAAE;AAC3C,KAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAAE,QAAO;CAEhF,MAAM,OAAO;AACb,MAAK,MAAM,CAAC,KAAK,cAAc,OAAO,QAAQ,KAAK,EAAE;AACnD,MAAI,QAAQ,QAAQ,QAAQ,WAAW,QAAQ,SAAU;EACzD,MAAM,MAAM,MAAM,QAAQ,UAAU,GAAG,YAAY,cAAc,UAAa,cAAc,OAAO,EAAE,GAAG,CAAC,UAAU;AACnH,OAAK,MAAM,MAAM,KAAK;AACpB,OAAI,OAAO,OAAO,SAAU;AAC5B,OAAI,CAAC,OAAO,IAAI,GAAG,CACjB,QAAO,KAAK;IACV,MAAM,GAAG,KAAK,GAAG;IACjB,SAAS,kBAAkB,GAAG;IAC/B,CAAC;;;AAIR,QAAO"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@genetik/content",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.mjs",
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.mjs",
|
|
14
|
+
"require": "./dist/index.cjs",
|
|
15
|
+
"types": "./dist/index.d.mts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"nanoid": "^5.0.9",
|
|
23
|
+
"@genetik/schema": "0.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"eslint": "^9.39.1",
|
|
27
|
+
"tsdown": "0.20.3",
|
|
28
|
+
"typescript": "5.9.2",
|
|
29
|
+
"vitest": "^2.1.8",
|
|
30
|
+
"@genetik/typescript-config": "0.0.0",
|
|
31
|
+
"@genetik/eslint-config": "0.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"dev": "tsdown --watch",
|
|
40
|
+
"lint": "eslint . --max-warnings 0",
|
|
41
|
+
"check-types": "tsc --noEmit",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest"
|
|
44
|
+
}
|
|
45
|
+
}
|