@beechcms/core 0.6.0-preview.1 → 0.6.0-preview.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/content.repository.d.ts +84 -3
  2. package/dist/content.repository.d.ts.map +1 -1
  3. package/dist/content.repository.js +12 -0
  4. package/dist/dashboard-layout.d.ts +154 -0
  5. package/dist/dashboard-layout.d.ts.map +1 -0
  6. package/dist/dashboard-layout.js +186 -0
  7. package/dist/dashboard-layout.repository.d.ts +18 -0
  8. package/dist/dashboard-layout.repository.d.ts.map +1 -0
  9. package/dist/dashboard-layout.repository.js +3 -0
  10. package/dist/dashboard-permissions.d.ts +3 -0
  11. package/dist/dashboard-permissions.d.ts.map +1 -0
  12. package/dist/dashboard-permissions.js +11 -0
  13. package/dist/dashboard-scopes.d.ts +12 -0
  14. package/dist/dashboard-scopes.d.ts.map +1 -0
  15. package/dist/dashboard-scopes.js +30 -0
  16. package/dist/engine.d.ts.map +1 -1
  17. package/dist/engine.js +13 -0
  18. package/dist/hooks.d.ts +29 -0
  19. package/dist/hooks.d.ts.map +1 -0
  20. package/dist/hooks.js +3 -0
  21. package/dist/index.d.ts +5 -0
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +5 -0
  24. package/dist/policies.d.ts.map +1 -1
  25. package/dist/policies.js +6 -3
  26. package/dist/seed-ddl-destructive.d.ts +40 -0
  27. package/dist/seed-ddl-destructive.d.ts.map +1 -0
  28. package/dist/seed-ddl-destructive.js +149 -0
  29. package/dist/seed-validation.d.ts.map +1 -1
  30. package/dist/seed-validation.js +70 -2
  31. package/dist/types.d.ts +27 -1
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/validation.d.ts.map +1 -1
  34. package/dist/validation.js +58 -1
  35. package/dist/widget/widget.repository.d.ts +32 -3
  36. package/dist/widget/widget.repository.d.ts.map +1 -1
  37. package/dist/widget/widget.repository.js +4 -1
  38. package/package.json +11 -11
@@ -0,0 +1,29 @@
1
+ import type { ContentRepository } from './content.repository.js';
2
+ import type { Seed } from './types.js';
3
+ export interface HookActor {
4
+ id: string;
5
+ role?: string;
6
+ email?: string;
7
+ }
8
+ export interface HookContext {
9
+ seed: Seed;
10
+ /** Canale lecito per side-effect sui contenuti dagli hook (rispetta la Botanical Engine). */
11
+ repository: ContentRepository;
12
+ /** Utente che esegue l'operazione, estratto dal JWT. Assente per operazioni di sistema/cron. */
13
+ actor?: HookActor;
14
+ /**
15
+ * Escape hatch per la connessione nativa (D1Database in prod, better-sqlite3 nei test).
16
+ * Tipizzato `unknown` per non accoppiare @beechcms/core a Cloudflare. Usare con cautela:
17
+ * scrivere qui bypassa la Botanical Engine.
18
+ */
19
+ db: unknown;
20
+ }
21
+ export interface BeechHooks {
22
+ beforeCreate?: (data: Record<string, any>, ctx: HookContext) => Promise<Record<string, any> | void> | Record<string, any> | void;
23
+ beforeUpdate?: (id: string, patches: Record<string, any>, ctx: HookContext) => Promise<Record<string, any> | void> | Record<string, any> | void;
24
+ beforeDelete?: (id: string, ctx: HookContext) => Promise<void> | void;
25
+ afterCreate?: (entry: Record<string, any>, ctx: HookContext) => Promise<void> | void;
26
+ afterUpdate?: (entry: Record<string, any>, ctx: HookContext) => Promise<void> | void;
27
+ afterDelete?: (id: string, ctx: HookContext) => Promise<void> | void;
28
+ }
29
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,IAAI,CAAA;IACV,6FAA6F;IAC7F,UAAU,EAAE,iBAAiB,CAAA;IAC7B,gGAAgG;IAChG,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB;;;;OAIG;IACH,EAAE,EAAE,OAAO,CAAA;CACZ;AAED,MAAM,WAAW,UAAU;IAGzB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAChI,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAAA;IAC/I,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAGrE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACpF,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACpF,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CACrE"}
package/dist/hooks.js ADDED
@@ -0,0 +1,3 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ export {};
package/dist/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export * from './richtext.js';
17
17
  export * from './richtext-render.js';
18
18
  export * from './slug-utils.js';
19
19
  export * from './content.repository.js';
20
+ export * from './hooks.js';
20
21
  export * from './idempotency.repository.js';
21
22
  export * from './media.repository.js';
22
23
  export * from './storage.js';
@@ -51,6 +52,10 @@ export * from './demo-data.repository.js';
51
52
  export * from './seed-layout.js';
52
53
  export * from './seed-layout.repository.js';
53
54
  export * from './layout-permissions.js';
55
+ export * from './dashboard-layout.js';
56
+ export * from './dashboard-scopes.js';
57
+ export * from './dashboard-permissions.js';
58
+ export * from './dashboard-layout.repository.js';
54
59
  export * from './seed.repository.js';
55
60
  export * from './seed-validation.js';
56
61
  export * from './seed-ddl.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2CAA2C,CAAA;AACzD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,oCAAoC,CAAA;AAClD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,uCAAuC,CAAA;AACrD,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,eAAe,CAAA;AAC7B,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,yBAAyB,CAAA;AACvC,cAAc,YAAY,CAAA;AAC1B,cAAc,6BAA6B,CAAA;AAC3C,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,eAAe,CAAA;AAC7B,cAAc,yBAAyB,CAAA;AACvC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2CAA2C,CAAA;AACzD,cAAc,8BAA8B,CAAA;AAC5C,cAAc,oCAAoC,CAAA;AAClD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,4CAA4C,CAAA;AAC1D,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,8BAA8B,CAAA;AAC5C,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,oBAAoB,CAAA;AAClC,cAAc,wBAAwB,CAAA;AACtC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,mCAAmC,CAAA;AACjD,cAAc,uCAAuC,CAAA;AACrD,cAAc,0BAA0B,CAAA;AACxC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,+BAA+B,CAAA;AAC7C,cAAc,2BAA2B,CAAA;AACzC,cAAc,kBAAkB,CAAA;AAChC,cAAc,6BAA6B,CAAA;AAC3C,cAAc,yBAAyB,CAAA;AACvC,cAAc,uBAAuB,CAAA;AACrC,cAAc,uBAAuB,CAAA;AACrC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,kCAAkC,CAAA;AAChD,cAAc,sBAAsB,CAAA;AACpC,cAAc,sBAAsB,CAAA;AACpC,cAAc,eAAe,CAAA;AAC7B,cAAc,qBAAqB,CAAA"}
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ export * from './richtext.js';
19
19
  export * from './richtext-render.js';
20
20
  export * from './slug-utils.js';
21
21
  export * from './content.repository.js';
22
+ export * from './hooks.js';
22
23
  export * from './idempotency.repository.js';
23
24
  export * from './media.repository.js';
24
25
  export * from './storage.js';
@@ -53,6 +54,10 @@ export * from './demo-data.repository.js';
53
54
  export * from './seed-layout.js';
54
55
  export * from './seed-layout.repository.js';
55
56
  export * from './layout-permissions.js';
57
+ export * from './dashboard-layout.js';
58
+ export * from './dashboard-scopes.js';
59
+ export * from './dashboard-permissions.js';
60
+ export * from './dashboard-layout.repository.js';
56
61
  export * from './seed.repository.js';
57
62
  export * from './seed-validation.js';
58
63
  export * from './seed-ddl.js';
@@ -1 +1 @@
1
- {"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAazF"}
1
+ {"version":3,"file":"policies.d.ts","sourceRoot":"","sources":["../src/policies.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAExC,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK9D;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEzF;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAgBzF"}
package/dist/policies.js CHANGED
@@ -19,12 +19,15 @@ export function resolvePolicies(branch) {
19
19
  // Non-plain privacy implies hidden by default: the CMS hashes/encrypts on write,
20
20
  // so returning the stored value would leak the digest to readers.
21
21
  const defaultVisibility = privacy !== 'plain' ? 'hidden' : 'full';
22
+ // Repeaters live in a single JSON column — never filterable, sortable, or
23
+ // searchable/facetable in v1, regardless of what's set on the branch.
24
+ const isRepeater = branch.type === 'repeater';
22
25
  return {
23
26
  privacy,
24
27
  visibility: branch.policies?.visibility ?? defaultVisibility,
25
- search: branch.policies?.search ?? true,
26
- filter: branch.policies?.filter ?? true,
27
- sort: branch.policies?.sort ?? true,
28
+ search: isRepeater ? false : branch.policies?.search ?? true,
29
+ filter: isRepeater ? false : branch.policies?.filter ?? true,
30
+ sort: isRepeater ? false : branch.policies?.sort ?? true,
28
31
  public: branch.policies?.public ?? true,
29
32
  };
30
33
  }
@@ -0,0 +1,40 @@
1
+ import type { Seed, BranchType } from './types.js';
2
+ /**
3
+ * Ordered DROP TABLE statements for a full hard-delete of a seed's tables.
4
+ * Order: junction drafts → junctions → FTS triggers → FTS table → drafts → main table.
5
+ * All statements use IF EXISTS (idempotent).
6
+ */
7
+ export declare function generateDropTable(seed: Seed): string[];
8
+ /**
9
+ * DROP COLUMN statements for a single branch (by alias).
10
+ * For multi-relation branches: drops the junction tables (no column on parent).
11
+ * For regular branches: drops from main table + drafts table (if allowDrafts).
12
+ * Does NOT include FTS handling — callers must prepend ftsTriggerDrops() and
13
+ * append planFtsRebuild() when the branch is searchable.
14
+ */
15
+ export declare function generateDropColumn(seed: Seed, alias: string): string[];
16
+ /**
17
+ * RENAME COLUMN statements for a branch alias change.
18
+ * Updates main table + drafts (if allowDrafts).
19
+ * Does NOT handle FTS — callers must rebuild FTS separately (SQLite auto-updates
20
+ * trigger bodies on RENAME but the FTS virtual-table columns keep the old name).
21
+ */
22
+ export declare function generateRenameColumn(seed: Seed, fromAlias: string, toAlias: string): string[];
23
+ /**
24
+ * Full FTS rebuild: drop triggers + FTS table, then recreate them, then backfill.
25
+ * Returns an empty array if the seed has no searchable branches.
26
+ * The drop steps use IF EXISTS so this is safe to call even if FTS was already dropped.
27
+ */
28
+ export declare function planFtsRebuild(seed: Seed): string[];
29
+ export interface RetypeColumnPlan {
30
+ statements: string[];
31
+ ftsRebuildNeeded: boolean;
32
+ }
33
+ /**
34
+ * Generates statements to change a branch's SQL storage type using the
35
+ * add-new-column / copy / drop-old / rename approach.
36
+ * Includes FTS rebuild statements when the branch is (or becomes) searchable.
37
+ * `updatedSeed` must already reflect the new type on the renamed branch.
38
+ */
39
+ export declare function planRetypeColumn(seed: Seed, alias: string, newType: BranchType, updatedSeed: Seed): RetypeColumnPlan;
40
+ //# sourceMappingURL=seed-ddl-destructive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed-ddl-destructive.d.ts","sourceRoot":"","sources":["../src/seed-ddl-destructive.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAU,UAAU,EAAE,MAAM,YAAY,CAAA;AAkC1D;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAgBtD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAgBtE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ7F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAiBnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,EACnB,WAAW,EAAE,IAAI,GAChB,gBAAgB,CAyClB"}
@@ -0,0 +1,149 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { junctionTableName, generateFtsTable, generateFtsTriggers } from './engine.js';
4
+ const BRANCH_SQL_TYPE = {
5
+ text: 'TEXT',
6
+ number: 'REAL',
7
+ boolean: 'INTEGER',
8
+ date: 'INTEGER',
9
+ json: 'TEXT',
10
+ richtext: 'TEXT',
11
+ file: 'TEXT',
12
+ tags: 'TEXT',
13
+ relation: 'TEXT',
14
+ repeater: 'TEXT',
15
+ };
16
+ /** Branch types that receive a B-tree index in generateIndexes (used by retype to drop/recreate). */
17
+ const INDEXABLE_TYPES = new Set(['text', 'number', 'date', 'boolean', 'relation']);
18
+ function searchBranches(seed) {
19
+ return seed.branches.filter(b => (b.type === 'text' || b.type === 'richtext') && b.policies?.search !== false);
20
+ }
21
+ function ftsTriggerDrops(slug) {
22
+ return [
23
+ `DROP TRIGGER IF EXISTS fts_${slug}_insert;`,
24
+ `DROP TRIGGER IF EXISTS fts_${slug}_update;`,
25
+ `DROP TRIGGER IF EXISTS fts_${slug}_delete;`,
26
+ `DROP TABLE IF EXISTS fts_${slug};`,
27
+ ];
28
+ }
29
+ /**
30
+ * Ordered DROP TABLE statements for a full hard-delete of a seed's tables.
31
+ * Order: junction drafts → junctions → FTS triggers → FTS table → drafts → main table.
32
+ * All statements use IF EXISTS (idempotent).
33
+ */
34
+ export function generateDropTable(seed) {
35
+ const stmts = [];
36
+ const { slug } = seed;
37
+ for (const branch of seed.branches) {
38
+ if (branch.type !== 'relation' || branch.multiple !== true)
39
+ continue;
40
+ const jt = junctionTableName(slug, branch.alias);
41
+ stmts.push(`DROP TABLE IF EXISTS ${jt}_drafts;`);
42
+ stmts.push(`DROP TABLE IF EXISTS ${jt};`);
43
+ }
44
+ stmts.push(...ftsTriggerDrops(slug));
45
+ stmts.push(`DROP TABLE IF EXISTS content_${slug}_drafts;`);
46
+ stmts.push(`DROP TABLE IF EXISTS content_${slug};`);
47
+ return stmts;
48
+ }
49
+ /**
50
+ * DROP COLUMN statements for a single branch (by alias).
51
+ * For multi-relation branches: drops the junction tables (no column on parent).
52
+ * For regular branches: drops from main table + drafts table (if allowDrafts).
53
+ * Does NOT include FTS handling — callers must prepend ftsTriggerDrops() and
54
+ * append planFtsRebuild() when the branch is searchable.
55
+ */
56
+ export function generateDropColumn(seed, alias) {
57
+ const stmts = [];
58
+ const { slug } = seed;
59
+ const branch = seed.branches.find(b => b.alias === alias);
60
+ if (!branch)
61
+ throw new Error(`Branch '${alias}' not found in seed '${slug}'`);
62
+ if (branch.type === 'relation' && branch.multiple === true) {
63
+ const jt = junctionTableName(slug, alias);
64
+ if (seed.allowDrafts)
65
+ stmts.push(`DROP TABLE IF EXISTS ${jt}_drafts;`);
66
+ stmts.push(`DROP TABLE IF EXISTS ${jt};`);
67
+ }
68
+ else {
69
+ stmts.push(`ALTER TABLE content_${slug} DROP COLUMN ${alias};`);
70
+ if (seed.allowDrafts)
71
+ stmts.push(`ALTER TABLE content_${slug}_drafts DROP COLUMN ${alias};`);
72
+ }
73
+ return stmts;
74
+ }
75
+ /**
76
+ * RENAME COLUMN statements for a branch alias change.
77
+ * Updates main table + drafts (if allowDrafts).
78
+ * Does NOT handle FTS — callers must rebuild FTS separately (SQLite auto-updates
79
+ * trigger bodies on RENAME but the FTS virtual-table columns keep the old name).
80
+ */
81
+ export function generateRenameColumn(seed, fromAlias, toAlias) {
82
+ const stmts = [];
83
+ const { slug } = seed;
84
+ stmts.push(`ALTER TABLE content_${slug} RENAME COLUMN ${fromAlias} TO ${toAlias};`);
85
+ if (seed.allowDrafts) {
86
+ stmts.push(`ALTER TABLE content_${slug}_drafts RENAME COLUMN ${fromAlias} TO ${toAlias};`);
87
+ }
88
+ return stmts;
89
+ }
90
+ /**
91
+ * Full FTS rebuild: drop triggers + FTS table, then recreate them, then backfill.
92
+ * Returns an empty array if the seed has no searchable branches.
93
+ * The drop steps use IF EXISTS so this is safe to call even if FTS was already dropped.
94
+ */
95
+ export function planFtsRebuild(seed) {
96
+ const rtBranches = searchBranches(seed);
97
+ if (rtBranches.length === 0)
98
+ return [];
99
+ const { slug } = seed;
100
+ const stmts = [...ftsTriggerDrops(slug)];
101
+ const ftsTable = generateFtsTable(seed);
102
+ if (ftsTable)
103
+ stmts.push(ftsTable);
104
+ stmts.push(...generateFtsTriggers(seed));
105
+ const cols = rtBranches.map(b => b.alias);
106
+ stmts.push(`INSERT INTO fts_${slug}(entry_id, ${cols.join(', ')}) SELECT id, ${cols.join(', ')} FROM content_${slug};`);
107
+ return stmts;
108
+ }
109
+ /**
110
+ * Generates statements to change a branch's SQL storage type using the
111
+ * add-new-column / copy / drop-old / rename approach.
112
+ * Includes FTS rebuild statements when the branch is (or becomes) searchable.
113
+ * `updatedSeed` must already reflect the new type on the renamed branch.
114
+ */
115
+ export function planRetypeColumn(seed, alias, newType, updatedSeed) {
116
+ const branch = seed.branches.find(b => b.alias === alias);
117
+ if (!branch)
118
+ throw new Error(`Branch '${alias}' not found in seed '${seed.slug}'`);
119
+ const { slug } = seed;
120
+ const newSqlType = BRANCH_SQL_TYPE[newType];
121
+ const tmpAlias = `${alias}_migrate`;
122
+ const stmts = [];
123
+ // Always drop FTS first to avoid "column referenced in trigger" errors on DROP COLUMN.
124
+ stmts.push(...ftsTriggerDrops(slug));
125
+ // Drop the branch index if it exists on the old column (required before DROP COLUMN).
126
+ if (INDEXABLE_TYPES.has(branch.type) && branch.policies?.filter !== false) {
127
+ stmts.push(`DROP INDEX IF EXISTS idx_${slug}_${alias};`);
128
+ }
129
+ // Main table: add migration column, copy data, drop old, rename.
130
+ stmts.push(`ALTER TABLE content_${slug} ADD COLUMN ${tmpAlias} ${newSqlType};`);
131
+ stmts.push(`UPDATE content_${slug} SET ${tmpAlias} = CAST(${alias} AS ${newSqlType});`);
132
+ stmts.push(`ALTER TABLE content_${slug} DROP COLUMN ${alias};`);
133
+ stmts.push(`ALTER TABLE content_${slug} RENAME COLUMN ${tmpAlias} TO ${alias};`);
134
+ // Recreate index for the renamed column if the new type warrants it.
135
+ if (INDEXABLE_TYPES.has(newType) && branch.policies?.filter !== false) {
136
+ stmts.push(`CREATE INDEX IF NOT EXISTS idx_${slug}_${alias} ON content_${slug}(${alias});`);
137
+ }
138
+ // Drafts table (no indexes to worry about).
139
+ if (seed.allowDrafts) {
140
+ stmts.push(`ALTER TABLE content_${slug}_drafts ADD COLUMN ${tmpAlias} ${newSqlType};`);
141
+ stmts.push(`UPDATE content_${slug}_drafts SET ${tmpAlias} = CAST(${alias} AS ${newSqlType});`);
142
+ stmts.push(`ALTER TABLE content_${slug}_drafts DROP COLUMN ${alias};`);
143
+ stmts.push(`ALTER TABLE content_${slug}_drafts RENAME COLUMN ${tmpAlias} TO ${alias};`);
144
+ }
145
+ // Rebuild FTS if the seed has searchable branches in the updated definition.
146
+ const rebuild = planFtsRebuild(updatedSeed);
147
+ stmts.push(...rebuild);
148
+ return { statements: stmts, ftsRebuildNeeded: rebuild.length > 0 };
149
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"seed-validation.d.ts","sourceRoot":"","sources":["../src/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAAsB,CAAA;AAElD,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,CA6I5E;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAErD"}
1
+ {"version":3,"file":"seed-validation.d.ts","sourceRoot":"","sources":["../src/seed-validation.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAMtC;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,QAAyB,CAAA;AAErD,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,CA0N5E;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAErD"}
@@ -10,7 +10,7 @@ const BRANCH_ID_RE = /^br_[A-Za-z0-9]+$/;
10
10
  * prevent DDL/query injection. Exported so the seeds API rename route reuses the
11
11
  * exact same guard — do not inline a divergent copy.
12
12
  */
13
- export const BRANCH_ALIAS_RE = /^[a-z][a-z0-9_]*$/;
13
+ export const BRANCH_ALIAS_RE = /^[a-z][a-zA-Z0-9_]*$/;
14
14
  /**
15
15
  * Pure, console-free, throw-free validation of a seed set.
16
16
  * The single seed being created/edited should be validated in the context of
@@ -93,7 +93,7 @@ export function validateSeedDefinitions(seeds) {
93
93
  // identifier charset before it can reach DDL/query string interpolation.
94
94
  if (typeof branch.alias !== 'string' || !BRANCH_ALIAS_RE.test(branch.alias)) {
95
95
  messages.push(`branch alias '${branch.alias}' is invalid. Expected format ${BRANCH_ALIAS_RE.source} ` +
96
- `(lowercase letter followed by lowercase letters, digits or underscores).`);
96
+ `(lowercase letter followed by alphanumeric characters or underscores).`);
97
97
  }
98
98
  }
99
99
  if (messages.length > 0)
@@ -111,6 +111,44 @@ export function validateSeedDefinitions(seeds) {
111
111
  if (messages.length > 0)
112
112
  result.push({ slug: seed.slug, messages, fatal: true });
113
113
  }
114
+ // ── Fatal 10: repeater sub-field constraints ─────────────────────────────
115
+ {
116
+ const REPEATER_DISALLOWED_SUBTYPES = new Set(['repeater', 'relation', 'file']);
117
+ for (const seed of seeds) {
118
+ const messages = [];
119
+ for (const branch of seed.branches) {
120
+ if (branch.type !== 'repeater')
121
+ continue;
122
+ const subIds = new Set();
123
+ const subAliases = new Set();
124
+ for (const sub of branch.fields ?? []) {
125
+ if (REPEATER_DISALLOWED_SUBTYPES.has(sub.type)) {
126
+ messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has disallowed type '${sub.type}'. ` +
127
+ `Repeater sub-fields cannot be 'repeater', 'relation', or 'file' in v1.`);
128
+ }
129
+ if (!sub.id || !BRANCH_ID_RE.test(sub.id)) {
130
+ messages.push(`branch '${branch.alias}': sub-field '${sub.alias}' has invalid id '${sub.id}'. ` +
131
+ `Expected format ^br_[A-Za-z0-9]+$.`);
132
+ }
133
+ else if (subIds.has(sub.id)) {
134
+ messages.push(`branch '${branch.alias}': duplicate sub-field id '${sub.id}'`);
135
+ }
136
+ if (sub.id)
137
+ subIds.add(sub.id);
138
+ if (typeof sub.alias !== 'string' || !BRANCH_ALIAS_RE.test(sub.alias)) {
139
+ messages.push(`branch '${branch.alias}': sub-field alias '${sub.alias}' is invalid. ` +
140
+ `Expected format ${BRANCH_ALIAS_RE.source}.`);
141
+ }
142
+ else if (subAliases.has(sub.alias)) {
143
+ messages.push(`branch '${branch.alias}': duplicate sub-field alias '${sub.alias}'`);
144
+ }
145
+ subAliases.add(sub.alias);
146
+ }
147
+ }
148
+ if (messages.length > 0)
149
+ result.push({ slug: seed.slug, messages, fatal: true });
150
+ }
151
+ }
114
152
  // ── Warning 7: duplicate slug ─────────────────────────────────────────────
115
153
  {
116
154
  const slugsSeen = new Set();
@@ -148,6 +186,36 @@ export function validateSeedDefinitions(seeds) {
148
186
  });
149
187
  }
150
188
  }
189
+ // ── Fatal 11 / Warning 10: repeater cardinality bounds ───────────────────
190
+ {
191
+ for (const seed of seeds) {
192
+ const fatals = [];
193
+ const warnings = [];
194
+ for (const branch of seed.branches) {
195
+ const hasBounds = branch.minItems !== undefined || branch.maxItems !== undefined;
196
+ if (!hasBounds)
197
+ continue;
198
+ if (branch.type !== 'repeater') {
199
+ warnings.push(`branch '${branch.alias}': minItems/maxItems are ignored on type ` +
200
+ `'${branch.type}' (repeater-only).`);
201
+ continue;
202
+ }
203
+ for (const [key, val] of [['minItems', branch.minItems], ['maxItems', branch.maxItems]]) {
204
+ if (val !== undefined && (!Number.isInteger(val) || val < 0)) {
205
+ fatals.push(`branch '${branch.alias}': ${key} must be a non-negative integer (got ${val}).`);
206
+ }
207
+ }
208
+ if (Number.isInteger(branch.minItems) && Number.isInteger(branch.maxItems) &&
209
+ branch.minItems > branch.maxItems) {
210
+ fatals.push(`branch '${branch.alias}': minItems (${branch.minItems}) must be <= maxItems (${branch.maxItems}).`);
211
+ }
212
+ }
213
+ if (fatals.length > 0)
214
+ result.push({ slug: seed.slug, messages: fatals, fatal: true });
215
+ if (warnings.length > 0)
216
+ result.push({ slug: seed.slug, messages: warnings, fatal: false });
217
+ }
218
+ }
151
219
  return result;
152
220
  }
153
221
  /** Convenience: true iff no fatal issues. */
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { FileAccept } from './file-types.js';
2
- export type BranchType = 'text' | 'number' | 'boolean' | 'json' | 'date' | 'richtext' | 'file' | 'tags' | 'relation';
2
+ export type BranchType = 'text' | 'number' | 'boolean' | 'json' | 'date' | 'richtext' | 'file' | 'tags' | 'relation' | 'repeater';
3
3
  /** Configurazioni specializzate per il tipo di branch 'number' */
4
4
  export interface NumberFieldOptions {
5
5
  /** Stile di presentazione visiva. Default: 'decimal' */
@@ -120,6 +120,32 @@ export interface Branch {
120
120
  * rule applies to the FK from the junction table to the target table.
121
121
  */
122
122
  onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT';
123
+ /**
124
+ * Sub-schema for `type === 'repeater'`. Each item of the repeater's array
125
+ * value is a record keyed by sub-branch alias, validated against this list.
126
+ * Sub-branches are restricted to leaf/scalar types (no nested `repeater`,
127
+ * `relation`, or `file`) — enforced by validation.ts and seed-validation.ts.
128
+ * Ignored for any other branch type.
129
+ */
130
+ fields?: Branch[];
131
+ /**
132
+ * Minimum number of items a `repeater` value must contain when a value is
133
+ * provided. Repeater-only — ignored for every other branch type.
134
+ *
135
+ * NOTE: this constrains array *length when the field is present*. It does NOT by
136
+ * itself make the field mandatory — an absent/null payload is still allowed unless
137
+ * `requiredOnCreate` / `requiredOnUpdate` is also set. To model "exactly one
138
+ * required object", combine `minItems: 1, maxItems: 1, requiredOnCreate: true`.
139
+ * Must be a non-negative integer and `<= maxItems` when both are set
140
+ * (enforced at boot by seed-validation.ts).
141
+ */
142
+ minItems?: number;
143
+ /**
144
+ * Maximum number of items a `repeater` value may contain. Repeater-only — ignored
145
+ * for every other branch type. `maxItems: 1` models a single "object" column.
146
+ * Must be a non-negative integer and `>= minItems` when both are set.
147
+ */
148
+ maxItems?: number;
123
149
  }
124
150
  /** Dashboard-specific config embedded in a Seed. All fields optional — defaults applied by the dashboard. */
125
151
  export interface DashboardSeedConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAE9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC/B,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAuBD,QAAA,MAAM,aAAa,2CAA4C,CAAA;AAmD/D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAM1F;AA8hBD;;;;;;GAMG;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;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAE5F"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAc,IAAI,EAAE,MAAM,YAAY,CAAA;AAE1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAEjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAErD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC/B,2BAA2B,CAAC,EAAE,OAAO,CAAA;IACrC,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAA;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7B,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,qBAAqB,EAAE,MAAM,EAAE,CAAA;IAC/B,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAuBD,QAAA,MAAM,aAAa,2CAA4C,CAAA;AAmD/D,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAM1F;AA2lBD;;;;;;GAMG;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;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAE5F"}
@@ -348,6 +348,33 @@ function fileSchema(branch, allowNull) {
348
348
  .pipe(z.string().url());
349
349
  return withNullable(withEmptyPreprocessing(inner, allowNull), allowNull);
350
350
  }
351
+ // Sub-branches of a `repeater` are restricted to leaf/scalar types — no nested
352
+ // `repeater`, `relation`, or `file` (v1 restriction, see types.ts Branch.fields).
353
+ const REPEATER_DISALLOWED_SUBTYPES = new Set(['repeater', 'relation', 'file']);
354
+ function repeaterSchema(branch, options) {
355
+ const requiredFlag = options.operation === 'create' ? 'requiredOnCreate' : 'requiredOnUpdate';
356
+ const subBranches = (branch.fields ?? []).filter((sub) => !REPEATER_DISALLOWED_SUBTYPES.has(sub.type));
357
+ const shape = {};
358
+ for (const sub of subBranches) {
359
+ const subSchema = schemaForBranch(sub, options);
360
+ shape[sub.alias] = sub[requiredFlag] ? subSchema : subSchema.optional();
361
+ }
362
+ // z.object() strips unknown keys by default — old item shapes from a renamed/
363
+ // removed sub-field are dropped rather than rejected (sprint 10 §5.1).
364
+ const itemSchema = z.object(shape);
365
+ let arraySchema = z.array(itemSchema);
366
+ if (Number.isInteger(branch.minItems) && branch.minItems >= 0) {
367
+ arraySchema = arraySchema.min(branch.minItems, {
368
+ message: `Expected array(min:${branch.minItems})`,
369
+ });
370
+ }
371
+ if (Number.isInteger(branch.maxItems) && branch.maxItems >= 0) {
372
+ arraySchema = arraySchema.max(branch.maxItems, {
373
+ message: `Expected array(max:${branch.maxItems})`,
374
+ });
375
+ }
376
+ return withNullable(withEmptyPreprocessing(arraySchema, options.allowNull), options.allowNull);
377
+ }
351
378
  const BRANCH_SCHEMA_BUILDERS = {
352
379
  text: (_branch, options) => textSchema(options, options.allowNull),
353
380
  richtext: (_branch, options) => richtextSchema(options, options.allowNull),
@@ -358,6 +385,7 @@ const BRANCH_SCHEMA_BUILDERS = {
358
385
  tags: (_branch, options) => jsonOrTagsSchema(options.allowNull),
359
386
  file: (branch, options) => fileSchema(branch, options.allowNull),
360
387
  relation: (branch, options) => relationSchema(branch, options),
388
+ repeater: (branch, options) => repeaterSchema(branch, options),
361
389
  };
362
390
  function schemaForBranch(branch, options) {
363
391
  const builder = BRANCH_SCHEMA_BUILDERS[branch.type];
@@ -380,6 +408,14 @@ function buildSeedFingerprint(seed) {
380
408
  ru: branch.requiredOnUpdate === true,
381
409
  n: branch.numberOptions ?? null,
382
410
  fi: branch.fileOptions ?? null,
411
+ mi: branch.minItems ?? null,
412
+ ma: branch.maxItems ?? null,
413
+ sub: branch.fields?.map((sub) => ({
414
+ a: sub.alias,
415
+ t: sub.type,
416
+ rc: sub.requiredOnCreate === true,
417
+ ru: sub.requiredOnUpdate === true,
418
+ })) ?? null,
383
419
  }));
384
420
  return JSON.stringify({ s: seed.slug, b: parts });
385
421
  }
@@ -500,11 +536,32 @@ function splitUnknownAliases(seed, payload) {
500
536
  }
501
537
  return { filtered, unknown, details };
502
538
  }
539
+ function flattenZodIssues(issues, parentPath = []) {
540
+ const result = [];
541
+ for (const issue of issues) {
542
+ const issuePath = issue.path;
543
+ const currentPath = [...parentPath, ...issuePath];
544
+ if (issue.code === 'invalid_union' && 'errors' in issue) {
545
+ const unionErrors = issue.errors;
546
+ for (const subIssues of unionErrors) {
547
+ result.push(...flattenZodIssues(subIssues, currentPath));
548
+ }
549
+ }
550
+ else {
551
+ result.push({
552
+ ...issue,
553
+ path: currentPath,
554
+ });
555
+ }
556
+ }
557
+ return result;
558
+ }
503
559
  function processZodIssues(seed, issues, filtered, options) {
504
560
  const details = [];
505
561
  const unknown = [];
506
562
  const dangerous = [];
507
- for (const issue of issues) {
563
+ const flatIssues = flattenZodIssues(issues);
564
+ for (const issue of flatIssues) {
508
565
  if (issue.code === 'unrecognized_keys') {
509
566
  for (const alias of issue.keys) {
510
567
  unknown.push(alias);
@@ -36,6 +36,23 @@ export type AggregateFormula = {
36
36
  * 7 days, 1 month, and 1 year respectively, anchored on `created_at`.
37
37
  */
38
38
  export type TimeWindow = 'week' | 'month' | 'year' | 'all';
39
+ /**
40
+ * Explicit time range applied as a WHERE filter, anchored on `created_at`.
41
+ *
42
+ * Both bounds are unix seconds (inclusive). Used when the dashboard sets a
43
+ * custom date reference instead of one of the relative {@link TimeWindow} presets.
44
+ */
45
+ export interface DateRange {
46
+ from: number;
47
+ to: number;
48
+ }
49
+ /**
50
+ * Time filter accepted by widget queries: either a relative preset
51
+ * ({@link TimeWindow}) or an explicit {@link DateRange}.
52
+ */
53
+ export type WidgetWindow = TimeWindow | DateRange;
54
+ /** Narrows a {@link WidgetWindow} to a {@link DateRange}. */
55
+ export declare function isDateRange(window: WidgetWindow): window is DateRange;
39
56
  export interface LeaderboardEntry {
40
57
  id: string;
41
58
  label: string;
@@ -71,6 +88,10 @@ export interface GrowthResult {
71
88
  currentValue: number;
72
89
  previousValue: number;
73
90
  }
91
+ export interface DistributionSlice {
92
+ label: string;
93
+ value: number;
94
+ }
74
95
  /**
75
96
  * Read-only data access contract for widget routes.
76
97
  *
@@ -84,14 +105,14 @@ export interface IWidgetRepository {
84
105
  * Returns the formula result for the given time window. Always returns a
85
106
  * number; implementations must return 0 when the query produces no rows.
86
107
  */
87
- aggregate(seed: Seed, formula: AggregateFormula, window: TimeWindow): Promise<number>;
108
+ aggregate(seed: Seed, formula: AggregateFormula, window: WidgetWindow): Promise<number>;
88
109
  /**
89
110
  * Evaluates the formula twice — once for the current window period and once
90
111
  * for the equivalent previous period — to support trend calculations.
91
112
  * Implementations must return { currentValue: 0, previousValue: 0 } on
92
113
  * empty results.
93
114
  */
94
- growth(seed: Seed, formula: AggregateFormula, window: TimeWindow): Promise<GrowthResult>;
115
+ growth(seed: Seed, formula: AggregateFormula, window: WidgetWindow): Promise<GrowthResult>;
95
116
  /**
96
117
  * Returns entries sorted by scoreColumn, excluding nulls. label resolves
97
118
  * from seed.displayNameAlias; falls back to id when not set.
@@ -108,6 +129,14 @@ export interface IWidgetRepository {
108
129
  * the formula. Days with no entries are omitted (no zero-fill). Points are
109
130
  * ordered ascending by label.
110
131
  */
111
- timeseries(seed: Seed, formula: AggregateFormula, window: TimeWindow, groupColumn: string): Promise<TimeseriesPoint[]>;
132
+ timeseries(seed: Seed, formula: AggregateFormula, window: WidgetWindow, groupColumn: string): Promise<TimeseriesPoint[]>;
133
+ /**
134
+ * Counts entries grouped by the values of `column` within the window,
135
+ * descending by count, capped at `limit` slices. Implementations must
136
+ * validate `column` against the seed (UNSAFE_COLUMN on failure) and must
137
+ * return [] on empty results. Values beyond `limit` are NOT merged into
138
+ * an 'other' bucket — the client decides how to present truncation.
139
+ */
140
+ distribution(seed: Seed, column: string, window: WidgetWindow, limit: number): Promise<DistributionSlice[]>;
112
141
  }
113
142
  //# sourceMappingURL=widget.repository.d.ts.map