@beechcms/core 0.8.0 → 0.8.1
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/dist/.tsbuildinfo +1 -1
- package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -1
- package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -1
- package/dist/automations-grammar-words.d.ts +2 -0
- package/dist/automations-grammar-words.d.ts.map +1 -0
- package/dist/automations-grammar-words.js +8 -0
- package/dist/automations.repository.interface.d.ts +19 -0
- package/dist/automations.repository.interface.d.ts.map +1 -0
- package/dist/automations.repository.interface.js +3 -0
- package/dist/automations.runner.interface.d.ts +10 -0
- package/dist/automations.runner.interface.d.ts.map +1 -0
- package/dist/automations.runner.interface.js +3 -0
- package/dist/automations.runner.stub.d.ts +5 -0
- package/dist/automations.runner.stub.d.ts.map +1 -0
- package/dist/automations.runner.stub.js +7 -0
- package/dist/automations.types.d.ts +75 -0
- package/dist/automations.types.d.ts.map +1 -0
- package/dist/automations.types.js +3 -0
- package/dist/clock.d.ts +25 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +11 -0
- package/dist/common/canonical-json.d.ts +30 -0
- package/dist/common/canonical-json.d.ts.map +1 -0
- package/dist/common/canonical-json.js +89 -0
- package/dist/content/content.repository.d.ts +2 -2
- package/dist/content/content.repository.d.ts.map +1 -1
- package/dist/content/deletion-ledger.d.ts +39 -0
- package/dist/content/deletion-ledger.d.ts.map +1 -0
- package/dist/content/deletion-ledger.js +3 -0
- package/dist/content-scan.repository.d.ts +9 -0
- package/dist/content-scan.repository.d.ts.map +1 -0
- package/dist/content-scan.repository.js +3 -0
- package/dist/content.repository.d.ts +247 -0
- package/dist/content.repository.d.ts.map +1 -0
- package/dist/content.repository.js +59 -0
- package/dist/dashboard-layout.d.ts +154 -0
- package/dist/dashboard-layout.d.ts.map +1 -0
- package/dist/dashboard-layout.js +186 -0
- package/dist/dashboard-layout.repository.d.ts +18 -0
- package/dist/dashboard-layout.repository.d.ts.map +1 -0
- package/dist/dashboard-layout.repository.js +3 -0
- package/dist/dashboard-permissions.d.ts +3 -0
- package/dist/dashboard-permissions.d.ts.map +1 -0
- package/dist/dashboard-permissions.js +11 -0
- package/dist/dashboard-scopes.d.ts +12 -0
- package/dist/dashboard-scopes.d.ts.map +1 -0
- package/dist/dashboard-scopes.js +30 -0
- package/dist/define-seed.d.ts +3 -0
- package/dist/define-seed.d.ts.map +1 -0
- package/dist/define-seed.js +5 -0
- package/dist/demo-data.repository.d.ts +5 -0
- package/dist/demo-data.repository.d.ts.map +1 -0
- package/dist/demo-data.repository.js +3 -0
- package/dist/email.provider.d.ts +32 -0
- package/dist/email.provider.d.ts.map +1 -0
- package/dist/email.provider.js +4 -0
- package/dist/engine/ddl/ddl.d.ts +216 -0
- package/dist/engine/ddl/ddl.d.ts.map +1 -0
- package/dist/engine/ddl/ddl.js +574 -0
- package/dist/engine/ddl/schema-mutator.d.ts +26 -0
- package/dist/engine/ddl/schema-mutator.d.ts.map +1 -0
- package/dist/engine/ddl/schema-mutator.js +3 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts +40 -0
- package/dist/engine/ddl/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/engine/ddl/seed-ddl-destructive.js +149 -0
- package/dist/engine/ddl/seed-ddl.d.ts +43 -0
- package/dist/engine/ddl/seed-ddl.d.ts.map +1 -0
- package/dist/engine/ddl/seed-ddl.js +115 -0
- package/dist/engine/introspection/introspection.d.ts +101 -0
- package/dist/engine/introspection/introspection.d.ts.map +1 -0
- package/dist/engine/introspection/introspection.js +127 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts +73 -0
- package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -0
- package/dist/engine/introspection/schema-fingerprint.js +97 -0
- package/dist/engine/introspection.d.ts +101 -0
- package/dist/engine/introspection.d.ts.map +1 -0
- package/dist/engine/introspection.js +127 -0
- package/dist/engine/localization/localization.d.ts +127 -0
- package/dist/engine/localization/localization.d.ts.map +1 -0
- package/dist/engine/localization/localization.js +212 -0
- package/dist/engine/localization.d.ts +127 -0
- package/dist/engine/localization.d.ts.map +1 -0
- package/dist/engine/localization.js +212 -0
- package/dist/engine/privacy/policies.d.ts +58 -0
- package/dist/engine/privacy/policies.d.ts.map +1 -0
- package/dist/engine/privacy/policies.js +185 -0
- package/dist/engine/privacy/privacy.service.d.ts +65 -0
- package/dist/engine/privacy/privacy.service.d.ts.map +1 -0
- package/dist/engine/privacy/privacy.service.js +114 -0
- package/dist/engine/privacy.service.d.ts.map +1 -1
- package/dist/engine/query/query.d.ts +23 -0
- package/dist/engine/query/query.d.ts.map +1 -0
- package/dist/engine/query/query.js +301 -0
- package/dist/engine/query/relations.d.ts +28 -0
- package/dist/engine/query/relations.d.ts.map +1 -0
- package/dist/engine/query/relations.js +25 -0
- package/dist/engine/query/serialize.d.ts +22 -0
- package/dist/engine/query/serialize.d.ts.map +1 -0
- package/dist/engine/query/serialize.js +175 -0
- package/dist/engine/schema-fingerprint.d.ts +73 -0
- package/dist/engine/schema-fingerprint.d.ts.map +1 -0
- package/dist/engine/schema-fingerprint.js +97 -0
- package/dist/engine/seed-registry.d.ts.map +1 -1
- package/dist/engine/seeds/define-seed.d.ts +3 -0
- package/dist/engine/seeds/define-seed.d.ts.map +1 -0
- package/dist/engine/seeds/define-seed.js +5 -0
- package/dist/engine/seeds/seed-registry.d.ts +71 -0
- package/dist/engine/seeds/seed-registry.d.ts.map +1 -0
- package/dist/engine/seeds/seed-registry.js +167 -0
- package/dist/engine/seeds/seed-types-generator.d.ts +25 -0
- package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -0
- package/dist/engine/seeds/seed-types-generator.js +117 -0
- package/dist/engine/seeds/seed-validation.d.ts +30 -0
- package/dist/engine/seeds/seed-validation.d.ts.map +1 -0
- package/dist/engine/seeds/seed-validation.js +364 -0
- package/dist/engine/seeds/seeds.d.ts +20 -0
- package/dist/engine/seeds/seeds.d.ts.map +1 -0
- package/dist/engine/seeds/seeds.js +21 -0
- package/dist/engine/validation/emptiness.d.ts +12 -0
- package/dist/engine/validation/emptiness.d.ts.map +1 -0
- package/dist/engine/validation/emptiness.js +33 -0
- package/dist/engine/validation/index.d.ts +1 -1
- package/dist/engine/validation/index.d.ts.map +1 -1
- package/dist/engine.d.ts +144 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +741 -0
- package/dist/file-types.d.ts +13 -0
- package/dist/file-types.d.ts.map +1 -0
- package/dist/file-types.js +85 -0
- package/dist/hooks.d.ts +29 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +3 -0
- package/dist/id-generator.d.ts +26 -0
- package/dist/id-generator.d.ts.map +1 -0
- package/dist/id-generator.js +12 -0
- package/dist/idempotency.repository.d.ts +13 -0
- package/dist/idempotency.repository.d.ts.map +1 -0
- package/dist/idempotency.repository.js +3 -0
- package/dist/kanban-position.repository.d.ts +19 -0
- package/dist/kanban-position.repository.d.ts.map +1 -0
- package/dist/kanban-position.repository.js +3 -0
- package/dist/kanban.d.ts +75 -0
- package/dist/kanban.d.ts.map +1 -0
- package/dist/kanban.js +82 -0
- package/dist/layout-permissions.d.ts +3 -0
- package/dist/layout-permissions.d.ts.map +1 -0
- package/dist/layout-permissions.js +11 -0
- package/dist/media/image-transformer.d.ts +34 -0
- package/dist/media/image-transformer.d.ts.map +1 -0
- package/dist/media/image-transformer.js +3 -0
- package/dist/media/media-transform.d.ts +88 -0
- package/dist/media/media-transform.d.ts.map +1 -0
- package/dist/media/media-transform.js +193 -0
- package/dist/media.repository.d.ts +61 -0
- package/dist/media.repository.d.ts.map +1 -0
- package/dist/media.repository.js +3 -0
- package/dist/notifications/notification-service.d.ts +1 -1
- package/dist/oauth/pkce.d.ts +1 -1
- package/dist/oauth/role-guard.d.ts.map +1 -1
- package/dist/oauth/scopes.d.ts +1 -1
- package/dist/oauth/scopes.d.ts.map +1 -1
- package/dist/policies.d.ts +10 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +33 -0
- package/dist/queue/queue.stub.d.ts.map +1 -1
- package/dist/queue/scheduler.stub.d.ts.map +1 -1
- package/dist/queue.interface.d.ts +39 -0
- package/dist/queue.interface.d.ts.map +1 -0
- package/dist/queue.interface.js +3 -0
- package/dist/queue.stub.d.ts +6 -0
- package/dist/queue.stub.d.ts.map +1 -0
- package/dist/queue.stub.js +8 -0
- package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -1
- package/dist/rbac/evaluate.d.ts +57 -0
- package/dist/rbac/evaluate.d.ts.map +1 -0
- package/dist/rbac/evaluate.js +113 -0
- package/dist/rbac/invitation.repository.d.ts +66 -0
- package/dist/rbac/invitation.repository.d.ts.map +1 -0
- package/dist/rbac/invitation.repository.js +3 -0
- package/dist/rbac/permissions.d.ts +39 -0
- package/dist/rbac/permissions.d.ts.map +1 -0
- package/dist/rbac/permissions.js +43 -0
- package/dist/rbac/types.d.ts +118 -0
- package/dist/rbac/types.d.ts.map +1 -0
- package/dist/rbac/types.js +3 -0
- package/dist/relations.d.ts +28 -0
- package/dist/relations.d.ts.map +1 -0
- package/dist/relations.js +25 -0
- package/dist/richtext-render.d.ts +11 -0
- package/dist/richtext-render.d.ts.map +1 -0
- package/dist/richtext-render.js +87 -0
- package/dist/richtext.d.ts +11 -0
- package/dist/richtext.d.ts.map +1 -0
- package/dist/richtext.js +13 -0
- package/dist/scheduler.interface.d.ts +4 -0
- package/dist/scheduler.interface.d.ts.map +1 -0
- package/dist/scheduler.interface.js +3 -0
- package/dist/scheduler.stub.d.ts +5 -0
- package/dist/scheduler.stub.d.ts.map +1 -0
- package/dist/scheduler.stub.js +7 -0
- package/dist/schema/canonical.d.ts +15 -0
- package/dist/schema/canonical.d.ts.map +1 -0
- package/dist/schema/canonical.js +37 -0
- package/dist/schema/define.d.ts +42 -0
- package/dist/schema/define.d.ts.map +1 -0
- package/dist/schema/define.js +56 -0
- package/dist/schema/emit.d.ts +14 -0
- package/dist/schema/emit.d.ts.map +1 -0
- package/dist/schema/emit.js +43 -0
- package/dist/schema/index.d.ts +12 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +13 -0
- package/dist/schema/manifest-seeds.d.ts +12 -0
- package/dist/schema/manifest-seeds.d.ts.map +1 -0
- package/dist/schema/manifest-seeds.js +48 -0
- package/dist/schema/manifest-validation.d.ts +13 -0
- package/dist/schema/manifest-validation.d.ts.map +1 -0
- package/dist/schema/manifest-validation.js +41 -0
- package/dist/schema/manifest.types.d.ts +54 -0
- package/dist/schema/manifest.types.d.ts.map +1 -0
- package/dist/schema/manifest.types.js +7 -0
- package/dist/schema-mutator.d.ts +26 -0
- package/dist/schema-mutator.d.ts.map +1 -0
- package/dist/schema-mutator.js +3 -0
- package/dist/seed-ddl-destructive.d.ts +40 -0
- package/dist/seed-ddl-destructive.d.ts.map +1 -0
- package/dist/seed-ddl-destructive.js +149 -0
- package/dist/seed-ddl.d.ts +40 -0
- package/dist/seed-ddl.d.ts.map +1 -0
- package/dist/seed-ddl.js +100 -0
- package/dist/seed-layout.d.ts +145 -0
- package/dist/seed-layout.d.ts.map +1 -0
- package/dist/seed-layout.js +192 -0
- package/dist/seed-layout.repository.d.ts +22 -0
- package/dist/seed-layout.repository.d.ts.map +1 -0
- package/dist/seed-layout.repository.js +3 -0
- package/dist/seed-registry.d.ts +71 -0
- package/dist/seed-registry.d.ts.map +1 -0
- package/dist/seed-registry.js +143 -0
- package/dist/seed-types-generator.d.ts +1 -1
- package/dist/seed-types-generator.d.ts.map +1 -1
- package/dist/seed-validation.d.ts +23 -0
- package/dist/seed-validation.d.ts.map +1 -0
- package/dist/seed-validation.js +224 -0
- package/dist/seed.repository.d.ts +37 -0
- package/dist/seed.repository.d.ts.map +1 -0
- package/dist/seed.repository.js +3 -0
- package/dist/seeds.d.ts +20 -0
- package/dist/seeds.d.ts.map +1 -0
- package/dist/seeds.js +21 -0
- package/dist/site-settings.repository.d.ts +16 -0
- package/dist/site-settings.repository.d.ts.map +1 -0
- package/dist/site-settings.repository.js +3 -0
- package/dist/slug-utils.d.ts +18 -0
- package/dist/slug-utils.d.ts.map +1 -0
- package/dist/slug-utils.js +32 -0
- package/dist/storage.d.ts +46 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +3 -0
- package/dist/transfer/csv.d.ts +34 -0
- package/dist/transfer/csv.d.ts.map +1 -0
- package/dist/transfer/csv.js +135 -0
- package/dist/transfer/flat-seed.d.ts +13 -0
- package/dist/transfer/flat-seed.d.ts.map +1 -0
- package/dist/transfer/flat-seed.js +45 -0
- package/dist/transfer/index.d.ts +7 -0
- package/dist/transfer/index.d.ts.map +1 -0
- package/dist/transfer/index.js +8 -0
- package/dist/transfer/ndjson.d.ts +25 -0
- package/dist/transfer/ndjson.d.ts.map +1 -0
- package/dist/transfer/ndjson.js +62 -0
- package/dist/transfer/row-mapping.d.ts +36 -0
- package/dist/transfer/row-mapping.d.ts.map +1 -0
- package/dist/transfer/row-mapping.js +90 -0
- package/dist/transfer/transfer.constants.d.ts +20 -0
- package/dist/transfer/transfer.constants.d.ts.map +1 -0
- package/dist/transfer/transfer.constants.js +21 -0
- package/dist/transfer/transfer.types.d.ts +46 -0
- package/dist/transfer/transfer.types.d.ts.map +1 -0
- package/dist/transfer/transfer.types.js +6 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/validation.d.ts +50 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +660 -0
- package/dist/webhook-validation.d.ts +3 -0
- package/dist/webhook-validation.d.ts.map +1 -0
- package/dist/webhook-validation.js +6 -0
- package/package.json +4 -4
|
@@ -0,0 +1,364 @@
|
|
|
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/ddl.js';
|
|
5
|
+
import { LOCALIZABLE_BRANCH_TYPES } from '../localization/localization.js';
|
|
6
|
+
import { resolveClassification } from '../privacy/policies.js';
|
|
7
|
+
import { sortSeedsByDependencies } from './seed-registry.js';
|
|
8
|
+
import { SQL_RESERVED_WORDS } from '../sql-reserved-words.js';
|
|
9
|
+
const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
|
|
10
|
+
/**
|
|
11
|
+
* Allowed charset for a branch alias. An alias becomes a raw SQL column name in
|
|
12
|
+
* CREATE TABLE / ADD COLUMN / CREATE INDEX and in SELECT/WHERE/ORDER BY clauses
|
|
13
|
+
* (see engine.ts), so it MUST be restricted to a safe identifier charset to
|
|
14
|
+
* prevent DDL/query injection. Exported so the seeds API rename route reuses the
|
|
15
|
+
* exact same guard — do not inline a divergent copy.
|
|
16
|
+
*/
|
|
17
|
+
export const BRANCH_ALIAS_RE = /^[a-z][a-zA-Z0-9_]*$/;
|
|
18
|
+
/**
|
|
19
|
+
* Allowed charset for a seed slug identifier.
|
|
20
|
+
* Must consist solely of lowercase ASCII letters, digits, and underscores.
|
|
21
|
+
* Exported so route handlers and validation functions reuse the exact same guard.
|
|
22
|
+
*/
|
|
23
|
+
export const SEED_SLUG_RE = /^[a-z0-9_]+$/;
|
|
24
|
+
export const SLUG_RE = SEED_SLUG_RE;
|
|
25
|
+
/**
|
|
26
|
+
* Pure, console-free, throw-free validation of a seed set.
|
|
27
|
+
* The single seed being created/edited should be validated in the context of
|
|
28
|
+
* the full active set: call validateSeedDefinitions([...otherActiveSeeds, edited]).
|
|
29
|
+
*/
|
|
30
|
+
export function validateSeedDefinitions(seeds) {
|
|
31
|
+
const result = [];
|
|
32
|
+
// ── Fatal 1: unknown relation targets ────────────────────────────────────
|
|
33
|
+
const slugSet = new Set(seeds.map(s => s.slug));
|
|
34
|
+
for (const seed of seeds) {
|
|
35
|
+
const messages = [];
|
|
36
|
+
for (const branch of seed.branches) {
|
|
37
|
+
if (branch.type === 'relation' && branch.targetSeed) {
|
|
38
|
+
if (!slugSet.has(branch.targetSeed)) {
|
|
39
|
+
messages.push(`branch '${branch.alias}' targets unknown seed '${branch.targetSeed}'`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (messages.length > 0)
|
|
44
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
45
|
+
}
|
|
46
|
+
// ── Fatal 2: multi-relation with SET NULL ────────────────────────────────
|
|
47
|
+
for (const seed of seeds) {
|
|
48
|
+
const messages = [];
|
|
49
|
+
for (const branch of seed.branches) {
|
|
50
|
+
if (branch.type === 'relation' && branch.multiple === true && branch.onDelete === 'SET NULL') {
|
|
51
|
+
messages.push(`Branch '${branch.alias}': multi-relations cannot use ON DELETE SET NULL. ` +
|
|
52
|
+
`Use 'CASCADE' or 'RESTRICT'.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (messages.length > 0)
|
|
56
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
57
|
+
}
|
|
58
|
+
// ── Fatal 3: junction table name collisions / length ─────────────────────
|
|
59
|
+
{
|
|
60
|
+
const junctionNames = new Set();
|
|
61
|
+
for (const seed of seeds) {
|
|
62
|
+
const messages = [];
|
|
63
|
+
for (const branch of seed.branches) {
|
|
64
|
+
if (branch.type !== 'relation' || branch.multiple !== true)
|
|
65
|
+
continue;
|
|
66
|
+
const name = `rel_${seed.slug}_${branch.alias}`;
|
|
67
|
+
if (name.length > 256) {
|
|
68
|
+
messages.push(`Junction table name '${name}' exceeds 256 characters (${name.length})`);
|
|
69
|
+
}
|
|
70
|
+
if (junctionNames.has(name)) {
|
|
71
|
+
messages.push(`Junction table name collision: '${name}' already used by another seed/branch`);
|
|
72
|
+
}
|
|
73
|
+
junctionNames.add(name);
|
|
74
|
+
}
|
|
75
|
+
if (messages.length > 0)
|
|
76
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ── Fatal 4: dependency cycles ────────────────────────────────────────────
|
|
80
|
+
const hasUnknownTargets = result.some(e => e.fatal);
|
|
81
|
+
if (!hasUnknownTargets) {
|
|
82
|
+
try {
|
|
83
|
+
sortSeedsByDependencies(seeds);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
87
|
+
const cycleMatch = msg.match(/Cyclic dependency detected among seeds: (.*)/);
|
|
88
|
+
if (cycleMatch) {
|
|
89
|
+
const cycleSlugs = cycleMatch[1].split(', ');
|
|
90
|
+
for (const slug of cycleSlugs) {
|
|
91
|
+
result.push({ slug, messages: [msg], fatal: true });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
result.push({ slug: '<graph>', messages: [msg], fatal: true });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ── Fatal 5: branch id format / uniqueness ───────────────────────────────
|
|
100
|
+
for (const seed of seeds) {
|
|
101
|
+
const messages = [];
|
|
102
|
+
const idsInSeed = new Set();
|
|
103
|
+
for (const branch of seed.branches) {
|
|
104
|
+
if (!branch.id || !BRANCH_ID_RE.test(branch.id)) {
|
|
105
|
+
messages.push(`branch '${branch.alias}' has invalid id '${branch.id}'. Expected format ^br_[A-Za-z0-9]+$.`);
|
|
106
|
+
}
|
|
107
|
+
else if (idsInSeed.has(branch.id)) {
|
|
108
|
+
messages.push(`duplicate branch id '${branch.id}'`);
|
|
109
|
+
}
|
|
110
|
+
if (branch.id)
|
|
111
|
+
idsInSeed.add(branch.id);
|
|
112
|
+
// Alias becomes a raw SQL column name — reject anything outside the safe
|
|
113
|
+
// identifier charset before it can reach DDL/query string interpolation.
|
|
114
|
+
if (typeof branch.alias !== 'string' || !BRANCH_ALIAS_RE.test(branch.alias)) {
|
|
115
|
+
messages.push(`branch alias '${branch.alias}' is invalid. Expected format ${BRANCH_ALIAS_RE.source} ` +
|
|
116
|
+
`(lowercase letter followed by alphanumeric characters or underscores).`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (messages.length > 0)
|
|
120
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
121
|
+
}
|
|
122
|
+
// ── Fatal 6: reserved aliases ─────────────────────────────────────────────
|
|
123
|
+
for (const seed of seeds) {
|
|
124
|
+
const messages = [];
|
|
125
|
+
for (const branch of seed.branches) {
|
|
126
|
+
if (SYSTEM_COLUMNS.has(branch.alias)) {
|
|
127
|
+
messages.push(`branch '${branch.alias}' collides with reserved system column. ` +
|
|
128
|
+
`System columns (id, slug, status, created_at, updated_at) cannot be used as branch aliases.`);
|
|
129
|
+
}
|
|
130
|
+
else if (AUTOMATION_RESERVED_WORDS.has(branch.alias)) {
|
|
131
|
+
messages.push(`branch '${branch.alias}' uses reserved alias. ` +
|
|
132
|
+
`This word is used by the automation template grammar.`);
|
|
133
|
+
}
|
|
134
|
+
else if (SQL_RESERVED_WORDS.has(branch.alias.toLowerCase())) {
|
|
135
|
+
messages.push(`branch '${branch.alias}' uses SQL reserved keyword. ` +
|
|
136
|
+
`Please pick a different alias to prevent database errors.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (messages.length > 0)
|
|
140
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
141
|
+
}
|
|
142
|
+
// ── Fatal 10: repeater sub-field constraints ─────────────────────────────
|
|
143
|
+
{
|
|
144
|
+
const REPEATER_DISALLOWED_SUBTYPES = new Set(['repeater', 'relation', 'file']);
|
|
145
|
+
for (const seed of seeds) {
|
|
146
|
+
const messages = [];
|
|
147
|
+
for (const branch of seed.branches) {
|
|
148
|
+
if (branch.type !== 'repeater')
|
|
149
|
+
continue;
|
|
150
|
+
const subIds = new Set();
|
|
151
|
+
const subAliases = new Set();
|
|
152
|
+
for (const sub of branch.fields ?? []) {
|
|
153
|
+
if (REPEATER_DISALLOWED_SUBTYPES.has(sub.type)) {
|
|
154
|
+
messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has disallowed type '${sub.type}'. ` +
|
|
155
|
+
`Repeater sub-fields cannot be 'repeater', 'relation', or 'file' in v1.`);
|
|
156
|
+
}
|
|
157
|
+
if (!sub.id || !BRANCH_ID_RE.test(sub.id)) {
|
|
158
|
+
messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has invalid id '${sub.id}'. ` +
|
|
159
|
+
`Expected format ^br_[A-Za-z0-9]+$.`);
|
|
160
|
+
}
|
|
161
|
+
else if (subIds.has(sub.id)) {
|
|
162
|
+
messages.push(`branch '${branch.alias}': duplicate sub-field id '${sub.id}'`);
|
|
163
|
+
}
|
|
164
|
+
if (sub.id)
|
|
165
|
+
subIds.add(sub.id);
|
|
166
|
+
if (typeof sub.alias !== 'string' || !BRANCH_ALIAS_RE.test(sub.alias)) {
|
|
167
|
+
messages.push(`branch '${branch.alias}': sub-field alias '${sub.alias}' is invalid. ` +
|
|
168
|
+
`Expected format ${BRANCH_ALIAS_RE.source}.`);
|
|
169
|
+
}
|
|
170
|
+
else if (subAliases.has(sub.alias)) {
|
|
171
|
+
messages.push(`branch '${branch.alias}': duplicate sub-field alias '${sub.alias}'`);
|
|
172
|
+
}
|
|
173
|
+
subAliases.add(sub.alias);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (messages.length > 0)
|
|
177
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ── Warning 7: duplicate slug ─────────────────────────────────────────────
|
|
181
|
+
{
|
|
182
|
+
const slugsSeen = new Set();
|
|
183
|
+
for (const seed of seeds) {
|
|
184
|
+
const messages = [];
|
|
185
|
+
if (slugsSeen.has(seed.slug)) {
|
|
186
|
+
messages.push(`duplicate slug "${seed.slug}" — each seed must have a unique slug`);
|
|
187
|
+
}
|
|
188
|
+
slugsSeen.add(seed.slug);
|
|
189
|
+
if (messages.length > 0)
|
|
190
|
+
result.push({ slug: seed.slug, messages, fatal: false });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ── Warning 8: duplicate branch alias within a seed ───────────────────────
|
|
194
|
+
for (const seed of seeds) {
|
|
195
|
+
const messages = [];
|
|
196
|
+
const aliasesSeen = new Set();
|
|
197
|
+
for (const branch of seed.branches) {
|
|
198
|
+
if (aliasesSeen.has(branch.alias)) {
|
|
199
|
+
messages.push(`duplicate branch alias "${branch.alias}"`);
|
|
200
|
+
}
|
|
201
|
+
aliasesSeen.add(branch.alias);
|
|
202
|
+
}
|
|
203
|
+
if (messages.length > 0)
|
|
204
|
+
result.push({ slug: seed.slug, messages, fatal: false });
|
|
205
|
+
}
|
|
206
|
+
// ── Warning 9: displayNameAlias not in branches ───────────────────────────
|
|
207
|
+
for (const seed of seeds) {
|
|
208
|
+
const allAliases = new Set(seed.branches.map(b => b.alias));
|
|
209
|
+
if (!allAliases.has(seed.displayNameAlias)) {
|
|
210
|
+
result.push({
|
|
211
|
+
slug: seed.slug,
|
|
212
|
+
messages: [`displayNameAlias "${seed.displayNameAlias}" not found in branches`],
|
|
213
|
+
fatal: false,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ── Fatal 11 / Warning 10: repeater cardinality bounds ───────────────────
|
|
218
|
+
{
|
|
219
|
+
for (const seed of seeds) {
|
|
220
|
+
const fatals = [];
|
|
221
|
+
const warnings = [];
|
|
222
|
+
for (const branch of seed.branches) {
|
|
223
|
+
const hasBounds = branch.minItems !== undefined || branch.maxItems !== undefined;
|
|
224
|
+
if (!hasBounds)
|
|
225
|
+
continue;
|
|
226
|
+
if (branch.type !== 'repeater') {
|
|
227
|
+
warnings.push(`branch '${branch.alias}': minItems/maxItems are ignored on type ` +
|
|
228
|
+
`'${branch.type}' (repeater-only).`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
for (const [key, val] of [['minItems', branch.minItems], ['maxItems', branch.maxItems]]) {
|
|
232
|
+
if (val !== undefined && (!Number.isInteger(val) || val < 0)) {
|
|
233
|
+
fatals.push(`branch '${branch.alias}': ${key} must be a non-negative integer (got ${val}).`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (Number.isInteger(branch.minItems) && Number.isInteger(branch.maxItems) &&
|
|
237
|
+
branch.minItems > branch.maxItems) {
|
|
238
|
+
fatals.push(`branch '${branch.alias}': minItems (${branch.minItems}) must be <= maxItems (${branch.maxItems}).`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (fatals.length > 0)
|
|
242
|
+
result.push({ slug: seed.slug, messages: fatals, fatal: true });
|
|
243
|
+
if (warnings.length > 0)
|
|
244
|
+
result.push({ slug: seed.slug, messages: warnings, fatal: false });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// ── Fatal 12: displayNameAlias format validation ───────────────────────────
|
|
248
|
+
for (const seed of seeds) {
|
|
249
|
+
if (typeof seed.displayNameAlias !== 'string' || !BRANCH_ALIAS_RE.test(seed.displayNameAlias)) {
|
|
250
|
+
result.push({
|
|
251
|
+
slug: seed.slug,
|
|
252
|
+
messages: [
|
|
253
|
+
`displayNameAlias '${seed.displayNameAlias}' is invalid. Expected format ${BRANCH_ALIAS_RE.source} ` +
|
|
254
|
+
`(lowercase letter followed by alphanumeric characters or underscores).`,
|
|
255
|
+
],
|
|
256
|
+
fatal: true,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else if (SYSTEM_COLUMNS.has(seed.displayNameAlias)) {
|
|
260
|
+
result.push({
|
|
261
|
+
slug: seed.slug,
|
|
262
|
+
messages: [
|
|
263
|
+
`displayNameAlias '${seed.displayNameAlias}' collides with reserved system column. ` +
|
|
264
|
+
`System columns (id, slug, status, created_at, updated_at) cannot be used as displayNameAlias.`,
|
|
265
|
+
],
|
|
266
|
+
fatal: true,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
else if (SQL_RESERVED_WORDS.has(seed.displayNameAlias.toLowerCase())) {
|
|
270
|
+
result.push({
|
|
271
|
+
slug: seed.slug,
|
|
272
|
+
messages: [
|
|
273
|
+
`displayNameAlias '${seed.displayNameAlias}' uses SQL reserved keyword. ` +
|
|
274
|
+
`Please pick a different alias to prevent database errors.`,
|
|
275
|
+
],
|
|
276
|
+
fatal: true,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// ── Fatal 13: retentionDays validation ─────────────────────────────────────
|
|
281
|
+
for (const seed of seeds) {
|
|
282
|
+
if (seed.retentionDays !== undefined && (!Number.isInteger(seed.retentionDays) || seed.retentionDays <= 0)) {
|
|
283
|
+
result.push({
|
|
284
|
+
slug: seed.slug,
|
|
285
|
+
messages: [
|
|
286
|
+
`retentionDays must be a positive integer >= 1 (got ${seed.retentionDays}).`,
|
|
287
|
+
],
|
|
288
|
+
fatal: true,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ── Fatal 14: publicEdit validation ─────────────────────────────────────────
|
|
293
|
+
for (const seed of seeds) {
|
|
294
|
+
const messages = [];
|
|
295
|
+
for (const branch of seed.branches) {
|
|
296
|
+
if (branch.policies?.publicEdit !== undefined && typeof branch.policies.publicEdit !== 'boolean') {
|
|
297
|
+
messages.push(`branch '${branch.alias}': policies.publicEdit must be a boolean`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (messages.length > 0)
|
|
301
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
302
|
+
}
|
|
303
|
+
// ── Fatal 15: seed slug format validation ─────────────────────────────────
|
|
304
|
+
for (const seed of seeds) {
|
|
305
|
+
if (typeof seed.slug !== 'string' || !SEED_SLUG_RE.test(seed.slug)) {
|
|
306
|
+
result.push({
|
|
307
|
+
slug: seed.slug,
|
|
308
|
+
messages: [
|
|
309
|
+
`slug '${seed.slug}' is invalid. Expected format ${SEED_SLUG_RE.source} ` +
|
|
310
|
+
`(lowercase letters, numbers, and underscores only).`,
|
|
311
|
+
],
|
|
312
|
+
fatal: true,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// ── Fatal 16: softDelete type validation ────────────────────────────────────
|
|
317
|
+
for (const seed of seeds) {
|
|
318
|
+
if (seed.softDelete !== undefined && typeof seed.softDelete !== 'boolean') {
|
|
319
|
+
result.push({
|
|
320
|
+
slug: seed.slug,
|
|
321
|
+
messages: [`softDelete must be a boolean (got ${typeof seed.softDelete}).`],
|
|
322
|
+
fatal: true,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ── Fatal 17: field-level localization constraints ─────────────────────────
|
|
327
|
+
// A localized value is a readable locale dictionary. Encrypted or hashed storage turns it into an
|
|
328
|
+
// opaque blob: per-locale extraction would be meaningless and would break decryption. Repeater
|
|
329
|
+
// sub-fields are excluded by domain rule, whatever their type.
|
|
330
|
+
for (const seed of seeds) {
|
|
331
|
+
const messages = [];
|
|
332
|
+
for (const branch of seed.branches) {
|
|
333
|
+
if (branch.localized !== undefined && typeof branch.localized !== 'boolean') {
|
|
334
|
+
messages.push(`branch '${branch.alias}': localized must be a boolean`);
|
|
335
|
+
}
|
|
336
|
+
else if (branch.localized === true) {
|
|
337
|
+
if (!LOCALIZABLE_BRANCH_TYPES.has(branch.type)) {
|
|
338
|
+
messages.push(`branch '${branch.alias}': localized is only supported on text, richtext and json branches ` +
|
|
339
|
+
`(got '${branch.type}').`);
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
const { classification, storage } = resolveClassification(branch);
|
|
343
|
+
if (storage !== 'plain') {
|
|
344
|
+
messages.push(`branch '${branch.alias}': localized cannot be combined with '${classification}' classification ` +
|
|
345
|
+
`(values are stored ${storage === 'encrypt' ? 'encrypted' : 'hashed'}).`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const sub of branch.fields ?? []) {
|
|
350
|
+
if (sub.localized !== undefined && sub.localized !== false) {
|
|
351
|
+
messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' cannot be localized. ` +
|
|
352
|
+
`Repeater sub-fields are never localized.`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (messages.length > 0)
|
|
357
|
+
result.push({ slug: seed.slug, messages, fatal: true });
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
/** Convenience: true iff no fatal issues. */
|
|
362
|
+
export function isSeedSetValid(seeds) {
|
|
363
|
+
return validateSeedDefinitions(seeds).every(i => !i.fatal);
|
|
364
|
+
}
|
|
@@ -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/seeds.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC;;;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,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a value is effectively empty (e.g. null, undefined, empty string, empty array, or empty rich text).
|
|
3
|
+
*
|
|
4
|
+
* Shared by top-level required-field detection (`index.ts`) and repeater item validation
|
|
5
|
+
* (`schema-builders.ts`) so both tiers apply the same emptiness rule to required fields.
|
|
6
|
+
*
|
|
7
|
+
* @param value - The value to check.
|
|
8
|
+
* @param branchType - The type of the branch being checked.
|
|
9
|
+
* @returns True if effectively empty, false otherwise.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isEffectivelyEmpty(value: unknown, branchType?: string): boolean;
|
|
12
|
+
//# sourceMappingURL=emptiness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emptiness.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/emptiness.ts"],"names":[],"mappings":"AAOA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAY/E"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
import { isRichtextEnvelopeV1 } from '../../content/richtext/richtext.js';
|
|
4
|
+
import { cleanString, isPlainObject } from './primitives.js';
|
|
5
|
+
import { isRichtextDocEmpty } from './richtext-sanitizer.js';
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a value is effectively empty (e.g. null, undefined, empty string, empty array, or empty rich text).
|
|
8
|
+
*
|
|
9
|
+
* Shared by top-level required-field detection (`index.ts`) and repeater item validation
|
|
10
|
+
* (`schema-builders.ts`) so both tiers apply the same emptiness rule to required fields.
|
|
11
|
+
*
|
|
12
|
+
* @param value - The value to check.
|
|
13
|
+
* @param branchType - The type of the branch being checked.
|
|
14
|
+
* @returns True if effectively empty, false otherwise.
|
|
15
|
+
*/
|
|
16
|
+
export function isEffectivelyEmpty(value, branchType) {
|
|
17
|
+
if (value === null || value === undefined)
|
|
18
|
+
return true;
|
|
19
|
+
if (typeof value === 'string')
|
|
20
|
+
return cleanString(value).length === 0;
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return value.length === 0;
|
|
23
|
+
if (isPlainObject(value)) {
|
|
24
|
+
// Richtext envelope: delegate to the richtext emptiness check.
|
|
25
|
+
if (isRichtextEnvelopeV1(value))
|
|
26
|
+
return isRichtextDocEmpty(value.doc);
|
|
27
|
+
// Raw TipTap doc root: only delegate for richtext branches to avoid misclassifying generic json fields.
|
|
28
|
+
if (branchType === 'richtext' && value.type === 'doc')
|
|
29
|
+
return isRichtextDocEmpty(value);
|
|
30
|
+
return Object.keys(value).length === 0;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
@@ -91,7 +91,7 @@ export type ResolvedOptions = {
|
|
|
91
91
|
idGenerator: IIdGenerator | undefined;
|
|
92
92
|
};
|
|
93
93
|
/** Set of valid content status values. */
|
|
94
|
-
declare const STATUS_VALUES: readonly [
|
|
94
|
+
declare const STATUS_VALUES: readonly ['draft', 'review', 'published'];
|
|
95
95
|
/**
|
|
96
96
|
* Validates and sanitizes a seed payload against its schema definition.
|
|
97
97
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAOhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAErD;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb,0GAA0G;IAC1G,QAAQ,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,QAAQ,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC/B;;;OAGG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,+EAA+E;IAC/E,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,8FAA8F;IAC9F,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,4FAA4F;IAC5F,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,sEAAsE;IACtE,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,uFAAuF;IACvF,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC9B,2BAA2B,EAAE,OAAO,CAAA;IACpC,qBAAqB,EAAE,OAAO,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,YAAY,GAAG,SAAS,CAAA;CACtC,CAAA;AAKD,0CAA0C;AAC1C,QAAA,MAAM,aAAa,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAOhE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAErD;;;;;;GAMG;AAEH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAA;IACb,0GAA0G;IAC1G,QAAQ,EAAE,MAAM,CAAA;IAChB,kGAAkG;IAClG,QAAQ,EAAE,MAAM,CAAA;IAChB,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC/B;;;OAGG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAA;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,+EAA+E;IAC/E,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,8FAA8F;IAC9F,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,4FAA4F;IAC5F,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,sEAAsE;IACtE,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,uFAAuF;IACvF,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,OAAO,CAAA;IAClB,SAAS,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC9B,2BAA2B,EAAE,OAAO,CAAA;IACpC,qBAAqB,EAAE,OAAO,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,YAAY,GAAG,SAAS,CAAA;CACtC,CAAA;AAKD,0CAA0C;AAC1C,QAAA,MAAM,aAAa,YAAI,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAU,CAAA;AA+R/D;;;;;;;;;;;GAWG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,GAAE,0BAA+B,GACvC,yBAAyB,CAqD3B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAE5F"}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Botanical Engine: Schema Compiler + Query Builder.
|
|
3
|
+
*
|
|
4
|
+
* In v0.4.0 each Seed has a dedicated SQL table (`content_{slug}`) with
|
|
5
|
+
* real typed columns. This module generates the DDL and builds parameterized queries.
|
|
6
|
+
* It does not know about HTTP, auth, or UI — it's a pure TypeScript library.
|
|
7
|
+
*/
|
|
8
|
+
import type { Seed, Branch, SelectOptions, ParameterizedQuery } from './types.js';
|
|
9
|
+
export declare function indexableSearchBranches(seed: Seed): Branch[];
|
|
10
|
+
/**
|
|
11
|
+
* Generates `CREATE TABLE IF NOT EXISTS content_{slug}` with system columns
|
|
12
|
+
* + one column per Branch. Pure function: same Seed → same SQL.
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateCreateTable(seed: Seed): string;
|
|
15
|
+
/**
|
|
16
|
+
* Generates the drafts table `content_{slug}_drafts` for Seeds with `allowDrafts: true`.
|
|
17
|
+
* All branch columns are nullable (drafts are partial).
|
|
18
|
+
* Returns null if the Seed does not have `allowDrafts: true`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function generateDraftTable(seed: Seed): string | null;
|
|
21
|
+
/**
|
|
22
|
+
* Generates `ALTER TABLE content_{slug} ADD COLUMN {alias} {type}`.
|
|
23
|
+
* New columns are always nullable (SQLite limit on ALTER TABLE).
|
|
24
|
+
*/
|
|
25
|
+
export declare function generateAddColumn(seed: Seed, branch: Branch): string;
|
|
26
|
+
/**
|
|
27
|
+
* Generates B-tree indexes for status, created_at and every filterable Branch
|
|
28
|
+
* with an indexable type (text, number, date, boolean).
|
|
29
|
+
*/
|
|
30
|
+
export declare function generateIndexes(seed: Seed): string[];
|
|
31
|
+
/**
|
|
32
|
+
* Generates the FTS5 virtual table for indexable text/richtext Branches.
|
|
33
|
+
* Returns null if the Seed has no branches with search enabled.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateFtsTable(seed: Seed): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Generates the 3 SQLite triggers (insert/update/delete) that keep the FTS
|
|
38
|
+
* automatically synchronized — eliminates the need for manual syncFts.
|
|
39
|
+
* Returns an empty array if the Seed has no indexable branches.
|
|
40
|
+
*/
|
|
41
|
+
export declare function generateFtsTriggers(seed: Seed): string[];
|
|
42
|
+
/**
|
|
43
|
+
* Builds a parameterized SELECT on `content_{slug}`.
|
|
44
|
+
* Never uses json_extract — every column is a real column.
|
|
45
|
+
* Unknown columns in filters/orderBy are ignored (fail-closed).
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildSelectQuery(seed: Seed, options?: SelectOptions): ParameterizedQuery;
|
|
48
|
+
export interface SchemaColumn {
|
|
49
|
+
name: string;
|
|
50
|
+
sqlType: 'TEXT' | 'REAL' | 'INTEGER';
|
|
51
|
+
notNull: boolean;
|
|
52
|
+
isPk: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Returns the list of expected columns for a Seed's table.
|
|
56
|
+
* Used by `beech seed:load --diff` to compare current vs expected schema.
|
|
57
|
+
*/
|
|
58
|
+
export declare function getExpectedColumns(seed: Seed): SchemaColumn[];
|
|
59
|
+
/**
|
|
60
|
+
* Serializes a value for writing to the DB.
|
|
61
|
+
* boolean → 0/1 | date → Unix timestamp | json/asset-list → JSON string
|
|
62
|
+
*/
|
|
63
|
+
export declare function serializeForDb(branch: Branch, value: unknown): string | number | null;
|
|
64
|
+
/**
|
|
65
|
+
* Returns the junction table name for a many-to-many relation branch.
|
|
66
|
+
* Format: `rel_<seedSlug>_<branchAlias>`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function junctionTableName(seedSlug: string, branchAlias: string): string;
|
|
69
|
+
/**
|
|
70
|
+
* Generates `CREATE TABLE IF NOT EXISTS rel_<seed>_<alias>` for a multi-relation
|
|
71
|
+
* branch. The parent FK always cascades (parent lifecycle owns the edge);
|
|
72
|
+
* the target FK uses `branch.onDelete` (default CASCADE).
|
|
73
|
+
*
|
|
74
|
+
* @throws {Error} when called on a non-multi-relation branch
|
|
75
|
+
* @throws {Error} when `branch.targetSeed` is missing
|
|
76
|
+
*/
|
|
77
|
+
export declare function generateJunctionTable(seed: Seed, branch: Branch): string;
|
|
78
|
+
/**
|
|
79
|
+
* Generates the two B-tree indexes for a junction table:
|
|
80
|
+
* one on `parent_id` (list entries by parent) and one on `target_id`
|
|
81
|
+
* (cascade checks from target side).
|
|
82
|
+
*/
|
|
83
|
+
export declare function generateJunctionIndexes(seed: Seed, branch: Branch): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Generates the drafts junction table `rel_<seed>_<alias>_drafts` for a
|
|
86
|
+
* many-to-many relation branch. Note: no FK on `target_id` — targets may be
|
|
87
|
+
* deleted between draft save and publish; the FK would be wrong here.
|
|
88
|
+
* Returns null when `seed.allowDrafts` is falsy.
|
|
89
|
+
*/
|
|
90
|
+
export declare function generateJunctionDraftTable(seed: Seed, branch: Branch): string | null;
|
|
91
|
+
/**
|
|
92
|
+
* Returns every `DROP TABLE IF EXISTS` needed to fully remove a content type:
|
|
93
|
+
* the main `content_{slug}` table, its `fts_{slug}` virtual table, the
|
|
94
|
+
* `content_{slug}_drafts` mirror (when drafts are enabled), and each
|
|
95
|
+
* multi-relation junction table `rel_{slug}_{alias}` (+ its `_drafts` mirror).
|
|
96
|
+
*
|
|
97
|
+
* FTS triggers reference the main table; dropping the main table also drops
|
|
98
|
+
* its triggers in SQLite, but we drop the FTS table explicitly so no orphaned
|
|
99
|
+
* virtual table survives. Order: junction (children) → drafts → fts → main.
|
|
100
|
+
*/
|
|
101
|
+
export declare function generateDropTable(seed: Seed): string[];
|
|
102
|
+
/**
|
|
103
|
+
* Returns the `DROP COLUMN` statements for removing a single field.
|
|
104
|
+
*
|
|
105
|
+
* - For a multi-relation branch (no column on the parent table) it drops the
|
|
106
|
+
* junction table `rel_{slug}_{alias}` (+ `_drafts`) instead.
|
|
107
|
+
* - For a normal branch it drops the column from `content_{slug}` and, when
|
|
108
|
+
* drafts are enabled, the mirror column from `content_{slug}_drafts`.
|
|
109
|
+
* - For an orphan column (no matching branch in the definition) it falls back
|
|
110
|
+
* to a plain `ALTER TABLE … DROP COLUMN`, never touching drafts.
|
|
111
|
+
*
|
|
112
|
+
* Callers must rebuild FTS separately (planFtsRebuild) when the dropped column
|
|
113
|
+
* was searchable, since SQLite cannot ALTER an fts5 table's columns.
|
|
114
|
+
*/
|
|
115
|
+
export declare function generateDropColumn(seed: Seed, alias: string): string[];
|
|
116
|
+
/**
|
|
117
|
+
* Returns the `RENAME COLUMN` statements for renaming a field's alias while
|
|
118
|
+
* keeping the stable `branch.id`. Renames the column on `content_{slug}` and,
|
|
119
|
+
* when drafts are enabled, the mirror column on `content_{slug}_drafts`.
|
|
120
|
+
*
|
|
121
|
+
* For a multi-relation branch the alias is part of the junction table name, so
|
|
122
|
+
* the junction table (+ `_drafts`) is renamed instead of a column.
|
|
123
|
+
*
|
|
124
|
+
* Callers must rebuild FTS separately (planFtsRebuild) when the renamed column
|
|
125
|
+
* was searchable.
|
|
126
|
+
*/
|
|
127
|
+
export declare function generateRenameColumn(seed: Seed, from: string, to: string): string[];
|
|
128
|
+
/**
|
|
129
|
+
* Returns the add/copy/drop/rename statements that change a column's SQL type
|
|
130
|
+
* in place, preserving data via `CAST`. This is the documented simpler
|
|
131
|
+
* alternative to SQLite's full 12-step table rebuild.
|
|
132
|
+
*
|
|
133
|
+
* The `branch` passed in is the *target* definition (already carrying the new
|
|
134
|
+
* `type`); its SQL type is read from BRANCH_TYPE_SQL. Multi-relation branches
|
|
135
|
+
* have no column to retype and are rejected. Mirrors the change onto the drafts
|
|
136
|
+
* table when drafts are enabled. Callers rebuild FTS separately when needed.
|
|
137
|
+
*/
|
|
138
|
+
export declare function generateRetypeColumn(seed: Seed, branch: Branch): string[];
|
|
139
|
+
/**
|
|
140
|
+
* Deserializes a value read from the DB for the API response.
|
|
141
|
+
* 0/1 → boolean | Unix timestamp → ISO 8601 | JSON string → object
|
|
142
|
+
*/
|
|
143
|
+
export declare function deserializeFromDb(branch: Branch, value: unknown): unknown;
|
|
144
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,IAAI,EACJ,MAAM,EAKN,aAAa,EACb,kBAAkB,EACnB,MAAM,YAAY,CAAA;AAsCnB,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAI5D;AAkED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CA0BtD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAyB5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAuBpD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,GAAG,IAAI,CAc1D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAgCxD;AAID;;;;GAIG;AAGH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,aAAkB,GAAG,kBAAkB,CAsF5F;AAgHD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;IACpC,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,OAAO,CAAA;CACd;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,EAAE,CAiB7D;AAID;;;GAGG;AAEH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAwCrF;AAID;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAkBxE;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM5E;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAapF;AAcD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CA0BtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBtE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBnF;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBzE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAqCzE"}
|