@protoyard/blueprint 0.0.0-stage → 1.1.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 +1149 -2
- package/dist/aggregate.d.ts +83 -0
- package/dist/aggregate.js +268 -0
- package/dist/app-model.d.ts +76 -0
- package/dist/app-model.js +287 -0
- package/dist/archetypes/index.d.ts +1 -0
- package/dist/archetypes/index.js +1 -0
- package/dist/archetypes/schema.d.ts +126 -0
- package/dist/archetypes/schema.js +77 -0
- package/dist/audit.d.ts +52 -0
- package/dist/audit.js +149 -0
- package/dist/authoring/deck-lint.d.ts +32 -0
- package/dist/authoring/deck-lint.js +567 -0
- package/dist/authoring/index.d.ts +1 -0
- package/dist/authoring/index.js +1 -0
- package/dist/authoring/select-deck-layouts.d.ts +50 -0
- package/dist/authoring/select-deck-layouts.js +213 -0
- package/dist/authoring-apis.d.ts +13 -0
- package/dist/authoring-apis.js +13 -0
- package/dist/authoring-validation.d.ts +65 -0
- package/dist/authoring-validation.js +267 -0
- package/dist/avatar.d.ts +25 -0
- package/dist/avatar.js +30 -0
- package/dist/brand.d.ts +70 -0
- package/dist/brand.js +294 -0
- package/dist/brief/archetypes.d.ts +28 -0
- package/dist/brief/archetypes.js +798 -0
- package/dist/brief/content-synthesize.d.ts +31 -0
- package/dist/brief/content-synthesize.js +210 -0
- package/dist/brief/deck-outlines.d.ts +96 -0
- package/dist/brief/deck-outlines.js +561 -0
- package/dist/brief/draft-bodies.d.ts +29 -0
- package/dist/brief/draft-bodies.js +224 -0
- package/dist/brief/fields.d.ts +19 -0
- package/dist/brief/fields.js +229 -0
- package/dist/brief/index.d.ts +8 -0
- package/dist/brief/index.js +8 -0
- package/dist/brief/kind-detection.d.ts +69 -0
- package/dist/brief/kind-detection.js +353 -0
- package/dist/brief/limits.d.ts +117 -0
- package/dist/brief/limits.js +217 -0
- package/dist/brief/page-doc-outlines.d.ts +80 -0
- package/dist/brief/page-doc-outlines.js +485 -0
- package/dist/brief/synthesize.d.ts +59 -0
- package/dist/brief/synthesize.js +511 -0
- package/dist/brief/text.d.ts +13 -0
- package/dist/brief/text.js +59 -0
- package/dist/canvas-screen.d.ts +31 -0
- package/dist/canvas-screen.js +33 -0
- package/dist/canvas-seed.d.ts +58 -0
- package/dist/canvas-seed.js +82 -0
- package/dist/computed/evaluate.d.ts +11 -0
- package/dist/computed/evaluate.js +101 -0
- package/dist/computed/index.d.ts +4 -0
- package/dist/computed/index.js +3 -0
- package/dist/computed/public.d.ts +14 -0
- package/dist/computed/public.js +19 -0
- package/dist/computed/schema.d.ts +10 -0
- package/dist/computed/schema.js +140 -0
- package/dist/computed/types.d.ts +70 -0
- package/dist/computed/types.js +1 -0
- package/dist/computed/validate.d.ts +6 -0
- package/dist/computed/validate.js +146 -0
- package/dist/csv-guard.d.ts +15 -0
- package/dist/csv-guard.js +29 -0
- package/dist/csv.d.ts +85 -0
- package/dist/csv.js +288 -0
- package/dist/files.d.ts +35 -0
- package/dist/files.js +246 -0
- package/dist/form-schema.d.ts +124 -0
- package/dist/form-schema.js +300 -0
- package/dist/generator-version.d.ts +11 -0
- package/dist/generator-version.js +51 -0
- package/dist/generator.d.ts +34 -0
- package/dist/generator.js +445 -0
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +91 -0
- package/dist/import/csv-shape.d.ts +40 -0
- package/dist/import/csv-shape.js +265 -0
- package/dist/import/json-shape.d.ts +55 -0
- package/dist/import/json-shape.js +491 -0
- package/dist/import/limits.d.ts +13 -0
- package/dist/import/limits.js +14 -0
- package/dist/import/sqlite-schema.d.ts +36 -0
- package/dist/import/sqlite-schema.js +729 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +58 -0
- package/dist/kinds/anchors.d.ts +18 -0
- package/dist/kinds/anchors.js +46 -0
- package/dist/kinds/bodies.d.ts +6 -0
- package/dist/kinds/bodies.js +32 -0
- package/dist/kinds/brand-runtime.d.ts +19 -0
- package/dist/kinds/brand-runtime.js +35 -0
- package/dist/kinds/deck.d.ts +8 -0
- package/dist/kinds/deck.js +34 -0
- package/dist/kinds/doc.d.ts +19 -0
- package/dist/kinds/doc.js +41 -0
- package/dist/kinds/embed/index.d.ts +39 -0
- package/dist/kinds/embed/index.js +91 -0
- package/dist/kinds/envelope.d.ts +159 -0
- package/dist/kinds/envelope.js +78 -0
- package/dist/kinds/index.d.ts +11 -0
- package/dist/kinds/index.js +11 -0
- package/dist/kinds/kind.d.ts +36 -0
- package/dist/kinds/kind.js +55 -0
- package/dist/kinds/limits.d.ts +64 -0
- package/dist/kinds/limits.js +190 -0
- package/dist/kinds/page-doc-runtime.d.ts +37 -0
- package/dist/kinds/page-doc-runtime.js +55 -0
- package/dist/kinds/page.d.ts +16 -0
- package/dist/kinds/page.js +67 -0
- package/dist/kinds/parse.d.ts +18 -0
- package/dist/kinds/parse.js +27 -0
- package/dist/kinds/runtime.d.ts +128 -0
- package/dist/kinds/runtime.js +206 -0
- package/dist/kinds/services.d.ts +49 -0
- package/dist/kinds/services.js +249 -0
- package/dist/kinds/testing.d.ts +9 -0
- package/dist/kinds/testing.js +16 -0
- package/dist/migrations.d.ts +97 -0
- package/dist/migrations.js +351 -0
- package/dist/motion.d.ts +50 -0
- package/dist/motion.js +52 -0
- package/dist/notifications.d.ts +19 -0
- package/dist/notifications.js +177 -0
- package/dist/patch.d.ts +122 -0
- package/dist/patch.js +322 -0
- package/dist/policy.d.ts +23 -0
- package/dist/policy.js +105 -0
- package/dist/runtime-definition.d.ts +39 -0
- package/dist/runtime-definition.js +74 -0
- package/dist/scenarios.d.ts +9 -0
- package/dist/scenarios.js +49 -0
- package/dist/schema/blueprint.schema.json +10191 -0
- package/dist/schema-json.d.ts +6 -0
- package/dist/schema-json.js +36 -0
- package/dist/schema.d.ts +6348 -0
- package/dist/schema.js +1269 -0
- package/dist/search.d.ts +16 -0
- package/dist/search.js +96 -0
- package/dist/services.d.ts +89 -0
- package/dist/services.js +898 -0
- package/dist/share.d.ts +78 -0
- package/dist/share.js +234 -0
- package/dist/sheet-screen.d.ts +49 -0
- package/dist/sheet-screen.js +91 -0
- package/dist/storage.d.ts +43 -0
- package/dist/storage.js +179 -0
- package/dist/stream.d.ts +26 -0
- package/dist/stream.js +173 -0
- package/dist/untrusted.d.ts +63 -0
- package/dist/untrusted.js +502 -0
- package/dist/validation-tasks.d.ts +178 -0
- package/dist/validation-tasks.js +199 -0
- package/dist/validation.d.ts +35 -0
- package/dist/validation.js +312 -0
- package/dist/variants.d.ts +112 -0
- package/dist/variants.js +283 -0
- package/dist/workflows/index.d.ts +3 -0
- package/dist/workflows/index.js +3 -0
- package/dist/workflows/schema.d.ts +50 -0
- package/dist/workflows/schema.js +34 -0
- package/dist/workflows/transition.d.ts +15 -0
- package/dist/workflows/transition.js +25 -0
- package/dist/workflows/validate.d.ts +17 -0
- package/dist/workflows/validate.js +140 -0
- package/package.json +120 -3
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import { parseBlueprint } from "../schema.js";
|
|
2
|
+
import { IMPORT_MAX_BYTES, IMPORT_MAX_DEPTH, IMPORT_MAX_FIELDS, IMPORT_MAX_JSON_NODES, IMPORT_MAX_JSON_OBJECTS, IMPORT_MAX_SAMPLE_RECORDS, IMPORT_MAX_STRING_BYTES, } from "./limits.js";
|
|
3
|
+
export const JSON_SHAPE_MAX_BYTES = IMPORT_MAX_BYTES;
|
|
4
|
+
export const JSON_SHAPE_MAX_SAMPLE_OBJECTS = IMPORT_MAX_SAMPLE_RECORDS;
|
|
5
|
+
export const JSON_SHAPE_MAX_DEPTH = IMPORT_MAX_DEPTH;
|
|
6
|
+
export const JSON_SHAPE_MAX_OBJECTS = IMPORT_MAX_JSON_OBJECTS;
|
|
7
|
+
export const JSON_SHAPE_MAX_FIELDS = IMPORT_MAX_FIELDS;
|
|
8
|
+
export const JSON_SHAPE_MAX_STRING_BYTES = IMPORT_MAX_STRING_BYTES;
|
|
9
|
+
export class JsonShapeError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
constructor(code, message) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = 'JsonShapeError';
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const RESERVED_FIELDS = new Set(['id', 'createdAt', 'updatedAt', 'deletedAt', 'deletedBy']);
|
|
18
|
+
const BIDI_FORMATTING = /[\u202a-\u202e\u2066-\u2069]/u;
|
|
19
|
+
const TITLE_KINDS = new Set(['text', 'integer', 'email', 'url', 'phone']);
|
|
20
|
+
const SEARCHABLE_KINDS = new Set(['text', 'email', 'url', 'phone']);
|
|
21
|
+
const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
22
|
+
const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:?\d{2})$/;
|
|
23
|
+
const URL_PATTERN = /^https?:\/\/[^\s]+$/i;
|
|
24
|
+
function byteLength(value) {
|
|
25
|
+
return new TextEncoder().encode(value).length;
|
|
26
|
+
}
|
|
27
|
+
function limit(value, fallback, maximum) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return fallback;
|
|
30
|
+
if (!Number.isFinite(value) || value < 1) {
|
|
31
|
+
throw new JsonShapeError('invalid-options', 'JSON shape limits must be positive finite numbers');
|
|
32
|
+
}
|
|
33
|
+
return Math.min(Math.floor(value), maximum);
|
|
34
|
+
}
|
|
35
|
+
function limits(options) {
|
|
36
|
+
return {
|
|
37
|
+
maxBytes: limit(options.maxBytes, JSON_SHAPE_MAX_BYTES, JSON_SHAPE_MAX_BYTES),
|
|
38
|
+
maxSampleObjects: limit(options.maxSampleObjects, JSON_SHAPE_MAX_SAMPLE_OBJECTS, JSON_SHAPE_MAX_SAMPLE_OBJECTS),
|
|
39
|
+
maxDepth: limit(options.maxDepth, JSON_SHAPE_MAX_DEPTH, JSON_SHAPE_MAX_DEPTH),
|
|
40
|
+
maxObjects: limit(options.maxObjects, JSON_SHAPE_MAX_OBJECTS, JSON_SHAPE_MAX_OBJECTS),
|
|
41
|
+
maxFields: limit(options.maxFields, JSON_SHAPE_MAX_FIELDS, JSON_SHAPE_MAX_FIELDS),
|
|
42
|
+
maxStringBytes: limit(options.maxStringBytes, JSON_SHAPE_MAX_STRING_BYTES, JSON_SHAPE_MAX_STRING_BYTES),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function isRecord(value) {
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
function hasUnsafeKeyCharacter(value) {
|
|
49
|
+
return (BIDI_FORMATTING.test(value) ||
|
|
50
|
+
Array.from(value).some((character) => {
|
|
51
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
52
|
+
return codePoint < 0x20 || (codePoint >= 0x7f && codePoint <= 0x9f);
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
function assertTextDepth(text, maximum) {
|
|
56
|
+
let depth = 0;
|
|
57
|
+
let inString = false;
|
|
58
|
+
let escaped = false;
|
|
59
|
+
for (const character of text) {
|
|
60
|
+
if (inString) {
|
|
61
|
+
if (escaped)
|
|
62
|
+
escaped = false;
|
|
63
|
+
else if (character === '\\')
|
|
64
|
+
escaped = true;
|
|
65
|
+
else if (character === '"')
|
|
66
|
+
inString = false;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (character === '"')
|
|
70
|
+
inString = true;
|
|
71
|
+
else if (character === '{' || character === '[') {
|
|
72
|
+
depth += 1;
|
|
73
|
+
if (depth > maximum) {
|
|
74
|
+
throw new JsonShapeError('too-deep', `JSON shape exceeds the maximum depth of ${maximum.toLocaleString()}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if ((character === '}' || character === ']') && depth > 0) {
|
|
78
|
+
depth -= 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function validateTree(root, parsedLimits) {
|
|
83
|
+
const pending = [{ value: root, depth: 0 }];
|
|
84
|
+
let objectNodes = 0;
|
|
85
|
+
let valueNodes = 0;
|
|
86
|
+
while (pending.length > 0) {
|
|
87
|
+
const current = pending.pop();
|
|
88
|
+
if (!current)
|
|
89
|
+
continue;
|
|
90
|
+
const { value, depth } = current;
|
|
91
|
+
valueNodes += 1;
|
|
92
|
+
if (valueNodes > IMPORT_MAX_JSON_NODES) {
|
|
93
|
+
throw new JsonShapeError('too-many-objects', `JSON shape exceeds the ${IMPORT_MAX_JSON_NODES.toLocaleString()} value-node limit`);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(value) || isRecord(value))
|
|
96
|
+
objectNodes += 1;
|
|
97
|
+
if (objectNodes > parsedLimits.maxObjects) {
|
|
98
|
+
throw new JsonShapeError('too-many-objects', `JSON shape exceeds the ${parsedLimits.maxObjects.toLocaleString()} object and array limit`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof value === 'string' && byteLength(value) > parsedLimits.maxStringBytes) {
|
|
101
|
+
throw new JsonShapeError('string-too-large', `A JSON string exceeds the ${parsedLimits.maxStringBytes.toLocaleString()}-byte limit`);
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(value) && !isRecord(value))
|
|
104
|
+
continue;
|
|
105
|
+
const childDepth = depth + 1;
|
|
106
|
+
if (childDepth > parsedLimits.maxDepth) {
|
|
107
|
+
throw new JsonShapeError('too-deep', `JSON shape exceeds the maximum depth of ${parsedLimits.maxDepth.toLocaleString()}`);
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
if (valueNodes + pending.length + value.length > IMPORT_MAX_JSON_NODES) {
|
|
111
|
+
throw new JsonShapeError('too-many-objects', `JSON shape exceeds the ${IMPORT_MAX_JSON_NODES.toLocaleString()} value-node limit`);
|
|
112
|
+
}
|
|
113
|
+
for (const child of value)
|
|
114
|
+
pending.push({ value: child, depth: childDepth });
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const keys = Object.keys(value);
|
|
118
|
+
if (keys.length > parsedLimits.maxFields) {
|
|
119
|
+
throw new JsonShapeError('too-many-fields', `A JSON object exceeds the ${parsedLimits.maxFields.toLocaleString()} field limit`);
|
|
120
|
+
}
|
|
121
|
+
for (const key of keys) {
|
|
122
|
+
if (byteLength(key) > parsedLimits.maxStringBytes) {
|
|
123
|
+
throw new JsonShapeError('string-too-large', `A JSON key exceeds the ${parsedLimits.maxStringBytes.toLocaleString()}-byte limit`);
|
|
124
|
+
}
|
|
125
|
+
if (key === '__proto__' ||
|
|
126
|
+
key === 'constructor' ||
|
|
127
|
+
key === 'prototype' ||
|
|
128
|
+
hasUnsafeKeyCharacter(key)) {
|
|
129
|
+
throw new JsonShapeError('unsafe-key', 'JSON shape contains an unsupported field name');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const children = Object.values(value);
|
|
133
|
+
if (valueNodes + pending.length + children.length > IMPORT_MAX_JSON_NODES) {
|
|
134
|
+
throw new JsonShapeError('too-many-objects', `JSON shape exceeds the ${IMPORT_MAX_JSON_NODES.toLocaleString()} value-node limit`);
|
|
135
|
+
}
|
|
136
|
+
for (const child of children)
|
|
137
|
+
pending.push({ value: child, depth: childDepth });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function words(value) {
|
|
141
|
+
return value
|
|
142
|
+
.normalize('NFKD')
|
|
143
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
144
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
145
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
146
|
+
.trim()
|
|
147
|
+
.split(/\s+/)
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
}
|
|
150
|
+
function fieldName(key, index, used) {
|
|
151
|
+
const parts = words(key);
|
|
152
|
+
let name = parts
|
|
153
|
+
.map((part, partIndex) => {
|
|
154
|
+
const lower = part.toLowerCase();
|
|
155
|
+
return partIndex === 0 ? lower : `${lower[0]?.toUpperCase() ?? ''}${lower.slice(1)}`;
|
|
156
|
+
})
|
|
157
|
+
.join('');
|
|
158
|
+
if (!name)
|
|
159
|
+
name = `field${index + 1}`;
|
|
160
|
+
if (/^\d/.test(name))
|
|
161
|
+
name = `field${name}`;
|
|
162
|
+
if (RESERVED_FIELDS.has(name))
|
|
163
|
+
name = `field${name[0]?.toUpperCase() ?? ''}${name.slice(1)}`;
|
|
164
|
+
name = name.slice(0, 64);
|
|
165
|
+
const base = name;
|
|
166
|
+
let suffix = 2;
|
|
167
|
+
while (used.has(name)) {
|
|
168
|
+
const tail = String(suffix++);
|
|
169
|
+
name = `${base.slice(0, 64 - tail.length)}${tail}`;
|
|
170
|
+
}
|
|
171
|
+
used.add(name);
|
|
172
|
+
return name;
|
|
173
|
+
}
|
|
174
|
+
function entityName(value, fallback) {
|
|
175
|
+
const parts = words(value ?? fallback);
|
|
176
|
+
const slug = parts.map((part) => part.toLowerCase()).join('-');
|
|
177
|
+
return slug || 'imported-record';
|
|
178
|
+
}
|
|
179
|
+
function singular(value) {
|
|
180
|
+
if (value.endsWith('ies') && value.length > 3)
|
|
181
|
+
return `${value.slice(0, -3)}y`;
|
|
182
|
+
if (/(?:ss|us|is)$/.test(value))
|
|
183
|
+
return value;
|
|
184
|
+
if (value.endsWith('s') && value.length > 1)
|
|
185
|
+
return value.slice(0, -1);
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
function label(value, fallback) {
|
|
189
|
+
const text = value.trim().replace(/\s+/g, ' ');
|
|
190
|
+
return (text || fallback).slice(0, 60);
|
|
191
|
+
}
|
|
192
|
+
function uniqueEntityName(value, used) {
|
|
193
|
+
const base = entityName(singular(value), 'related-record');
|
|
194
|
+
let candidate = base;
|
|
195
|
+
let suffix = 2;
|
|
196
|
+
while (used.has(candidate))
|
|
197
|
+
candidate = `${base}-${suffix++}`;
|
|
198
|
+
used.add(candidate);
|
|
199
|
+
return candidate;
|
|
200
|
+
}
|
|
201
|
+
function isoDate(value) {
|
|
202
|
+
if (!DATE_PATTERN.test(value))
|
|
203
|
+
return false;
|
|
204
|
+
const date = new Date(`${value}T00:00:00.000Z`);
|
|
205
|
+
return !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value);
|
|
206
|
+
}
|
|
207
|
+
/** Checks an email-shaped value in one pass; avoid backtracking on hostile cells. */
|
|
208
|
+
function isEmailAddress(value) {
|
|
209
|
+
if (value.length > 254)
|
|
210
|
+
return false;
|
|
211
|
+
let at = -1;
|
|
212
|
+
let domainDot = false;
|
|
213
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
214
|
+
const character = value[index];
|
|
215
|
+
if (character === undefined || /\s/u.test(character))
|
|
216
|
+
return false;
|
|
217
|
+
if (character === '@') {
|
|
218
|
+
if (at !== -1)
|
|
219
|
+
return false;
|
|
220
|
+
at = index;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (character === '.' && at !== -1) {
|
|
224
|
+
if (index === at + 1 || index === value.length - 1 || value[index - 1] === '.')
|
|
225
|
+
return false;
|
|
226
|
+
domainDot = true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return at > 0 && at < value.length - 1 && value[at - 1] !== '.' && domainDot;
|
|
230
|
+
}
|
|
231
|
+
function stringKind(key, values) {
|
|
232
|
+
if (values.length > 0 && values.every((value) => DATE_PATTERN.test(value) && isoDate(value))) {
|
|
233
|
+
return 'date';
|
|
234
|
+
}
|
|
235
|
+
if (values.length > 0 &&
|
|
236
|
+
values.every((value) => DATETIME_PATTERN.test(value) && !Number.isNaN(Date.parse(value)))) {
|
|
237
|
+
return 'datetime';
|
|
238
|
+
}
|
|
239
|
+
const normalizedKey = key.toLowerCase();
|
|
240
|
+
if (normalizedKey.includes('email') && values.every(isEmailAddress)) {
|
|
241
|
+
return 'email';
|
|
242
|
+
}
|
|
243
|
+
if (/(?:url|uri|website|link)/.test(normalizedKey) &&
|
|
244
|
+
values.every((value) => URL_PATTERN.test(value))) {
|
|
245
|
+
return 'url';
|
|
246
|
+
}
|
|
247
|
+
if (values.every((value) => value.length > 200))
|
|
248
|
+
return 'longText';
|
|
249
|
+
return 'text';
|
|
250
|
+
}
|
|
251
|
+
function scalarKind(key, values) {
|
|
252
|
+
if (values.length === 0)
|
|
253
|
+
return undefined;
|
|
254
|
+
if (values.every((value) => typeof value === 'string')) {
|
|
255
|
+
return stringKind(key, values);
|
|
256
|
+
}
|
|
257
|
+
if (values.every((value) => typeof value === 'boolean'))
|
|
258
|
+
return 'boolean';
|
|
259
|
+
if (values.every((value) => typeof value === 'number')) {
|
|
260
|
+
return values.every((value) => Number.isSafeInteger(value)) ? 'integer' : 'number';
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
function inferredField(name, fieldLabel, kind, required, target) {
|
|
265
|
+
const common = { name, label: fieldLabel, kind, required };
|
|
266
|
+
if (kind === 'text')
|
|
267
|
+
return { ...common, maxLength: 200 };
|
|
268
|
+
if (kind === 'longText')
|
|
269
|
+
return { ...common, maxLength: 10_000 };
|
|
270
|
+
if (kind === 'tags')
|
|
271
|
+
return { ...common, maxItems: 8 };
|
|
272
|
+
if (kind === 'relation')
|
|
273
|
+
return { ...common, to: target, cardinality: 'many' };
|
|
274
|
+
return common;
|
|
275
|
+
}
|
|
276
|
+
function inferOneEntity(name, entityLabel, rows, depth, state) {
|
|
277
|
+
const sampledRows = rows.slice(0, state.parsedLimits.maxSampleObjects);
|
|
278
|
+
state.objectsInspected += sampledRows.length;
|
|
279
|
+
if (sampledRows.length === 1) {
|
|
280
|
+
state.notes.push(`${entityLabel}: one sample cannot establish required fields; fields remain optional.`);
|
|
281
|
+
}
|
|
282
|
+
const allKeys = [...new Set(sampledRows.flatMap((row) => Object.keys(row)))].sort((a, b) => a.localeCompare(b, 'en'));
|
|
283
|
+
if (allKeys.length > state.parsedLimits.maxFields) {
|
|
284
|
+
throw new JsonShapeError('too-many-fields', `An inferred entity exceeds the ${state.parsedLimits.maxFields.toLocaleString()} field limit`);
|
|
285
|
+
}
|
|
286
|
+
const usedFieldNames = new Set();
|
|
287
|
+
const fields = [];
|
|
288
|
+
let firstTitle;
|
|
289
|
+
let preferredTitle;
|
|
290
|
+
const searchable = [];
|
|
291
|
+
for (const [index, key] of allKeys.entries()) {
|
|
292
|
+
if (RESERVED_FIELDS.has(key)) {
|
|
293
|
+
state.notes.push(`${entityLabel}: managed field "${key}" was omitted from the draft.`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const nameForField = fieldName(key, index, usedFieldNames);
|
|
297
|
+
const fieldLabel = label(key.replace(/([a-z0-9])([A-Z])/g, '$1 $2'), `Field ${index + 1}`);
|
|
298
|
+
const normalizedKey = words(key)
|
|
299
|
+
.map((part, partIndex) => {
|
|
300
|
+
const lower = part.toLowerCase();
|
|
301
|
+
return partIndex === 0 ? lower : `${lower[0]?.toUpperCase() ?? ''}${lower.slice(1)}`;
|
|
302
|
+
})
|
|
303
|
+
.join('');
|
|
304
|
+
if (nameForField !== normalizedKey) {
|
|
305
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: normalized to the field name "${nameForField}".`);
|
|
306
|
+
}
|
|
307
|
+
const values = sampledRows.map((row) => row[key]);
|
|
308
|
+
const presentValues = values.filter((value) => value !== undefined && value !== null);
|
|
309
|
+
const required = values.length > 1 && presentValues.length === values.length;
|
|
310
|
+
if (!required && presentValues.length > 0) {
|
|
311
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: absent or null in some samples; kept optional.`);
|
|
312
|
+
}
|
|
313
|
+
let kind;
|
|
314
|
+
let target;
|
|
315
|
+
const arrays = presentValues.filter(Array.isArray);
|
|
316
|
+
const records = presentValues.filter(isRecord);
|
|
317
|
+
const scalars = presentValues.filter((value) => !Array.isArray(value) && !isRecord(value));
|
|
318
|
+
const nonEmptyArrays = arrays.filter((value) => value.length > 0);
|
|
319
|
+
const objectArrays = nonEmptyArrays.filter((array) => array.every(isRecord));
|
|
320
|
+
if (presentValues.length > 0 &&
|
|
321
|
+
arrays.length === presentValues.length &&
|
|
322
|
+
objectArrays.length > 0) {
|
|
323
|
+
if (objectArrays.length !== nonEmptyArrays.length) {
|
|
324
|
+
kind = 'json';
|
|
325
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: inconsistent array shapes; kept as JSON.`);
|
|
326
|
+
}
|
|
327
|
+
else if (depth >= state.parsedLimits.maxDepth) {
|
|
328
|
+
kind = 'json';
|
|
329
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: relation depth limit reached; kept as JSON.`);
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
target = uniqueEntityName(key, state.usedEntityNames);
|
|
333
|
+
kind = 'relation';
|
|
334
|
+
const nestedRows = objectArrays.flat().slice(0, state.parsedLimits.maxSampleObjects);
|
|
335
|
+
const nestedLabel = label(key.replace(/([a-z0-9])([A-Z])/g, '$1 $2'), target);
|
|
336
|
+
const nestedEntity = inferOneEntity(target, nestedLabel, nestedRows, depth + 1, state);
|
|
337
|
+
state.relatedEntities.push(nestedEntity);
|
|
338
|
+
state.relationCandidates.push({
|
|
339
|
+
sourceEntity: name,
|
|
340
|
+
field: nameForField,
|
|
341
|
+
targetEntity: target,
|
|
342
|
+
cardinality: 'many',
|
|
343
|
+
reason: 'An array of objects suggests a one-to-many relationship; review before generation.',
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else if (presentValues.length > 0 && arrays.length === presentValues.length) {
|
|
348
|
+
const stringsOnly = arrays.every((array) => array.every((value) => typeof value === 'string'));
|
|
349
|
+
if (stringsOnly && arrays.some((array) => array.length > 0)) {
|
|
350
|
+
kind = 'tags';
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
kind = 'json';
|
|
354
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: array shape is ambiguous; kept as JSON.`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
else if (records.length > 0 && records.length === presentValues.length) {
|
|
358
|
+
kind = 'json';
|
|
359
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: nested objects stay in a JSON field; review whether to model a relation.`);
|
|
360
|
+
}
|
|
361
|
+
else if (records.length > 0 || arrays.length > 0) {
|
|
362
|
+
kind = 'json';
|
|
363
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: mixed nested and scalar shapes; kept as JSON.`);
|
|
364
|
+
}
|
|
365
|
+
else {
|
|
366
|
+
const scalarTypes = new Set(scalars.map((value) => typeof value));
|
|
367
|
+
const inferred = scalarKind(key, scalars);
|
|
368
|
+
if (inferred) {
|
|
369
|
+
kind = inferred;
|
|
370
|
+
}
|
|
371
|
+
else if (presentValues.length === 0) {
|
|
372
|
+
kind = 'text';
|
|
373
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: no sample value; kept as text for review.`);
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
kind = 'json';
|
|
377
|
+
state.notes.push(`${entityLabel}.${fieldLabel}: inconsistent scalar types (${[...scalarTypes].sort().join(', ')}); kept as JSON.`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
fields.push(inferredField(nameForField, fieldLabel, kind, required, target));
|
|
381
|
+
if (TITLE_KINDS.has(kind)) {
|
|
382
|
+
firstTitle ??= nameForField;
|
|
383
|
+
if (/name|title|label|subject/i.test(key))
|
|
384
|
+
preferredTitle ??= nameForField;
|
|
385
|
+
}
|
|
386
|
+
if (SEARCHABLE_KINDS.has(kind))
|
|
387
|
+
searchable.push(nameForField);
|
|
388
|
+
}
|
|
389
|
+
if (!firstTitle) {
|
|
390
|
+
const fallbackName = fieldName('Name', fields.length, usedFieldNames);
|
|
391
|
+
fields.push({
|
|
392
|
+
name: fallbackName,
|
|
393
|
+
label: 'Name',
|
|
394
|
+
kind: 'text',
|
|
395
|
+
maxLength: 200,
|
|
396
|
+
required: false,
|
|
397
|
+
});
|
|
398
|
+
firstTitle = fallbackName;
|
|
399
|
+
preferredTitle = fallbackName;
|
|
400
|
+
state.notes.push(`${entityLabel}: no title-compatible field; added a blank Name field for review.`);
|
|
401
|
+
}
|
|
402
|
+
const titleField = preferredTitle ?? firstTitle;
|
|
403
|
+
if (!titleField) {
|
|
404
|
+
throw new JsonShapeError('invalid-shape', 'JSON shape inference could not create a title field');
|
|
405
|
+
}
|
|
406
|
+
return {
|
|
407
|
+
name,
|
|
408
|
+
plural: `${name}s`,
|
|
409
|
+
label: entityLabel,
|
|
410
|
+
fields: fields,
|
|
411
|
+
titleField,
|
|
412
|
+
searchable: searchable.filter((fieldNameValue) => fieldNameValue !== titleField).slice(0, 3),
|
|
413
|
+
features: { csv: false },
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Infer a validated, reviewable draft from one JSON record or an array of
|
|
418
|
+
* records. Only names and shape metadata are returned; source values never
|
|
419
|
+
* enter entities, relation candidates, notes, or errors.
|
|
420
|
+
*/
|
|
421
|
+
export function inferJsonShape(text, options = {}) {
|
|
422
|
+
const parsedLimits = limits(options);
|
|
423
|
+
if (byteLength(text) > parsedLimits.maxBytes) {
|
|
424
|
+
throw new JsonShapeError('input-too-large', `JSON shape input exceeds the ${parsedLimits.maxBytes.toLocaleString()}-byte limit`);
|
|
425
|
+
}
|
|
426
|
+
assertTextDepth(text, parsedLimits.maxDepth);
|
|
427
|
+
let parsed;
|
|
428
|
+
try {
|
|
429
|
+
parsed = JSON.parse(text);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
throw new JsonShapeError('invalid-json', 'JSON shape input is not valid JSON');
|
|
433
|
+
}
|
|
434
|
+
validateTree(parsed, parsedLimits);
|
|
435
|
+
const notes = [];
|
|
436
|
+
let rootRows;
|
|
437
|
+
if (isRecord(parsed)) {
|
|
438
|
+
rootRows = [parsed];
|
|
439
|
+
}
|
|
440
|
+
else if (Array.isArray(parsed)) {
|
|
441
|
+
if (parsed.length === 0) {
|
|
442
|
+
throw new JsonShapeError('invalid-shape', 'JSON shape input needs at least one object');
|
|
443
|
+
}
|
|
444
|
+
const nonNullRows = parsed.filter((row) => row !== null);
|
|
445
|
+
if (!nonNullRows.every(isRecord)) {
|
|
446
|
+
throw new JsonShapeError('invalid-shape', 'A top-level JSON array must contain only objects (or null samples)');
|
|
447
|
+
}
|
|
448
|
+
rootRows = nonNullRows;
|
|
449
|
+
if (rootRows.length === 0) {
|
|
450
|
+
throw new JsonShapeError('invalid-shape', 'JSON shape input needs at least one object');
|
|
451
|
+
}
|
|
452
|
+
if (parsed.length > parsedLimits.maxSampleObjects) {
|
|
453
|
+
notes.push(`Only the first ${parsedLimits.maxSampleObjects.toLocaleString()} top-level objects were inspected.`);
|
|
454
|
+
}
|
|
455
|
+
if (nonNullRows.length !== parsed.length) {
|
|
456
|
+
notes.push('Null top-level samples were ignored.');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
throw new JsonShapeError('invalid-shape', 'JSON shape input must be an object or an array of objects');
|
|
461
|
+
}
|
|
462
|
+
const rootName = entityName(options.entityName, 'imported-record');
|
|
463
|
+
const rootLabel = label(options.entityLabel ?? rootName.replace(/-/g, ' '), 'Imported record');
|
|
464
|
+
const usedEntityNames = new Set([rootName]);
|
|
465
|
+
const state = {
|
|
466
|
+
parsedLimits,
|
|
467
|
+
notes,
|
|
468
|
+
relationCandidates: [],
|
|
469
|
+
usedEntityNames,
|
|
470
|
+
relatedEntities: [],
|
|
471
|
+
objectsInspected: 0,
|
|
472
|
+
};
|
|
473
|
+
const rootEntity = inferOneEntity(rootName, rootLabel, rootRows, 0, state);
|
|
474
|
+
const blueprint = parseBlueprint({
|
|
475
|
+
formatVersion: 1,
|
|
476
|
+
name: `Imported ${rootLabel}`.slice(0, 80),
|
|
477
|
+
entities: [
|
|
478
|
+
rootEntity,
|
|
479
|
+
...state.relatedEntities.sort((left, right) => left.name.localeCompare(right.name, 'en')),
|
|
480
|
+
],
|
|
481
|
+
roles: [{ name: 'owner', permissions: { '*': ['*'] } }],
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
entities: blueprint.entities,
|
|
485
|
+
notes: state.notes,
|
|
486
|
+
relationCandidates: state.relationCandidates.sort((left, right) => `${left.sourceEntity}.${left.field}`.localeCompare(`${right.sourceEntity}.${right.field}`, 'en')),
|
|
487
|
+
objectsInspected: state.objectsInspected,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/** Alias emphasizing this is authoring-only shape inference. */
|
|
491
|
+
export const inferJsonEntity = inferJsonShape;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Shared hard bounds for data-shape imports. Per-format limits may be lower. */
|
|
2
|
+
export declare const IMPORT_MAX_BYTES: number;
|
|
3
|
+
export declare const IMPORT_MAX_ROWS = 10000;
|
|
4
|
+
export declare const IMPORT_MAX_DEPTH = 8;
|
|
5
|
+
export declare const IMPORT_MAX_COLUMNS = 100;
|
|
6
|
+
export declare const IMPORT_MAX_FIELDS = 100;
|
|
7
|
+
export declare const IMPORT_MAX_SAMPLE_RECORDS = 100;
|
|
8
|
+
export declare const IMPORT_MAX_STRING_BYTES: number;
|
|
9
|
+
export declare const IMPORT_MAX_JSON_OBJECTS = 2000;
|
|
10
|
+
export declare const IMPORT_MAX_JSON_NODES = 20000;
|
|
11
|
+
export declare const IMPORT_SQLITE_MAX_BYTES: number;
|
|
12
|
+
export declare const IMPORT_SQLITE_MAX_TABLES = 50;
|
|
13
|
+
export declare const IMPORT_SQLITE_MAX_TOKENS = 40000;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CSV_SHAPE_MAX_CELL_BYTES, CSV_SHAPE_MAX_COLUMNS, CSV_SHAPE_MAX_SAMPLE_ROWS, } from "../csv-guard.js";
|
|
2
|
+
/** Shared hard bounds for data-shape imports. Per-format limits may be lower. */
|
|
3
|
+
export const IMPORT_MAX_BYTES = 2 * 1024 * 1024;
|
|
4
|
+
export const IMPORT_MAX_ROWS = 10_000;
|
|
5
|
+
export const IMPORT_MAX_DEPTH = 8;
|
|
6
|
+
export const IMPORT_MAX_COLUMNS = CSV_SHAPE_MAX_COLUMNS;
|
|
7
|
+
export const IMPORT_MAX_FIELDS = IMPORT_MAX_COLUMNS;
|
|
8
|
+
export const IMPORT_MAX_SAMPLE_RECORDS = CSV_SHAPE_MAX_SAMPLE_ROWS;
|
|
9
|
+
export const IMPORT_MAX_STRING_BYTES = CSV_SHAPE_MAX_CELL_BYTES;
|
|
10
|
+
export const IMPORT_MAX_JSON_OBJECTS = 2_000;
|
|
11
|
+
export const IMPORT_MAX_JSON_NODES = 20_000;
|
|
12
|
+
export const IMPORT_SQLITE_MAX_BYTES = 256 * 1024;
|
|
13
|
+
export const IMPORT_SQLITE_MAX_TABLES = 50;
|
|
14
|
+
export const IMPORT_SQLITE_MAX_TOKENS = 40_000;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type BlueprintEntity } from '../schema.ts';
|
|
2
|
+
export declare const SQLITE_SCHEMA_MAX_BYTES: number;
|
|
3
|
+
export declare const SQLITE_SCHEMA_MAX_TABLES = 50;
|
|
4
|
+
export declare const SQLITE_SCHEMA_MAX_COLUMNS = 100;
|
|
5
|
+
export declare const SQLITE_SCHEMA_MAX_TOKENS = 40000;
|
|
6
|
+
export interface SqliteSchemaOptions {
|
|
7
|
+
/** Maximum UTF-8 bytes accepted. Overrides are clamped to the hard maximum. */
|
|
8
|
+
maxBytes?: number;
|
|
9
|
+
/** Maximum CREATE TABLE statements accepted. Overrides are clamped to the hard maximum. */
|
|
10
|
+
maxTables?: number;
|
|
11
|
+
/** Maximum columns and constraints per table. Overrides are clamped to the hard maximum. */
|
|
12
|
+
maxColumns?: number;
|
|
13
|
+
}
|
|
14
|
+
export type SqliteSchemaErrorCode = 'invalid-ddl' | 'unsupported-ddl' | 'unsupported-type' | 'invalid-reference' | 'input-too-large' | 'too-many-tables' | 'too-many-columns' | 'too-many-tokens' | 'too-deep' | 'invalid-options';
|
|
15
|
+
export declare class SqliteSchemaError extends Error {
|
|
16
|
+
readonly code: SqliteSchemaErrorCode;
|
|
17
|
+
readonly path: string;
|
|
18
|
+
constructor(code: SqliteSchemaErrorCode, path: string, message: string);
|
|
19
|
+
}
|
|
20
|
+
export interface SqlitePrimaryKeyDraft {
|
|
21
|
+
entity: string;
|
|
22
|
+
fields: string[];
|
|
23
|
+
/** SQLite rowid alias `INTEGER PRIMARY KEY` is represented by platform `id`. */
|
|
24
|
+
representedByPlatformId: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface SqliteSchemaResult {
|
|
27
|
+
/** Validated, value-free entity drafts. */
|
|
28
|
+
entities: BlueprintEntity[];
|
|
29
|
+
/** Primary-key shape that blueprint fields cannot express as uniqueness constraints. */
|
|
30
|
+
primaryKeys: SqlitePrimaryKeyDraft[];
|
|
31
|
+
/** Review notes never include SQL literals or database rows. */
|
|
32
|
+
notes: string[];
|
|
33
|
+
tablesImported: number;
|
|
34
|
+
}
|
|
35
|
+
/** Parse bounded CREATE TABLE text into a validated, value-free blueprint draft. */
|
|
36
|
+
export declare function importSqliteSchema(sql: string, options?: SqliteSchemaOptions): SqliteSchemaResult;
|