@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,97 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Copyright (c) 2024–2026 Flavio De Musso
|
|
3
|
+
/**
|
|
4
|
+
* @module engine/schema-fingerprint
|
|
5
|
+
* A deterministic name for "the shape this API answers in".
|
|
6
|
+
*
|
|
7
|
+
* Computed from live seed definitions (`introspectSeedDefinitions`), embedded in generated client
|
|
8
|
+
* types at build time (sprint 3), returned on every public response as `X-Schema-Revision`
|
|
9
|
+
* (sprint 4), and compared by the client at request time (sprint 5). Both ends MUST compute it
|
|
10
|
+
* with this function — two implementations would be free to disagree, and a drift detector whose
|
|
11
|
+
* ends disagree is worse than none.
|
|
12
|
+
*
|
|
13
|
+
* What is hashed is a CONTRACT PROJECTION, not the whole seed definition: labels, hints, dashboard
|
|
14
|
+
* icons and layout cannot change a single response byte, and hashing them would fire a hard
|
|
15
|
+
* `BeechProblem` in someone else's production client over a typo fix.
|
|
16
|
+
*/
|
|
17
|
+
import { canonicalStringify } from '../common/canonical-json.js';
|
|
18
|
+
import { resolvePolicies } from './policies.js';
|
|
19
|
+
/**
|
|
20
|
+
* Bumped ONLY when the projection below or the canonical byte format changes — i.e. when
|
|
21
|
+
* fingerprints computed by two builds are no longer comparable. It is carried in the fingerprint
|
|
22
|
+
* string itself so a version skew reads as a mismatch instead of as an accidental match.
|
|
23
|
+
*/
|
|
24
|
+
export const SCHEMA_FINGERPRINT_VERSION = 1;
|
|
25
|
+
function projectBranch(branch) {
|
|
26
|
+
const policies = resolvePolicies(branch);
|
|
27
|
+
return {
|
|
28
|
+
alias: branch.alias,
|
|
29
|
+
type: branch.type,
|
|
30
|
+
...(branch.format !== undefined ? { format: branch.format } : {}),
|
|
31
|
+
...(branch.multiple !== undefined ? { multiple: branch.multiple } : {}),
|
|
32
|
+
// Option order is the contract: it is the order a select renders and a union type lists.
|
|
33
|
+
...(branch.options !== undefined ? { options: [...branch.options] } : {}),
|
|
34
|
+
requiredOnCreate: branch.requiredOnCreate ?? false,
|
|
35
|
+
requiredOnUpdate: branch.requiredOnUpdate ?? false,
|
|
36
|
+
...(branch.targetSeed !== undefined ? { targetSeed: branch.targetSeed } : {}),
|
|
37
|
+
visibility: policies.visibility,
|
|
38
|
+
publicRead: policies.public,
|
|
39
|
+
publicEdit: policies.publicEdit,
|
|
40
|
+
...(branch.minItems !== undefined ? { minItems: branch.minItems } : {}),
|
|
41
|
+
...(branch.maxItems !== undefined ? { maxItems: branch.maxItems } : {}),
|
|
42
|
+
// Spread only when true so every schema without localized branches keeps its fingerprint.
|
|
43
|
+
...(branch.localized === true ? { localized: true } : {}),
|
|
44
|
+
...(branch.fields ? { fields: branch.fields.map(projectBranch) } : {}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reduces seeds to what a response shape depends on.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately EXCLUDED, and each for a reason a future reader must not "fix":
|
|
51
|
+
* - `id` (`br_XX`) — a stable internal handle; it never appears in a payload, and minting order
|
|
52
|
+
* differs between an environment built by migration and one grown through the dashboard.
|
|
53
|
+
* - `label`, `labelPlural`, `hint`, `dashboard`, `layout` — presentation, never response shape.
|
|
54
|
+
* - `retentionDays`, `onDelete`, `policies.search|filter|sort`, `numberOptions`, `fileOptions`,
|
|
55
|
+
* `policies.classification|privacy` — behaviour and storage concerns that leave the response
|
|
56
|
+
* shape identical. (`privacy` DOES change a stored value's encoding, but the API type stays
|
|
57
|
+
* `string`; drift there is a data concern, not a type concern.)
|
|
58
|
+
* - anything physical (column order, index names) — see the VETO Audit.
|
|
59
|
+
*
|
|
60
|
+
* Seeds are sorted by slug; BRANCH ORDER IS PRESERVED, because it is the declared field order the
|
|
61
|
+
* generated types and the dashboard form both follow.
|
|
62
|
+
*/
|
|
63
|
+
export function projectSchemaContract(seeds) {
|
|
64
|
+
return {
|
|
65
|
+
fingerprintVersion: SCHEMA_FINGERPRINT_VERSION,
|
|
66
|
+
seeds: [...seeds]
|
|
67
|
+
.sort((a, b) => a.slug.localeCompare(b.slug))
|
|
68
|
+
.map(seed => ({
|
|
69
|
+
slug: seed.slug,
|
|
70
|
+
displayNameAlias: seed.displayNameAlias,
|
|
71
|
+
allowDrafts: seed.allowDrafts ?? false,
|
|
72
|
+
allowPublicRead: seed.allowPublicRead ?? false,
|
|
73
|
+
allowPublicPost: seed.allowPublicPost ?? false,
|
|
74
|
+
allowPublicEdit: seed.allowPublicEdit ?? false,
|
|
75
|
+
branches: seed.branches.map(projectBranch),
|
|
76
|
+
})),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function toHex(buffer) {
|
|
80
|
+
return Array.from(new Uint8Array(buffer), byte => byte.toString(16).padStart(2, '0')).join('');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `v{VERSION}:{32 lowercase hex}` — SHA-256 over the canonical JSON of the contract projection,
|
|
84
|
+
* truncated to 128 bits.
|
|
85
|
+
*
|
|
86
|
+
* Truncation is deliberate: this value travels on EVERY public response as `X-Schema-Revision`,
|
|
87
|
+
* and 128 bits of a SHA-256 digest carries no realistic collision risk for a value space of a few
|
|
88
|
+
* hundred schema revisions. It is a revision marker, never a security token.
|
|
89
|
+
*
|
|
90
|
+
* Web Crypto only — the same API the Worker and Node ≥ 18 both expose, as `webhook-crypto.ts`
|
|
91
|
+
* already relies on. `@beechcms/core` gains no dependency.
|
|
92
|
+
*/
|
|
93
|
+
export async function computeSchemaFingerprint(seeds) {
|
|
94
|
+
const bytes = new TextEncoder().encode(canonicalStringify(projectSchemaContract(seeds)));
|
|
95
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes);
|
|
96
|
+
return `v${SCHEMA_FINGERPRINT_VERSION}:${toHex(digest).slice(0, 32)}`;
|
|
97
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../../src/engine/seed-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAI9C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAID,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;
|
|
1
|
+
{"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../../src/engine/seed-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAI9C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAID,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IAErC,YAAY,KAAK,EAAE,IAAI,EAAE,EAgCxB;IAED,GAAG,IAAI,IAAI,EAAE,CAEZ;IAED,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAE7B;IAED,kBAAkB,IAAI,IAAI,EAAE,CAE3B;IAED,cAAc,IAAI,IAAI,EAAE,CAEvB;IAED,YAAY,IAAI,IAAI,EAAE,CAErB;CACF;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG;AAEzD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,MAAM,CAQjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CA0F1E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-seed.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/define-seed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAEvC,wBAAgB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,CAE3C"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Seed, Branch } from '../types.js';
|
|
2
|
+
export interface ISeedRegistry {
|
|
3
|
+
/**
|
|
4
|
+
* Returns all seeds as a flat array, preserving insertion order.
|
|
5
|
+
* Decouples callers from the internal storage shape so the registry
|
|
6
|
+
* implementation can change without touching every route handler.
|
|
7
|
+
*/
|
|
8
|
+
all(): Seed[];
|
|
9
|
+
/**
|
|
10
|
+
* Returns the seed with the given slug, or null if not found.
|
|
11
|
+
* Provides a single lookup point that can be overridden in tests
|
|
12
|
+
* without rebuilding the full registry object.
|
|
13
|
+
*/
|
|
14
|
+
get(slug: string): Seed | null;
|
|
15
|
+
/**
|
|
16
|
+
* Returns seeds that are visible in the dashboard sidebar.
|
|
17
|
+
* A seed is visible when dashboard.hidden is not explicitly true.
|
|
18
|
+
* Eliminates the seeds.filter(s => !s.dashboard?.hidden) pattern
|
|
19
|
+
* that would otherwise be duplicated across route handlers.
|
|
20
|
+
*/
|
|
21
|
+
visibleInDashboard(): Seed[];
|
|
22
|
+
/**
|
|
23
|
+
* Returns seeds that have allowPublicRead enabled.
|
|
24
|
+
* Eliminates the seeds.filter(s => s.allowPublicRead) pattern.
|
|
25
|
+
*/
|
|
26
|
+
publicReadable(): Seed[];
|
|
27
|
+
/**
|
|
28
|
+
* Returns seeds that have the draft workflow enabled.
|
|
29
|
+
* Eliminates the seeds.filter(s => s.allowDrafts) pattern.
|
|
30
|
+
*/
|
|
31
|
+
draftEnabled(): Seed[];
|
|
32
|
+
}
|
|
33
|
+
export declare class SeedRegistry implements ISeedRegistry {
|
|
34
|
+
private readonly seedMap;
|
|
35
|
+
private readonly orderedSeeds;
|
|
36
|
+
constructor(seeds: Seed[]);
|
|
37
|
+
all(): Seed[];
|
|
38
|
+
get(slug: string): Seed | null;
|
|
39
|
+
visibleInDashboard(): Seed[];
|
|
40
|
+
publicReadable(): Seed[];
|
|
41
|
+
draftEnabled(): Seed[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Named subclass of SeedRegistry for use in test suites.
|
|
45
|
+
* Gives tests a semantic name without requiring any dependency on factory.ts.
|
|
46
|
+
*/
|
|
47
|
+
export declare class InMemorySeedRegistry extends SeedRegistry {
|
|
48
|
+
}
|
|
49
|
+
/** Finds a branch by its stable id within a Seed. Returns null if not found. */
|
|
50
|
+
export declare function findBranchById(seed: Seed, id: string): Branch | null;
|
|
51
|
+
/**
|
|
52
|
+
* Returns the next free branch id for a seed in the form `br_NN` (zero-padded to 2,
|
|
53
|
+
* growing as needed). Scans existing ids, never reuses a number already present —
|
|
54
|
+
* even if a branch was removed, its id is not recycled (ids must be globally stable
|
|
55
|
+
* for the life of the seed so layouts/automations/FTS triggers never collide).
|
|
56
|
+
*/
|
|
57
|
+
export declare function nextBranchId(seed: Pick<Seed, 'branches'>): string;
|
|
58
|
+
/**
|
|
59
|
+
* Returns the input seeds reordered so that every seed appears AFTER all the
|
|
60
|
+
* seeds it depends on (its `relation` branch targets). Pure function — does
|
|
61
|
+
* not read from any global registry.
|
|
62
|
+
*
|
|
63
|
+
* Uses Kahn's algorithm (BFS on in-degree) for cycle detection and topological
|
|
64
|
+
* ordering. Sprint 3 (CLI / migration runner) consumes this to create tables in
|
|
65
|
+
* the correct order without disabling SQLite FK constraints.
|
|
66
|
+
*
|
|
67
|
+
* @throws {Error} When a relation targets an unknown slug not in `seeds`.
|
|
68
|
+
* @throws {Error} When a cyclic dependency is detected, listing involved slugs.
|
|
69
|
+
*/
|
|
70
|
+
export declare function sortSeedsByDependencies(seeds: ReadonlyArray<Seed>): Seed[];
|
|
71
|
+
//# sourceMappingURL=seed-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-registry.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/seed-registry.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAI/C,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,GAAG,IAAI,IAAI,EAAE,CAAA;IAEb;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAA;IAE9B;;;;;OAKG;IACH,kBAAkB,IAAI,IAAI,EAAE,CAAA;IAE5B;;;OAGG;IACH,cAAc,IAAI,IAAI,EAAE,CAAA;IAExB;;;OAGG;IACH,YAAY,IAAI,IAAI,EAAE,CAAA;CACvB;AAID,qBAAa,YAAa,YAAW,aAAa;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;gBAEzB,KAAK,EAAE,IAAI,EAAE;IAkCzB,GAAG,IAAI,IAAI,EAAE;IAIb,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAI9B,kBAAkB,IAAI,IAAI,EAAE;IAI5B,cAAc,IAAI,IAAI,EAAE;IAIxB,YAAY,IAAI,IAAI,EAAE;CAGvB;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;CAAG;AAEzD,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEpE;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,GAAG,MAAM,CAQjE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,GAAG,IAAI,EAAE,CA0F1E"}
|
|
@@ -0,0 +1,167 @@
|
|
|
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 { SQL_RESERVED_WORDS } from '../sql-reserved-words.js';
|
|
5
|
+
const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
|
|
6
|
+
export class SeedRegistry {
|
|
7
|
+
seedMap;
|
|
8
|
+
orderedSeeds;
|
|
9
|
+
constructor(seeds) {
|
|
10
|
+
for (const seed of seeds) {
|
|
11
|
+
const idsInSeed = new Set();
|
|
12
|
+
for (const branch of seed.branches) {
|
|
13
|
+
if (AUTOMATION_RESERVED_WORDS.has(branch.alias)) {
|
|
14
|
+
throw new Error(`Seed "${seed.slug}" uses reserved alias "${branch.alias}". `
|
|
15
|
+
+ `Pick a different alias — this word is used by the automation template grammar.`);
|
|
16
|
+
}
|
|
17
|
+
if (SQL_RESERVED_WORDS.has(branch.alias.toLowerCase())) {
|
|
18
|
+
throw new Error(`Seed "${seed.slug}" uses SQL reserved keyword "${branch.alias}". `
|
|
19
|
+
+ `Please pick a different alias to prevent database errors.`);
|
|
20
|
+
}
|
|
21
|
+
if (!branch.id || !BRANCH_ID_RE.test(branch.id)) {
|
|
22
|
+
throw new Error(`Seed "${seed.slug}" branch "${branch.alias}" has invalid id "${branch.id}". `
|
|
23
|
+
+ `Expected format ^br_[A-Za-z0-9]+$.`);
|
|
24
|
+
}
|
|
25
|
+
if (idsInSeed.has(branch.id)) {
|
|
26
|
+
throw new Error(`Seed "${seed.slug}" has duplicate branch id "${branch.id}".`);
|
|
27
|
+
}
|
|
28
|
+
idsInSeed.add(branch.id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.orderedSeeds = seeds;
|
|
32
|
+
this.seedMap = new Map(seeds.map(seed => [seed.slug, seed]));
|
|
33
|
+
}
|
|
34
|
+
all() {
|
|
35
|
+
return this.orderedSeeds;
|
|
36
|
+
}
|
|
37
|
+
get(slug) {
|
|
38
|
+
return this.seedMap.get(slug) ?? null;
|
|
39
|
+
}
|
|
40
|
+
visibleInDashboard() {
|
|
41
|
+
return this.orderedSeeds.filter(seed => seed.dashboard?.hidden !== true);
|
|
42
|
+
}
|
|
43
|
+
publicReadable() {
|
|
44
|
+
return this.orderedSeeds.filter(seed => seed.allowPublicRead === true);
|
|
45
|
+
}
|
|
46
|
+
draftEnabled() {
|
|
47
|
+
return this.orderedSeeds.filter(seed => seed.allowDrafts === true);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Named subclass of SeedRegistry for use in test suites.
|
|
52
|
+
* Gives tests a semantic name without requiring any dependency on factory.ts.
|
|
53
|
+
*/
|
|
54
|
+
export class InMemorySeedRegistry extends SeedRegistry {
|
|
55
|
+
}
|
|
56
|
+
/** Finds a branch by its stable id within a Seed. Returns null if not found. */
|
|
57
|
+
export function findBranchById(seed, id) {
|
|
58
|
+
return seed.branches.find(b => b.id === id) ?? null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Returns the next free branch id for a seed in the form `br_NN` (zero-padded to 2,
|
|
62
|
+
* growing as needed). Scans existing ids, never reuses a number already present —
|
|
63
|
+
* even if a branch was removed, its id is not recycled (ids must be globally stable
|
|
64
|
+
* for the life of the seed so layouts/automations/FTS triggers never collide).
|
|
65
|
+
*/
|
|
66
|
+
export function nextBranchId(seed) {
|
|
67
|
+
let max = 0;
|
|
68
|
+
for (const b of seed.branches) {
|
|
69
|
+
const m = /^br_0*([0-9]+)$/.exec(b.id);
|
|
70
|
+
if (m)
|
|
71
|
+
max = Math.max(max, parseInt(m[1], 10));
|
|
72
|
+
}
|
|
73
|
+
const n = max + 1;
|
|
74
|
+
return `br_${String(n).padStart(2, '0')}`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Returns the input seeds reordered so that every seed appears AFTER all the
|
|
78
|
+
* seeds it depends on (its `relation` branch targets). Pure function — does
|
|
79
|
+
* not read from any global registry.
|
|
80
|
+
*
|
|
81
|
+
* Uses Kahn's algorithm (BFS on in-degree) for cycle detection and topological
|
|
82
|
+
* ordering. Sprint 3 (CLI / migration runner) consumes this to create tables in
|
|
83
|
+
* the correct order without disabling SQLite FK constraints.
|
|
84
|
+
*
|
|
85
|
+
* @throws {Error} When a relation targets an unknown slug not in `seeds`.
|
|
86
|
+
* @throws {Error} When a cyclic dependency is detected, listing involved slugs.
|
|
87
|
+
*/
|
|
88
|
+
export function sortSeedsByDependencies(seeds) {
|
|
89
|
+
const slugSet = new Set(seeds.map(s => s.slug));
|
|
90
|
+
// Validate all relation targets exist in the input array
|
|
91
|
+
for (const seed of seeds) {
|
|
92
|
+
for (const branch of seed.branches) {
|
|
93
|
+
if (branch.type === 'relation' && branch.targetSeed) {
|
|
94
|
+
if (!slugSet.has(branch.targetSeed)) {
|
|
95
|
+
throw new Error(`Seed '${seed.slug}' relates to unknown target '${branch.targetSeed}'`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// Build adjacency: inDegree[slug] = number of dependencies
|
|
101
|
+
// dependents[slug] = slugs that depend on slug
|
|
102
|
+
const inDegree = new Map();
|
|
103
|
+
const dependents = new Map();
|
|
104
|
+
for (const seed of seeds) {
|
|
105
|
+
if (!inDegree.has(seed.slug))
|
|
106
|
+
inDegree.set(seed.slug, 0);
|
|
107
|
+
if (!dependents.has(seed.slug))
|
|
108
|
+
dependents.set(seed.slug, []);
|
|
109
|
+
}
|
|
110
|
+
for (const seed of seeds) {
|
|
111
|
+
// Deduplicate targets to avoid inflating inDegree on multi-branch refs to same seed
|
|
112
|
+
const targets = new Set(seed.branches
|
|
113
|
+
.filter(b => b.type === 'relation' && b.targetSeed)
|
|
114
|
+
.map(b => b.targetSeed));
|
|
115
|
+
for (const target of targets) {
|
|
116
|
+
if (target === seed.slug)
|
|
117
|
+
continue; // self-reference needs no ordering
|
|
118
|
+
// seed depends on target → increment seed's in-degree
|
|
119
|
+
inDegree.set(seed.slug, (inDegree.get(seed.slug) ?? 0) + 1);
|
|
120
|
+
dependents.get(target).push(seed.slug);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Kahn's BFS: start with seeds that have no dependencies
|
|
124
|
+
const seedBySlug = new Map(seeds.map(s => [s.slug, s]));
|
|
125
|
+
const queue = [];
|
|
126
|
+
for (const [slug, deg] of inDegree) {
|
|
127
|
+
if (deg === 0)
|
|
128
|
+
queue.push(slug);
|
|
129
|
+
}
|
|
130
|
+
const result = [];
|
|
131
|
+
while (queue.length > 0) {
|
|
132
|
+
const slug = queue.shift();
|
|
133
|
+
result.push(seedBySlug.get(slug));
|
|
134
|
+
for (const dependent of dependents.get(slug) ?? []) {
|
|
135
|
+
const newDeg = (inDegree.get(dependent) ?? 0) - 1;
|
|
136
|
+
inDegree.set(dependent, newDeg);
|
|
137
|
+
if (newDeg === 0)
|
|
138
|
+
queue.push(dependent);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (result.length !== seeds.length) {
|
|
142
|
+
// Isolate true cycle members
|
|
143
|
+
const stuck = new Set([...inDegree.entries()]
|
|
144
|
+
.filter(([, deg]) => deg > 0)
|
|
145
|
+
.map(([slug]) => slug));
|
|
146
|
+
const cycleNodes = [...stuck].filter(slug => {
|
|
147
|
+
const visited = new Set();
|
|
148
|
+
const queue = [...(dependents.get(slug) ?? [])].filter(s => stuck.has(s));
|
|
149
|
+
while (queue.length > 0) {
|
|
150
|
+
const curr = queue.shift();
|
|
151
|
+
if (curr === slug)
|
|
152
|
+
return true;
|
|
153
|
+
if (!visited.has(curr)) {
|
|
154
|
+
visited.add(curr);
|
|
155
|
+
for (const next of dependents.get(curr) ?? []) {
|
|
156
|
+
if (stuck.has(next))
|
|
157
|
+
queue.push(next);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
});
|
|
163
|
+
const cycle = (cycleNodes.length > 0 ? cycleNodes : [...stuck]).sort();
|
|
164
|
+
throw new Error(`Cyclic dependency detected among seeds: ${cycle.join(', ')}`);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Seed, Branch } from '../types.js';
|
|
2
|
+
/** slug/alias → PascalCase identifier. 'blog-posts' → 'BlogPosts'. */
|
|
3
|
+
export declare function pascalCase(input: string): string;
|
|
4
|
+
/** Maps a single Branch to its TypeScript type expression (no optional marker). */
|
|
5
|
+
export declare function tsTypeForBranch(branch: Branch): string;
|
|
6
|
+
/** Emits one exported interface for a single Seed. */
|
|
7
|
+
export declare function interfaceForSeed(seed: Seed): string;
|
|
8
|
+
/** Options that affect the emitted module beyond the seed interfaces themselves. */
|
|
9
|
+
export interface SeedTypesOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Schema fingerprint to embed as `SCHEMA_FINGERPRINT`, from `computeSchemaFingerprint`.
|
|
12
|
+
* Omitted ⇒ no constant is emitted and the output is byte-identical to a pre-fingerprint build,
|
|
13
|
+
* which is what keeps the legacy `beech gen-types` aliases non-breaking.
|
|
14
|
+
*/
|
|
15
|
+
fingerprint?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Pure entry point. Deterministic: sorts seeds by slug for stable diffs.
|
|
19
|
+
*
|
|
20
|
+
* The embedded fingerprint is the client's half of the drift check: `@beechcms/client` compares it
|
|
21
|
+
* against the `X-Schema-Revision` header the API returns and raises an actionable error on a
|
|
22
|
+
* mismatch, instead of trusting a stale response shape.
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateSeedTypes(seeds: Seed[], options?: SeedTypesOptions): string;
|
|
25
|
+
//# sourceMappingURL=seed-types-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed-types-generator.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/seed-types-generator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAe/C,sEAAsE;AACtE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMhD;AAMD,mFAAmF;AACnF,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAoCtD;AAWD,sDAAsD;AACtD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAYnD;AAKD,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,MAAM,CAcvF"}
|
|
@@ -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/seeds/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AAUvC;;;;;;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,CAkW5E;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAErD"}
|