@beechcms/core 0.9.4 → 0.9.5

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.
Files changed (63) hide show
  1. package/dist/content/content-lock.repository.d.ts +63 -0
  2. package/dist/content/content-lock.repository.d.ts.map +1 -0
  3. package/dist/content/content-lock.repository.js +15 -0
  4. package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts +19 -0
  5. package/dist/dashboard-layout/kanban/kanban-position.repository.d.ts.map +1 -0
  6. package/dist/dashboard-layout/kanban/kanban-position.repository.js +3 -0
  7. package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
  8. package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
  9. package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
  10. package/dist/engine/ddl.d.ts +216 -0
  11. package/dist/engine/ddl.d.ts.map +1 -0
  12. package/dist/engine/ddl.js +574 -0
  13. package/dist/engine/define-seed.d.ts +3 -0
  14. package/dist/engine/define-seed.d.ts.map +1 -0
  15. package/dist/engine/define-seed.js +5 -0
  16. package/dist/engine/introspection.d.ts +101 -0
  17. package/dist/engine/introspection.d.ts.map +1 -0
  18. package/dist/engine/introspection.js +127 -0
  19. package/dist/engine/policies.d.ts +58 -0
  20. package/dist/engine/policies.d.ts.map +1 -0
  21. package/dist/engine/policies.js +168 -0
  22. package/dist/engine/privacy.service.d.ts +65 -0
  23. package/dist/engine/privacy.service.d.ts.map +1 -0
  24. package/dist/engine/privacy.service.js +114 -0
  25. package/dist/engine/query/relations.d.ts +15 -0
  26. package/dist/engine/query/relations.d.ts.map +1 -1
  27. package/dist/engine/query.d.ts +23 -0
  28. package/dist/engine/query.d.ts.map +1 -0
  29. package/dist/engine/query.js +248 -0
  30. package/dist/engine/relations.d.ts +28 -0
  31. package/dist/engine/relations.d.ts.map +1 -0
  32. package/dist/engine/relations.js +25 -0
  33. package/dist/engine/schema-fingerprint.d.ts +71 -0
  34. package/dist/engine/schema-fingerprint.d.ts.map +1 -0
  35. package/dist/engine/schema-fingerprint.js +95 -0
  36. package/dist/engine/schema-mutator.d.ts +26 -0
  37. package/dist/engine/schema-mutator.d.ts.map +1 -0
  38. package/dist/engine/schema-mutator.js +3 -0
  39. package/dist/engine/seed-ddl-destructive.d.ts +40 -0
  40. package/dist/engine/seed-ddl-destructive.d.ts.map +1 -0
  41. package/dist/engine/seed-ddl-destructive.js +149 -0
  42. package/dist/engine/seed-ddl.d.ts +43 -0
  43. package/dist/engine/seed-ddl.d.ts.map +1 -0
  44. package/dist/engine/seed-ddl.js +115 -0
  45. package/dist/engine/seed-registry.d.ts +71 -0
  46. package/dist/engine/seed-registry.d.ts.map +1 -0
  47. package/dist/engine/seed-registry.js +167 -0
  48. package/dist/engine/seed-types-generator.d.ts +25 -0
  49. package/dist/engine/seed-types-generator.d.ts.map +1 -0
  50. package/dist/engine/seed-types-generator.js +117 -0
  51. package/dist/engine/seed-validation.d.ts +30 -0
  52. package/dist/engine/seed-validation.d.ts.map +1 -0
  53. package/dist/engine/seed-validation.js +329 -0
  54. package/dist/engine/seeds.d.ts +20 -0
  55. package/dist/engine/seeds.d.ts.map +1 -0
  56. package/dist/engine/seeds.js +21 -0
  57. package/dist/engine/serialize.d.ts +20 -0
  58. package/dist/engine/serialize.d.ts.map +1 -0
  59. package/dist/engine/serialize.js +160 -0
  60. package/dist/seed-types-generator.d.ts +10 -0
  61. package/dist/seed-types-generator.d.ts.map +1 -0
  62. package/dist/seed-types-generator.js +86 -0
  63. package/package.json +2 -2
@@ -0,0 +1,117 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ const HEADER = '// Questo file è generato automaticamente da BeechCMS CLI. Non modificarlo direttamente.\n';
4
+ /** System columns emitted by generateCreateTable — always present, never optional. */
5
+ const SYSTEM_FIELDS = ` id: string\n` +
6
+ ` slug: string\n` +
7
+ ` status: 'draft' | 'review' | 'published' | 'archived'\n`;
8
+ const SYSTEM_TIMESTAMPS = ` created_at: number\n` +
9
+ ` updated_at: number\n`;
10
+ /** slug/alias → PascalCase identifier. 'blog-posts' → 'BlogPosts'. */
11
+ export function pascalCase(input) {
12
+ return input
13
+ .split(/[^A-Za-z0-9]+/)
14
+ .filter(Boolean)
15
+ .map(w => w.charAt(0).toUpperCase() + w.slice(1))
16
+ .join('');
17
+ }
18
+ function literalUnion(options) {
19
+ return options.map(o => `'${o.replace(/'/g, "\\'")}'`).join(' | ');
20
+ }
21
+ /** Maps a single Branch to its TypeScript type expression (no optional marker). */
22
+ export function tsTypeForBranch(branch) {
23
+ switch (branch.type) {
24
+ case 'number':
25
+ case 'date':
26
+ return 'number';
27
+ case 'boolean':
28
+ return 'boolean';
29
+ case 'json':
30
+ return 'unknown';
31
+ case 'text':
32
+ case 'richtext':
33
+ case 'file': {
34
+ const base = branch.type === 'text' && branch.options?.length
35
+ ? literalUnion(branch.options)
36
+ : 'string';
37
+ return branch.type === 'file' && branch.multiple ? `${base}[]` : base;
38
+ }
39
+ case 'tags': {
40
+ const base = branch.options?.length ? `(${literalUnion(branch.options)})` : 'string';
41
+ return `${base}[]`;
42
+ }
43
+ case 'relation':
44
+ return branch.multiple ? 'string[]' : 'string';
45
+ case 'repeater': {
46
+ const inner = (branch.fields ?? [])
47
+ .map(f => `${propName(f)}${optional(f)}: ${tsTypeForBranch(f)}`)
48
+ .join('; ');
49
+ return `Array<{ ${inner} }>`;
50
+ }
51
+ default: {
52
+ // Exhaustiveness guard — fails the build if BranchType gains a member.
53
+ const _never = branch.type;
54
+ return _never;
55
+ }
56
+ }
57
+ }
58
+ function optional(branch) {
59
+ return branch.requiredOnCreate ? '' : '?';
60
+ }
61
+ function propName(branch) {
62
+ // alias is a valid SQL column; quote only if not a plain identifier.
63
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(branch.alias) ? branch.alias : `'${branch.alias}'`;
64
+ }
65
+ /** Emits one exported interface for a single Seed. */
66
+ export function interfaceForSeed(seed) {
67
+ const name = pascalCase(seed.slug);
68
+ const props = seed.branches
69
+ .map(b => ` ${propName(b)}${optional(b)}: ${tsTypeForBranch(b)}`)
70
+ .join('\n');
71
+ return (`export interface ${name} {\n` +
72
+ SYSTEM_FIELDS +
73
+ (props ? props + '\n' : '') +
74
+ SYSTEM_TIMESTAMPS +
75
+ `}\n`);
76
+ }
77
+ /** `v{version}:{32 hex}` — the exact shape `computeSchemaFingerprint` returns. */
78
+ const FINGERPRINT_RE = /^v\d+:[0-9a-f]{32}$/;
79
+ /**
80
+ * Pure entry point. Deterministic: sorts seeds by slug for stable diffs.
81
+ *
82
+ * The embedded fingerprint is the client's half of the drift check: `@beechcms/client` compares it
83
+ * against the `X-Schema-Revision` header the API returns and raises an actionable error on a
84
+ * mismatch, instead of trusting a stale response shape.
85
+ */
86
+ export function generateSeedTypes(seeds, options = {}) {
87
+ const sorted = [...seeds].sort((a, b) => a.slug.localeCompare(b.slug));
88
+ const interfaces = sorted.map(interfaceForSeed).join('\n');
89
+ const registryProps = sorted
90
+ .map(s => ` ${propName({ alias: s.slug })}: ${pascalCase(s.slug)}`)
91
+ .join('\n');
92
+ const databaseRegistry = `export interface BeechDatabase {\n` +
93
+ (registryProps ? registryProps + '\n' : '') +
94
+ `}\n\n` +
95
+ `export type SeedRegistryTypes = BeechDatabase\n`;
96
+ return `${HEADER}\n${fingerprintBlock(options.fingerprint)}${interfaces}\n${databaseRegistry}`;
97
+ }
98
+ /**
99
+ * Emits the fingerprint constant, or nothing.
100
+ *
101
+ * The value is interpolated into source, so its shape is verified first: a fingerprint carrying a
102
+ * quote or a newline would emit a file that does not parse, and the failure would surface in the
103
+ * consumer's build rather than here.
104
+ */
105
+ function fingerprintBlock(fingerprint) {
106
+ if (fingerprint === undefined)
107
+ return '';
108
+ if (!FINGERPRINT_RE.test(fingerprint)) {
109
+ throw new Error(`Refusing to embed '${fingerprint}' as a schema fingerprint: expected the v{n}:{32 hex} form produced by computeSchemaFingerprint().`);
110
+ }
111
+ return (`/**\n` +
112
+ ` * Schema revision these types were generated from. \`@beechcms/client\` compares it against the\n` +
113
+ ` * \`X-Schema-Revision\` response header; a mismatch means the backend schema moved and this file\n` +
114
+ ` * must be regenerated with \`beech types generate\`.\n` +
115
+ ` */\n` +
116
+ `export const SCHEMA_FINGERPRINT = '${fingerprint}'\n\n`);
117
+ }
@@ -0,0 +1,30 @@
1
+ import type { Seed } from './types.js';
2
+ /**
3
+ * Allowed charset for a branch alias. An alias becomes a raw SQL column name in
4
+ * CREATE TABLE / ADD COLUMN / CREATE INDEX and in SELECT/WHERE/ORDER BY clauses
5
+ * (see engine.ts), so it MUST be restricted to a safe identifier charset to
6
+ * prevent DDL/query injection. Exported so the seeds API rename route reuses the
7
+ * exact same guard — do not inline a divergent copy.
8
+ */
9
+ export declare const BRANCH_ALIAS_RE: RegExp;
10
+ /**
11
+ * Allowed charset for a seed slug identifier.
12
+ * Must consist solely of lowercase ASCII letters, digits, and underscores.
13
+ * Exported so route handlers and validation functions reuse the exact same guard.
14
+ */
15
+ export declare const SEED_SLUG_RE: RegExp;
16
+ export declare const SLUG_RE: RegExp;
17
+ export interface SeedValidationIssue {
18
+ slug: string;
19
+ messages: string[];
20
+ fatal: boolean;
21
+ }
22
+ /**
23
+ * Pure, console-free, throw-free validation of a seed set.
24
+ * The single seed being created/edited should be validated in the context of
25
+ * the full active set: call validateSeedDefinitions([...otherActiveSeeds, edited]).
26
+ */
27
+ export declare function validateSeedDefinitions(seeds: Seed[]): SeedValidationIssue[];
28
+ /** Convenience: true iff no fatal issues. */
29
+ export declare function isSeedSetValid(seeds: Seed[]): boolean;
30
+ //# sourceMappingURL=seed-validation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-validation.d.ts","sourceRoot":"","sources":["../../src/engine/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAQtC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAAyB,CAAA;AAErD;;;;GAIG;AACH,eAAO,MAAM,YAAY,QAAiB,CAAA;AAC1C,eAAO,MAAM,OAAO,QAAe,CAAA;AAGnC,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;CACf;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,mBAAmB,EAAE,CA6T5E;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAErD"}
@@ -0,0 +1,329 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { AUTOMATION_RESERVED_WORDS } from '../automations/automations-grammar-words.js';
4
+ import { SYSTEM_COLUMNS } from './ddl.js';
5
+ import { sortSeedsByDependencies } from './seed-registry.js';
6
+ import { SQL_RESERVED_WORDS } from './sql-reserved-words.js';
7
+ const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
8
+ /**
9
+ * Allowed charset for a branch alias. An alias becomes a raw SQL column name in
10
+ * CREATE TABLE / ADD COLUMN / CREATE INDEX and in SELECT/WHERE/ORDER BY clauses
11
+ * (see engine.ts), so it MUST be restricted to a safe identifier charset to
12
+ * prevent DDL/query injection. Exported so the seeds API rename route reuses the
13
+ * exact same guard — do not inline a divergent copy.
14
+ */
15
+ export const BRANCH_ALIAS_RE = /^[a-z][a-zA-Z0-9_]*$/;
16
+ /**
17
+ * Allowed charset for a seed slug identifier.
18
+ * Must consist solely of lowercase ASCII letters, digits, and underscores.
19
+ * Exported so route handlers and validation functions reuse the exact same guard.
20
+ */
21
+ export const SEED_SLUG_RE = /^[a-z0-9_]+$/;
22
+ export const SLUG_RE = SEED_SLUG_RE;
23
+ /**
24
+ * Pure, console-free, throw-free validation of a seed set.
25
+ * The single seed being created/edited should be validated in the context of
26
+ * the full active set: call validateSeedDefinitions([...otherActiveSeeds, edited]).
27
+ */
28
+ export function validateSeedDefinitions(seeds) {
29
+ const result = [];
30
+ // ── Fatal 1: unknown relation targets ────────────────────────────────────
31
+ const slugSet = new Set(seeds.map(s => s.slug));
32
+ for (const seed of seeds) {
33
+ const messages = [];
34
+ for (const branch of seed.branches) {
35
+ if (branch.type === 'relation' && branch.targetSeed) {
36
+ if (!slugSet.has(branch.targetSeed)) {
37
+ messages.push(`branch '${branch.alias}' targets unknown seed '${branch.targetSeed}'`);
38
+ }
39
+ }
40
+ }
41
+ if (messages.length > 0)
42
+ result.push({ slug: seed.slug, messages, fatal: true });
43
+ }
44
+ // ── Fatal 2: multi-relation with SET NULL ────────────────────────────────
45
+ for (const seed of seeds) {
46
+ const messages = [];
47
+ for (const branch of seed.branches) {
48
+ if (branch.type === 'relation' && branch.multiple === true && branch.onDelete === 'SET NULL') {
49
+ messages.push(`Branch '${branch.alias}': multi-relations cannot use ON DELETE SET NULL. ` +
50
+ `Use 'CASCADE' or 'RESTRICT'.`);
51
+ }
52
+ }
53
+ if (messages.length > 0)
54
+ result.push({ slug: seed.slug, messages, fatal: true });
55
+ }
56
+ // ── Fatal 3: junction table name collisions / length ─────────────────────
57
+ {
58
+ const junctionNames = new Set();
59
+ for (const seed of seeds) {
60
+ const messages = [];
61
+ for (const branch of seed.branches) {
62
+ if (branch.type !== 'relation' || branch.multiple !== true)
63
+ continue;
64
+ const name = `rel_${seed.slug}_${branch.alias}`;
65
+ if (name.length > 256) {
66
+ messages.push(`Junction table name '${name}' exceeds 256 characters (${name.length})`);
67
+ }
68
+ if (junctionNames.has(name)) {
69
+ messages.push(`Junction table name collision: '${name}' already used by another seed/branch`);
70
+ }
71
+ junctionNames.add(name);
72
+ }
73
+ if (messages.length > 0)
74
+ result.push({ slug: seed.slug, messages, fatal: true });
75
+ }
76
+ }
77
+ // ── Fatal 4: dependency cycles ────────────────────────────────────────────
78
+ const hasUnknownTargets = result.some(e => e.fatal);
79
+ if (!hasUnknownTargets) {
80
+ try {
81
+ sortSeedsByDependencies(seeds);
82
+ }
83
+ catch (err) {
84
+ const msg = err instanceof Error ? err.message : String(err);
85
+ const cycleMatch = msg.match(/Cyclic dependency detected among seeds: (.*)/);
86
+ if (cycleMatch) {
87
+ const cycleSlugs = cycleMatch[1].split(', ');
88
+ for (const slug of cycleSlugs) {
89
+ result.push({ slug, messages: [msg], fatal: true });
90
+ }
91
+ }
92
+ else {
93
+ result.push({ slug: '<graph>', messages: [msg], fatal: true });
94
+ }
95
+ }
96
+ }
97
+ // ── Fatal 5: branch id format / uniqueness ───────────────────────────────
98
+ for (const seed of seeds) {
99
+ const messages = [];
100
+ const idsInSeed = new Set();
101
+ for (const branch of seed.branches) {
102
+ if (!branch.id || !BRANCH_ID_RE.test(branch.id)) {
103
+ messages.push(`branch '${branch.alias}' has invalid id '${branch.id}'. Expected format ^br_[A-Za-z0-9]+$.`);
104
+ }
105
+ else if (idsInSeed.has(branch.id)) {
106
+ messages.push(`duplicate branch id '${branch.id}'`);
107
+ }
108
+ if (branch.id)
109
+ idsInSeed.add(branch.id);
110
+ // Alias becomes a raw SQL column name — reject anything outside the safe
111
+ // identifier charset before it can reach DDL/query string interpolation.
112
+ if (typeof branch.alias !== 'string' || !BRANCH_ALIAS_RE.test(branch.alias)) {
113
+ messages.push(`branch alias '${branch.alias}' is invalid. Expected format ${BRANCH_ALIAS_RE.source} ` +
114
+ `(lowercase letter followed by alphanumeric characters or underscores).`);
115
+ }
116
+ }
117
+ if (messages.length > 0)
118
+ result.push({ slug: seed.slug, messages, fatal: true });
119
+ }
120
+ // ── Fatal 6: reserved aliases ─────────────────────────────────────────────
121
+ for (const seed of seeds) {
122
+ const messages = [];
123
+ for (const branch of seed.branches) {
124
+ if (SYSTEM_COLUMNS.has(branch.alias)) {
125
+ messages.push(`branch '${branch.alias}' collides with reserved system column. ` +
126
+ `System columns (id, slug, status, created_at, updated_at) cannot be used as branch aliases.`);
127
+ }
128
+ else if (AUTOMATION_RESERVED_WORDS.has(branch.alias)) {
129
+ messages.push(`branch '${branch.alias}' uses reserved alias. ` +
130
+ `This word is used by the automation template grammar.`);
131
+ }
132
+ else if (SQL_RESERVED_WORDS.has(branch.alias.toLowerCase())) {
133
+ messages.push(`branch '${branch.alias}' uses SQL reserved keyword. ` +
134
+ `Please pick a different alias to prevent database errors.`);
135
+ }
136
+ }
137
+ if (messages.length > 0)
138
+ result.push({ slug: seed.slug, messages, fatal: true });
139
+ }
140
+ // ── Fatal 10: repeater sub-field constraints ─────────────────────────────
141
+ {
142
+ const REPEATER_DISALLOWED_SUBTYPES = new Set(['repeater', 'relation', 'file']);
143
+ for (const seed of seeds) {
144
+ const messages = [];
145
+ for (const branch of seed.branches) {
146
+ if (branch.type !== 'repeater')
147
+ continue;
148
+ const subIds = new Set();
149
+ const subAliases = new Set();
150
+ for (const sub of branch.fields ?? []) {
151
+ if (REPEATER_DISALLOWED_SUBTYPES.has(sub.type)) {
152
+ messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has disallowed type '${sub.type}'. ` +
153
+ `Repeater sub-fields cannot be 'repeater', 'relation', or 'file' in v1.`);
154
+ }
155
+ if (!sub.id || !BRANCH_ID_RE.test(sub.id)) {
156
+ messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has invalid id '${sub.id}'. ` +
157
+ `Expected format ^br_[A-Za-z0-9]+$.`);
158
+ }
159
+ else if (subIds.has(sub.id)) {
160
+ messages.push(`branch '${branch.alias}': duplicate sub-field id '${sub.id}'`);
161
+ }
162
+ if (sub.id)
163
+ subIds.add(sub.id);
164
+ if (typeof sub.alias !== 'string' || !BRANCH_ALIAS_RE.test(sub.alias)) {
165
+ messages.push(`branch '${branch.alias}': sub-field alias '${sub.alias}' is invalid. ` +
166
+ `Expected format ${BRANCH_ALIAS_RE.source}.`);
167
+ }
168
+ else if (subAliases.has(sub.alias)) {
169
+ messages.push(`branch '${branch.alias}': duplicate sub-field alias '${sub.alias}'`);
170
+ }
171
+ subAliases.add(sub.alias);
172
+ }
173
+ }
174
+ if (messages.length > 0)
175
+ result.push({ slug: seed.slug, messages, fatal: true });
176
+ }
177
+ }
178
+ // ── Warning 7: duplicate slug ─────────────────────────────────────────────
179
+ {
180
+ const slugsSeen = new Set();
181
+ for (const seed of seeds) {
182
+ const messages = [];
183
+ if (slugsSeen.has(seed.slug)) {
184
+ messages.push(`duplicate slug "${seed.slug}" — each seed must have a unique slug`);
185
+ }
186
+ slugsSeen.add(seed.slug);
187
+ if (messages.length > 0)
188
+ result.push({ slug: seed.slug, messages, fatal: false });
189
+ }
190
+ }
191
+ // ── Warning 8: duplicate branch alias within a seed ───────────────────────
192
+ for (const seed of seeds) {
193
+ const messages = [];
194
+ const aliasesSeen = new Set();
195
+ for (const branch of seed.branches) {
196
+ if (aliasesSeen.has(branch.alias)) {
197
+ messages.push(`duplicate branch alias "${branch.alias}"`);
198
+ }
199
+ aliasesSeen.add(branch.alias);
200
+ }
201
+ if (messages.length > 0)
202
+ result.push({ slug: seed.slug, messages, fatal: false });
203
+ }
204
+ // ── Warning 9: displayNameAlias not in branches ───────────────────────────
205
+ for (const seed of seeds) {
206
+ const allAliases = new Set(seed.branches.map(b => b.alias));
207
+ if (!allAliases.has(seed.displayNameAlias)) {
208
+ result.push({
209
+ slug: seed.slug,
210
+ messages: [`displayNameAlias "${seed.displayNameAlias}" not found in branches`],
211
+ fatal: false,
212
+ });
213
+ }
214
+ }
215
+ // ── Fatal 11 / Warning 10: repeater cardinality bounds ───────────────────
216
+ {
217
+ for (const seed of seeds) {
218
+ const fatals = [];
219
+ const warnings = [];
220
+ for (const branch of seed.branches) {
221
+ const hasBounds = branch.minItems !== undefined || branch.maxItems !== undefined;
222
+ if (!hasBounds)
223
+ continue;
224
+ if (branch.type !== 'repeater') {
225
+ warnings.push(`branch '${branch.alias}': minItems/maxItems are ignored on type ` +
226
+ `'${branch.type}' (repeater-only).`);
227
+ continue;
228
+ }
229
+ for (const [key, val] of [['minItems', branch.minItems], ['maxItems', branch.maxItems]]) {
230
+ if (val !== undefined && (!Number.isInteger(val) || val < 0)) {
231
+ fatals.push(`branch '${branch.alias}': ${key} must be a non-negative integer (got ${val}).`);
232
+ }
233
+ }
234
+ if (Number.isInteger(branch.minItems) && Number.isInteger(branch.maxItems) &&
235
+ branch.minItems > branch.maxItems) {
236
+ fatals.push(`branch '${branch.alias}': minItems (${branch.minItems}) must be <= maxItems (${branch.maxItems}).`);
237
+ }
238
+ }
239
+ if (fatals.length > 0)
240
+ result.push({ slug: seed.slug, messages: fatals, fatal: true });
241
+ if (warnings.length > 0)
242
+ result.push({ slug: seed.slug, messages: warnings, fatal: false });
243
+ }
244
+ }
245
+ // ── Fatal 12: displayNameAlias format validation ───────────────────────────
246
+ for (const seed of seeds) {
247
+ if (typeof seed.displayNameAlias !== 'string' || !BRANCH_ALIAS_RE.test(seed.displayNameAlias)) {
248
+ result.push({
249
+ slug: seed.slug,
250
+ messages: [
251
+ `displayNameAlias '${seed.displayNameAlias}' is invalid. Expected format ${BRANCH_ALIAS_RE.source} ` +
252
+ `(lowercase letter followed by alphanumeric characters or underscores).`,
253
+ ],
254
+ fatal: true,
255
+ });
256
+ }
257
+ else if (SYSTEM_COLUMNS.has(seed.displayNameAlias)) {
258
+ result.push({
259
+ slug: seed.slug,
260
+ messages: [
261
+ `displayNameAlias '${seed.displayNameAlias}' collides with reserved system column. ` +
262
+ `System columns (id, slug, status, created_at, updated_at) cannot be used as displayNameAlias.`,
263
+ ],
264
+ fatal: true,
265
+ });
266
+ }
267
+ else if (SQL_RESERVED_WORDS.has(seed.displayNameAlias.toLowerCase())) {
268
+ result.push({
269
+ slug: seed.slug,
270
+ messages: [
271
+ `displayNameAlias '${seed.displayNameAlias}' uses SQL reserved keyword. ` +
272
+ `Please pick a different alias to prevent database errors.`,
273
+ ],
274
+ fatal: true,
275
+ });
276
+ }
277
+ }
278
+ // ── Fatal 13: retentionDays validation ─────────────────────────────────────
279
+ for (const seed of seeds) {
280
+ if (seed.retentionDays !== undefined && (!Number.isInteger(seed.retentionDays) || seed.retentionDays <= 0)) {
281
+ result.push({
282
+ slug: seed.slug,
283
+ messages: [
284
+ `retentionDays must be a positive integer >= 1 (got ${seed.retentionDays}).`,
285
+ ],
286
+ fatal: true,
287
+ });
288
+ }
289
+ }
290
+ // ── Fatal 14: publicEdit validation ─────────────────────────────────────────
291
+ for (const seed of seeds) {
292
+ const messages = [];
293
+ for (const branch of seed.branches) {
294
+ if (branch.policies?.publicEdit !== undefined && typeof branch.policies.publicEdit !== 'boolean') {
295
+ messages.push(`branch '${branch.alias}': policies.publicEdit must be a boolean`);
296
+ }
297
+ }
298
+ if (messages.length > 0)
299
+ result.push({ slug: seed.slug, messages, fatal: true });
300
+ }
301
+ // ── Fatal 15: seed slug format validation ─────────────────────────────────
302
+ for (const seed of seeds) {
303
+ if (typeof seed.slug !== 'string' || !SEED_SLUG_RE.test(seed.slug)) {
304
+ result.push({
305
+ slug: seed.slug,
306
+ messages: [
307
+ `slug '${seed.slug}' is invalid. Expected format ${SEED_SLUG_RE.source} ` +
308
+ `(lowercase letters, numbers, and underscores only).`,
309
+ ],
310
+ fatal: true,
311
+ });
312
+ }
313
+ }
314
+ // ── Fatal 16: softDelete type validation ────────────────────────────────────
315
+ for (const seed of seeds) {
316
+ if (seed.softDelete !== undefined && typeof seed.softDelete !== 'boolean') {
317
+ result.push({
318
+ slug: seed.slug,
319
+ messages: [`softDelete must be a boolean (got ${typeof seed.softDelete}).`],
320
+ fatal: true,
321
+ });
322
+ }
323
+ }
324
+ return result;
325
+ }
326
+ /** Convenience: true iff no fatal issues. */
327
+ export function isSeedSetValid(seeds) {
328
+ return validateSeedDefinitions(seeds).every(i => !i.fatal);
329
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Seed Registry: configurazione degli schemi di contenuto.
3
+ * In v0.4.0 ogni Seed genera una tabella `content_{slug}` con colonne reali.
4
+ * `branch.alias` è il nome della colonna SQL.
5
+ */
6
+ import type { Seed } from './types.js';
7
+ /**
8
+ * Registro globale dei Seed.
9
+ * In produzione, questo viene popolato dalle definizioni dell'utente.
10
+ */
11
+ export declare const SEED_REGISTRY: Record<string, Seed>;
12
+ /**
13
+ * Registra un set di Seed nel registro globale.
14
+ */
15
+ export declare function registerSeeds(seeds: Seed[]): void;
16
+ /**
17
+ * Ritorna il Seed per lo slug dato, o null se non esiste.
18
+ */
19
+ export declare function getSeed(slug: string): Seed | null;
20
+ //# sourceMappingURL=seeds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seeds.d.ts","sourceRoot":"","sources":["../../src/engine/seeds.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAM,CAAA;AAErD;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,EAAE,QAI1C;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAEjD"}
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ /**
4
+ * Registro globale dei Seed.
5
+ * In produzione, questo viene popolato dalle definizioni dell'utente.
6
+ */
7
+ export const SEED_REGISTRY = {};
8
+ /**
9
+ * Registra un set di Seed nel registro globale.
10
+ */
11
+ export function registerSeeds(seeds) {
12
+ seeds.forEach(seed => {
13
+ SEED_REGISTRY[seed.slug] = seed;
14
+ });
15
+ }
16
+ /**
17
+ * Ritorna il Seed per lo slug dato, o null se non esiste.
18
+ */
19
+ export function getSeed(slug) {
20
+ return SEED_REGISTRY[slug] ?? null;
21
+ }
@@ -0,0 +1,20 @@
1
+ import type { Branch } from './types.js';
2
+ /**
3
+ * Serializes a value for writing to the DB.
4
+ * boolean → 0/1 | date → Unix timestamp | json/tags/richtext/repeater → JSON string
5
+ *
6
+ * @param branch The branch definition.
7
+ * @param value The value to serialize.
8
+ * @returns The serialized DB value (string, number, or null).
9
+ */
10
+ export declare function serializeForDb(branch: Branch, value: unknown): string | number | null;
11
+ /**
12
+ * Deserializes a value read from the DB to its API/JS representation.
13
+ * 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object/array
14
+ *
15
+ * @param branch The branch definition.
16
+ * @param value The raw database value.
17
+ * @returns The deserialized value.
18
+ */
19
+ export declare function deserializeFromDb(branch: Branch, value: unknown): unknown;
20
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/engine/serialize.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAEV,MAAM,EAOP,MAAM,YAAY,CAAC;AAmEpB;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAsCrF;AAGD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAmCzE"}