@beechcms/core 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/antivirus/noop-antivirus.provider.d.ts.map +1 -1
  2. package/dist/antivirus/virustotal-antivirus.provider.d.ts.map +1 -1
  3. package/dist/auth/account-email.d.ts +3 -0
  4. package/dist/auth/account-email.d.ts.map +1 -0
  5. package/dist/auth/account-email.js +9 -0
  6. package/dist/auth/password-reset-token.repository.d.ts +13 -2
  7. package/dist/auth/password-reset-token.repository.d.ts.map +1 -1
  8. package/dist/auth/user.repository.d.ts +69 -4
  9. package/dist/auth/user.repository.d.ts.map +1 -1
  10. package/dist/common/canonical-json.d.ts +2 -1
  11. package/dist/common/canonical-json.d.ts.map +1 -1
  12. package/dist/common/canonical-json.js +5 -3
  13. package/dist/content/content-scan.repository.d.ts +1 -1
  14. package/dist/content/content.repository.d.ts +156 -5
  15. package/dist/content/content.repository.d.ts.map +1 -1
  16. package/dist/content/content.repository.js +62 -0
  17. package/dist/content/seed-media-purge.repository.d.ts +34 -0
  18. package/dist/content/seed-media-purge.repository.d.ts.map +1 -0
  19. package/dist/content/seed-media-purge.repository.js +3 -0
  20. package/dist/content/seed.repository.d.ts +14 -0
  21. package/dist/content/seed.repository.d.ts.map +1 -1
  22. package/dist/dashboard-layout/content-view.d.ts +757 -0
  23. package/dist/dashboard-layout/content-view.d.ts.map +1 -0
  24. package/dist/dashboard-layout/content-view.js +285 -0
  25. package/dist/dashboard-layout/content-view.repository.d.ts +39 -0
  26. package/dist/dashboard-layout/content-view.repository.d.ts.map +1 -0
  27. package/dist/dashboard-layout/content-view.repository.js +3 -0
  28. package/dist/dashboard-layout/dashboard-layout.d.ts +5 -0
  29. package/dist/dashboard-layout/dashboard-layout.d.ts.map +1 -1
  30. package/dist/dashboard-layout/dashboard-layout.js +17 -9
  31. package/dist/dashboard-layout/kanban/kanban.d.ts +32 -18
  32. package/dist/dashboard-layout/kanban/kanban.d.ts.map +1 -1
  33. package/dist/dashboard-layout/kanban/kanban.js +45 -21
  34. package/dist/dashboard-layout/seed-layout.d.ts +22 -31
  35. package/dist/dashboard-layout/seed-layout.d.ts.map +1 -1
  36. package/dist/dashboard-layout/seed-layout.js +93 -18
  37. package/dist/dashboard-layout/seed-layout.repository.d.ts +1 -5
  38. package/dist/dashboard-layout/seed-layout.repository.d.ts.map +1 -1
  39. package/dist/engine/ddl/ddl.d.ts +31 -0
  40. package/dist/engine/ddl/ddl.d.ts.map +1 -1
  41. package/dist/engine/ddl/ddl.js +92 -13
  42. package/dist/engine/ddl/schema-mutator.d.ts +3 -0
  43. package/dist/engine/ddl/schema-mutator.d.ts.map +1 -1
  44. package/dist/engine/ddl/seed-purge-guards.d.ts +7 -0
  45. package/dist/engine/ddl/seed-purge-guards.d.ts.map +1 -0
  46. package/dist/engine/ddl/seed-purge-guards.js +19 -0
  47. package/dist/engine/engine.d.ts +4 -3
  48. package/dist/engine/engine.d.ts.map +1 -1
  49. package/dist/engine/engine.js +4 -3
  50. package/dist/engine/introspection/relation-audit.d.ts +46 -0
  51. package/dist/engine/introspection/relation-audit.d.ts.map +1 -0
  52. package/dist/engine/introspection/relation-audit.js +103 -0
  53. package/dist/engine/introspection/schema-fingerprint.d.ts +2 -1
  54. package/dist/engine/introspection/schema-fingerprint.d.ts.map +1 -1
  55. package/dist/engine/introspection/schema-fingerprint.js +8 -3
  56. package/dist/engine/privacy/policies.d.ts +1 -1
  57. package/dist/engine/privacy/policies.d.ts.map +1 -1
  58. package/dist/engine/privacy/policies.js +12 -6
  59. package/dist/engine/privacy/privacy.service.d.ts +13 -0
  60. package/dist/engine/privacy/privacy.service.d.ts.map +1 -1
  61. package/dist/engine/privacy/privacy.service.js +24 -0
  62. package/dist/engine/query/active-clause.d.ts +19 -0
  63. package/dist/engine/query/active-clause.d.ts.map +1 -0
  64. package/dist/engine/query/active-clause.js +27 -0
  65. package/dist/engine/query/query.d.ts +14 -1
  66. package/dist/engine/query/query.d.ts.map +1 -1
  67. package/dist/engine/query/query.js +105 -13
  68. package/dist/engine/seeds/candidate-ids.d.ts +14 -0
  69. package/dist/engine/seeds/candidate-ids.d.ts.map +1 -0
  70. package/dist/engine/seeds/candidate-ids.js +40 -0
  71. package/dist/engine/seeds/seed-definition-diff.d.ts +38 -0
  72. package/dist/engine/seeds/seed-definition-diff.d.ts.map +1 -0
  73. package/dist/engine/seeds/seed-definition-diff.js +51 -0
  74. package/dist/engine/seeds/seed-registry.d.ts +17 -0
  75. package/dist/engine/seeds/seed-registry.d.ts.map +1 -1
  76. package/dist/engine/seeds/seed-registry.js +27 -0
  77. package/dist/engine/seeds/seed-types-generator.d.ts.map +1 -1
  78. package/dist/engine/seeds/seed-types-generator.js +4 -5
  79. package/dist/engine/seeds/seed-validation.d.ts +5 -0
  80. package/dist/engine/seeds/seed-validation.d.ts.map +1 -1
  81. package/dist/engine/seeds/seed-validation.js +39 -19
  82. package/dist/engine/seeds/tag-migration.d.ts +73 -0
  83. package/dist/engine/seeds/tag-migration.d.ts.map +1 -0
  84. package/dist/engine/seeds/tag-migration.js +212 -0
  85. package/dist/engine/seeds/tag-options.d.ts +15 -0
  86. package/dist/engine/seeds/tag-options.d.ts.map +1 -0
  87. package/dist/engine/seeds/tag-options.js +111 -0
  88. package/dist/engine/seeds/tag-value.d.ts +31 -0
  89. package/dist/engine/seeds/tag-value.d.ts.map +1 -0
  90. package/dist/engine/seeds/tag-value.js +51 -0
  91. package/dist/engine/types.d.ts +59 -7
  92. package/dist/engine/types.d.ts.map +1 -1
  93. package/dist/engine/validation/cache.d.ts.map +1 -1
  94. package/dist/engine/validation/cache.js +20 -4
  95. package/dist/engine/validation/index.d.ts +12 -1
  96. package/dist/engine/validation/index.d.ts.map +1 -1
  97. package/dist/engine/validation/index.js +29 -33
  98. package/dist/engine/validation/primitives.d.ts +12 -0
  99. package/dist/engine/validation/primitives.d.ts.map +1 -1
  100. package/dist/engine/validation/primitives.js +22 -0
  101. package/dist/engine/validation/richtext-sanitizer.d.ts +9 -1
  102. package/dist/engine/validation/richtext-sanitizer.d.ts.map +1 -1
  103. package/dist/engine/validation/richtext-sanitizer.js +36 -7
  104. package/dist/engine/validation/schema-builders.d.ts +16 -0
  105. package/dist/engine/validation/schema-builders.d.ts.map +1 -1
  106. package/dist/engine/validation/schema-builders.js +125 -18
  107. package/dist/index.d.ts +34 -11
  108. package/dist/index.d.ts.map +1 -1
  109. package/dist/index.js +34 -11
  110. package/dist/media/file-types.d.ts +12 -0
  111. package/dist/media/file-types.d.ts.map +1 -1
  112. package/dist/media/file-types.js +24 -4
  113. package/dist/media/magic-bytes.d.ts.map +1 -1
  114. package/dist/media/magic-bytes.js +19 -9
  115. package/dist/media/media-transform.d.ts +8 -4
  116. package/dist/media/media-transform.d.ts.map +1 -1
  117. package/dist/media/media-transform.js +10 -6
  118. package/dist/media/media.repository.d.ts +5 -0
  119. package/dist/media/media.repository.d.ts.map +1 -1
  120. package/dist/notifications/notification-service.d.ts +1 -1
  121. package/dist/notifications/notification.repository.d.ts +11 -2
  122. package/dist/notifications/notification.repository.d.ts.map +1 -1
  123. package/dist/oauth/authorization-code.repository.d.ts +10 -5
  124. package/dist/oauth/authorization-code.repository.d.ts.map +1 -1
  125. package/dist/oauth/pkce.d.ts +1 -1
  126. package/dist/oauth/role-guard.d.ts +30 -11
  127. package/dist/oauth/role-guard.d.ts.map +1 -1
  128. package/dist/oauth/role-guard.js +27 -5
  129. package/dist/oauth/scopes.d.ts +1 -1
  130. package/dist/oauth/scopes.d.ts.map +1 -1
  131. package/dist/oauth/token.repository.d.ts +2 -0
  132. package/dist/oauth/token.repository.d.ts.map +1 -1
  133. package/dist/queue/queue.interface.d.ts +9 -1
  134. package/dist/queue/queue.interface.d.ts.map +1 -1
  135. package/dist/queue/queue.stub.d.ts +1 -1
  136. package/dist/queue/queue.stub.d.ts.map +1 -1
  137. package/dist/queue/queue.stub.js +2 -3
  138. package/dist/queue/scheduler.stub.d.ts.map +1 -1
  139. package/dist/rate-limit/token-bucket-rate-limiter.d.ts +7 -0
  140. package/dist/rate-limit/token-bucket-rate-limiter.d.ts.map +1 -1
  141. package/dist/rate-limit/token-bucket-rate-limiter.js +66 -0
  142. package/dist/rbac/invitation.repository.d.ts +28 -10
  143. package/dist/rbac/invitation.repository.d.ts.map +1 -1
  144. package/dist/rbac/types.d.ts +40 -28
  145. package/dist/rbac/types.d.ts.map +1 -1
  146. package/dist/rbac/types.js +2 -1
  147. package/dist/schema/manifest-seeds.js +3 -3
  148. package/dist/search/search.repository.d.ts +5 -4
  149. package/dist/search/search.repository.d.ts.map +1 -1
  150. package/dist/search/vector-extractor.d.ts.map +1 -1
  151. package/dist/search/vector-extractor.js +23 -2
  152. package/dist/search/vector.repository.d.ts +2 -1
  153. package/dist/search/vector.repository.d.ts.map +1 -1
  154. package/dist/security/time-trap-token.repository.d.ts +5 -3
  155. package/dist/security/time-trap-token.repository.d.ts.map +1 -1
  156. package/dist/settings/site-settings.repository.d.ts +8 -0
  157. package/dist/settings/site-settings.repository.d.ts.map +1 -1
  158. package/dist/transfer/flat-seed.d.ts.map +1 -1
  159. package/dist/transfer/flat-seed.js +6 -1
  160. package/dist/transfer/row-mapping.d.ts.map +1 -1
  161. package/dist/transfer/row-mapping.js +9 -2
  162. package/package.json +8 -3
  163. package/dist/.tsbuildinfo +0 -1
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { cleanString, isPlainObject } from '../validation/primitives.js';
4
+ import { normalizeTagValue, TAG_MAX_LENGTH, TAG_OPTIONS_MAX_COUNT } from './tag-value.js';
5
+ /** Accepted option color format: 6-digit hex, the value `<input type="color">` emits. */
6
+ export const TAG_OPTION_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
7
+ /** True only for a `tags` branch with `multiple === false`. Omitted `multiple` on `tags` means multiple choice. */
8
+ export function isSingleChoiceTags(branch) {
9
+ return branch.type === 'tags' && branch.multiple === false;
10
+ }
11
+ /** Option values in declared order; `[]` when the branch has no options. */
12
+ export function tagOptionValues(branch) {
13
+ return (branch.options ?? []).map(option => option.value);
14
+ }
15
+ /** The option whose `value` equals `value` exactly, or `undefined`. */
16
+ export function findTagOption(branch, value) {
17
+ return branch.options?.find(option => option.value === value);
18
+ }
19
+ /** Issues for one option entry; `seenValues` accumulates the values already declared on the branch. */
20
+ function optionEntryIssues(prefix, entry, index, seenValues) {
21
+ if (typeof entry === 'string') {
22
+ return [`${prefix}option #${index} must be an object { value, color }, not a plain string.`];
23
+ }
24
+ if (!isPlainObject(entry)) {
25
+ return [`${prefix}option #${index} must be an object { value, color }.`];
26
+ }
27
+ const issues = [];
28
+ const { value, color } = entry;
29
+ const validValue = typeof value === 'string' && value !== '' && value === cleanString(value);
30
+ if (!validValue) {
31
+ issues.push(`${prefix}option #${index} value must be a non-empty string without leading or trailing whitespace.`);
32
+ }
33
+ else {
34
+ const normalized = normalizeTagValue(value);
35
+ if (!normalized.ok) {
36
+ issues.push(normalized.reason === 'too-long'
37
+ ? `${prefix}option '${value.slice(0, 40)}' value is too long (max ${TAG_MAX_LENGTH} characters).`
38
+ : `${prefix}option #${index} value is not valid text (lone surrogate).`);
39
+ }
40
+ else if (normalized.value !== value) {
41
+ issues.push(`${prefix}option #${index} value must already be normalized: no invisible or control characters, `
42
+ + `single spaces, Unicode NFC (use ${JSON.stringify(normalized.value)}).`);
43
+ }
44
+ else if (seenValues.has(value)) {
45
+ issues.push(`${prefix}duplicate option value '${value}'.`);
46
+ }
47
+ else {
48
+ seenValues.add(value);
49
+ }
50
+ }
51
+ if (typeof color !== 'string' || !TAG_OPTION_COLOR_RE.test(color)) {
52
+ const optionLabel = validValue ? `'${value}'` : `#${index}`;
53
+ issues.push(`${prefix}option ${optionLabel} color must be a hex color #RRGGBB (got '${String(color)}').`);
54
+ }
55
+ return issues;
56
+ }
57
+ function branchTagOptionsIssues(prefix, branch) {
58
+ const raw = branch;
59
+ const issues = [];
60
+ if (raw.options !== undefined) {
61
+ if (branch.type === 'text') {
62
+ return [
63
+ `${prefix}options are not supported on type 'text'. `
64
+ + `Use type 'tags' with options (add multiple: false for a single choice).`,
65
+ ];
66
+ }
67
+ if (branch.type !== 'tags') {
68
+ return [`${prefix}options are only supported on type 'tags' (got '${String(branch.type)}').`];
69
+ }
70
+ }
71
+ if (branch.type === 'tags') {
72
+ if (raw.multiple !== undefined && typeof raw.multiple !== 'boolean') {
73
+ issues.push(`${prefix}multiple must be a boolean.`);
74
+ }
75
+ if (raw.options !== undefined) {
76
+ if (!Array.isArray(raw.options) || raw.options.length === 0) {
77
+ issues.push(`${prefix}options must be a non-empty array of { value, color } objects.`);
78
+ }
79
+ else if (raw.options.length > TAG_OPTIONS_MAX_COUNT) {
80
+ issues.push(`${prefix}options must contain at most ${TAG_OPTIONS_MAX_COUNT} entries (got ${raw.options.length}).`);
81
+ }
82
+ else {
83
+ const seenValues = new Set();
84
+ raw.options.forEach((entry, index) => {
85
+ issues.push(...optionEntryIssues(prefix, entry, index, seenValues));
86
+ });
87
+ }
88
+ }
89
+ }
90
+ return issues;
91
+ }
92
+ /**
93
+ * Seed-definition issues for `options` / `multiple` on this branch AND, recursively, its repeater sub-fields.
94
+ * Pure, never throws, works on untyped input (stored JSON may carry any shape). `[]` = valid.
95
+ */
96
+ export function tagOptionsIssues(branch) {
97
+ const issues = branchTagOptionsIssues(`branch '${branch.alias}': `, branch);
98
+ const subFields = Array.isArray(branch.fields) ? branch.fields : [];
99
+ for (const sub of subFields) {
100
+ issues.push(...subFieldIssues(branch.alias, sub));
101
+ }
102
+ return issues;
103
+ }
104
+ function subFieldIssues(parentAlias, sub) {
105
+ const issues = branchTagOptionsIssues(`branch '${parentAlias}': sub-field '${sub.alias}': `, sub);
106
+ const nested = Array.isArray(sub.fields) ? sub.fields : [];
107
+ for (const child of nested) {
108
+ issues.push(...subFieldIssues(sub.alias, child));
109
+ }
110
+ return issues;
111
+ }
@@ -0,0 +1,31 @@
1
+ /** Max tags stored in one `tags` field. Tags flag small things; more than this stops being a label set. */
2
+ export declare const TAG_MAX_COUNT = 10;
3
+ /** Max user-perceived characters (grapheme clusters) in one tag, so one emoji or `é` counts as 1. */
4
+ export declare const TAG_MAX_LENGTH = 25;
5
+ /** Hard UTF-8 byte ceiling per tag: bounds stacked combining marks ("zalgo") that fit in 25 graphemes. */
6
+ export declare const TAG_MAX_BYTES = 100;
7
+ /** Max options a closed-vocabulary `tags` branch may declare. */
8
+ export declare const TAG_OPTIONS_MAX_COUNT = 50;
9
+ /** Array size accepted before cleaning, so blanks the cleaner drops do not eat the real budget. */
10
+ export declare const TAG_RAW_MAX_COUNT = 50;
11
+ /** Why a tag value cannot be stored. */
12
+ export type TagValueRejection = 'ill-formed' | 'too-long';
13
+ /** Outcome of {@link normalizeTagValue}: the canonical value (possibly `''`), or why it was rejected. */
14
+ export type TagValueResult = {
15
+ readonly ok: true;
16
+ readonly value: string;
17
+ } | {
18
+ readonly ok: false;
19
+ readonly reason: TagValueRejection;
20
+ };
21
+ /**
22
+ * Canonical form of one tag, the single rule shared by payload validation, seed option validation and filters:
23
+ * invisible and control characters removed (an emoji-gluing joiner survives), every whitespace run (tab, newline,
24
+ * NBSP, line separator…) collapsed to one space, trimmed, Unicode NFC. Case is preserved (`Foo` ≠ `foo`).
25
+ *
26
+ * `ok: true` with `''` means the tag was blank after cleaning: callers decide whether to drop or reject it.
27
+ * Rejected, never repaired: a lone UTF-16 surrogate (not valid UTF-8) and a value over
28
+ * {@link TAG_MAX_LENGTH} characters or {@link TAG_MAX_BYTES} bytes. Pure, never throws.
29
+ */
30
+ export declare function normalizeTagValue(raw: string): TagValueResult;
31
+ //# sourceMappingURL=tag-value.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag-value.d.ts","sourceRoot":"","sources":["../../../src/engine/seeds/tag-value.ts"],"names":[],"mappings":"AAKA,2GAA2G;AAC3G,eAAO,MAAM,aAAa,KAAK,CAAA;AAC/B,qGAAqG;AACrG,eAAO,MAAM,cAAc,KAAK,CAAA;AAChC,0GAA0G;AAC1G,eAAO,MAAM,aAAa,MAAM,CAAA;AAChC,iEAAiE;AACjE,eAAO,MAAM,qBAAqB,KAAK,CAAA;AACvC,mGAAmG;AACnG,eAAO,MAAM,iBAAiB,KAAK,CAAA;AAgBnC,wCAAwC;AACxC,MAAM,MAAM,iBAAiB,GAAG,YAAY,GAAG,UAAU,CAAA;AAEzD,yGAAyG;AACzG,MAAM,MAAM,cAAc,GACtB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAC7C;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CAAE,CAAA;AAM9D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAc7D"}
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Copyright (c) 2024–2026 Flavio De Musso
3
+ import { byteLength, stripControlChars } from '../validation/primitives.js';
4
+ /** Max tags stored in one `tags` field. Tags flag small things; more than this stops being a label set. */
5
+ export const TAG_MAX_COUNT = 10;
6
+ /** Max user-perceived characters (grapheme clusters) in one tag, so one emoji or `é` counts as 1. */
7
+ export const TAG_MAX_LENGTH = 25;
8
+ /** Hard UTF-8 byte ceiling per tag: bounds stacked combining marks ("zalgo") that fit in 25 graphemes. */
9
+ export const TAG_MAX_BYTES = 100;
10
+ /** Max options a closed-vocabulary `tags` branch may declare. */
11
+ export const TAG_OPTIONS_MAX_COUNT = 50;
12
+ /** Array size accepted before cleaning, so blanks the cleaner drops do not eat the real budget. */
13
+ export const TAG_RAW_MAX_COUNT = 50;
14
+ /**
15
+ * Invisible or layout-altering code points that render as nothing but make two tags differ:
16
+ * C1 controls, soft hyphen, Mongolian vowel separator, zero-width space/non-joiner, LRM/RLM,
17
+ * bidi embeddings and isolates (spoofing), word joiner and invisible operators, BOM, and the
18
+ * Unicode "tag" block (invisible payloads). The zero-width joiner is handled by {@link STRAY_JOINER_RE}.
19
+ */
20
+ const INVISIBLE_CHARS_RE = /[\u0080-\u009F\u00AD\u180E\u200B\u200C\u200E\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u{E0000}-\u{E007F}]/gu;
21
+ /** Zero-width joiner not gluing two emoji pictographs; between two (family, profession emoji) it is meaningful and kept. */
22
+ const STRAY_JOINER_RE = /(?<!\p{Extended_Pictographic}\uFE0F?)\u200D|\u200D(?!\p{Extended_Pictographic})/gu;
23
+ const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
24
+ const UNICODE_WHITESPACE_RUN_RE = /\s+/g;
25
+ const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
26
+ function graphemeCount(input) {
27
+ return [...graphemeSegmenter.segment(input)].length;
28
+ }
29
+ /**
30
+ * Canonical form of one tag, the single rule shared by payload validation, seed option validation and filters:
31
+ * invisible and control characters removed (an emoji-gluing joiner survives), every whitespace run (tab, newline,
32
+ * NBSP, line separator…) collapsed to one space, trimmed, Unicode NFC. Case is preserved (`Foo` ≠ `foo`).
33
+ *
34
+ * `ok: true` with `''` means the tag was blank after cleaning: callers decide whether to drop or reject it.
35
+ * Rejected, never repaired: a lone UTF-16 surrogate (not valid UTF-8) and a value over
36
+ * {@link TAG_MAX_LENGTH} characters or {@link TAG_MAX_BYTES} bytes. Pure, never throws.
37
+ */
38
+ export function normalizeTagValue(raw) {
39
+ if (LONE_SURROGATE_RE.test(raw))
40
+ return { ok: false, reason: 'ill-formed' };
41
+ const value = stripControlChars(raw)
42
+ .replaceAll(INVISIBLE_CHARS_RE, '')
43
+ .replaceAll(STRAY_JOINER_RE, '')
44
+ .replaceAll(UNICODE_WHITESPACE_RUN_RE, ' ')
45
+ .trim()
46
+ .normalize('NFC');
47
+ if (byteLength(value) > TAG_MAX_BYTES || graphemeCount(value) > TAG_MAX_LENGTH) {
48
+ return { ok: false, reason: 'too-long' };
49
+ }
50
+ return { ok: true, value };
51
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { FileAccept } from '../media/file-types.js';
8
8
  import type { DashboardView } from '../dashboard-layout/view-authorization.js';
9
+ import type { LocaleConfig } from './localization/localization.js';
9
10
  /** All supported field value types for a Branch. */
10
11
  export type BranchType = 'text' | 'number' | 'boolean' | 'json' | 'date' | 'richtext' | 'file' | 'tags' | 'relation' | 'repeater';
11
12
  /** Specialized configuration for a branch of type 'number'. */
@@ -57,6 +58,16 @@ export interface ActorContext {
57
58
  userId?: string;
58
59
  role?: string;
59
60
  }
61
+ /** One entry of a `tags` branch's closed vocabulary. Array order is significant (menu, kanban columns, sort). */
62
+ export interface TagOption {
63
+ /**
64
+ * Stored value and identity. Unique within the branch; compared exactly (case-sensitive, never trimmed).
65
+ * Must already be in canonical form (`normalizeTagValue(value).value === value`) and at most `TAG_MAX_LENGTH` characters.
66
+ */
67
+ value: string;
68
+ /** Presentation-only color, `#RRGGBB`. Lives in the seed only — never persisted in entries. */
69
+ color: string;
70
+ }
60
71
  /** Branch: definition of a single field. `alias` is the SQL column name. */
61
72
  export interface Branch {
62
73
  /**
@@ -83,16 +94,19 @@ export interface Branch {
83
94
  */
84
95
  format?: 'plain' | 'markdown' | 'html' | 'date' | 'datetime' | 'asset-list';
85
96
  /**
86
- * Optional cardinality for media fields:
87
- * - false/undefined: single asset (string URL)
88
- * - true: asset list (string[] URL)
97
+ * Cardinality:
98
+ * - `file`: false/undefined → single URL; true → URL list.
99
+ * - `relation`: false/undefined → FK column; true → junction table.
100
+ * - `tags`: undefined/true → multiple choice (default); false → at most one value. The value is always an array.
89
101
  */
90
102
  multiple?: boolean;
91
103
  /**
92
- * Predefined vocabulary for tag/select/multiselect fields.
93
- * Static list defined in the Seed (not persisted to the DB).
104
+ * Closed vocabulary — valid ONLY on `tags` (any other type is a fatal seed error).
105
+ * When present: non-empty, at most `TAG_OPTIONS_MAX_COUNT` entries, unique canonical values, `#RRGGBB` colors;
106
+ * written values must be one of the option values.
107
+ * Static seed metadata, never persisted in entries.
94
108
  */
95
- options?: string[];
109
+ options?: TagOption[];
96
110
  /** Required on create — generates NOT NULL in generateCreateTable. */
97
111
  requiredOnCreate?: boolean;
98
112
  /** Required on update. */
@@ -123,6 +137,15 @@ export interface Branch {
123
137
  numberOptions?: NumberFieldOptions;
124
138
  /** Advanced options for file fields. Ignored if type !== 'file'. */
125
139
  fileOptions?: FileFieldOptions;
140
+ /**
141
+ * Field-level localization. When true, the branch's existing column stores a locale dictionary
142
+ * (`{"it": "Scarpa", "en": "Shoe"}`) instead of a single value. Metadata-only: toggling it never
143
+ * emits DDL and never rewrites rows — values written before the toggle stay readable as-is.
144
+ * Valid only on top-level `text | richtext | json` branches with `plain` storage (not
145
+ * `confidential` / `restricted`); never on repeater sub-fields. Enforced by seed-validation.ts
146
+ * (Fatal 17). Default: false.
147
+ */
148
+ localized?: boolean;
126
149
  /**
127
150
  * Slug of the referenced Seed (without the `content_` prefix).
128
151
  * REQUIRED when `type === 'relation'`. Ignored otherwise.
@@ -213,6 +236,13 @@ export interface Seed {
213
236
  * Default: false.
214
237
  */
215
238
  allowDrafts?: boolean;
239
+ /**
240
+ * Enables the Trash for this content type. When true, `content_{slug}` carries a
241
+ * `deleted_at INTEGER NULL` column, `DELETE` becomes a reversible soft delete, and every
242
+ * read excludes trashed rows unless the caller opts in via `SelectOptions.trashed`.
243
+ * Default: false.
244
+ */
245
+ softDelete?: boolean;
216
246
  /**
217
247
  * Alias of the branch used as the entry's human-readable name (e.g. "title", "name", "author").
218
248
  * Required — UIs use it for display without heuristics.
@@ -235,7 +265,7 @@ export interface Seed {
235
265
  /** Comparison/matching operator applied by a filter condition. */
236
266
  export type FilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'contains' | 'not_contains' | 'starts_with' | 'ends_with' | 'is_empty' | 'is_not_empty' | 'in' | 'not_in' | 'has_tag' | 'has_any_tag' | 'has_all_tags';
237
267
  /** Declared value type of a filtered column, used to normalize/coerce filter values before binding. */
238
- export type FilterType = 'text' | 'number' | 'date' | 'boolean' | 'tags' | 'select' | 'system' | 'json';
268
+ export type FilterType = 'text' | 'number' | 'date' | 'boolean' | 'tags' | 'select' | 'system' | 'json' | 'repeater';
239
269
  /** A single operator + value pair applied to a FilterGroup's column. */
240
270
  export interface FilterCondition {
241
271
  /** Operator to apply. */
@@ -251,6 +281,20 @@ export interface FilterGroup {
251
281
  /** Conditions applied to this column, ANDed together. */
252
282
  conditions: FilterCondition[];
253
283
  }
284
+ /**
285
+ * Soft-delete visibility for a read.
286
+ * - 'active' (default) — only rows with `deleted_at IS NULL`
287
+ * - 'trashed' — only rows with `deleted_at IS NOT NULL` (the Trash view)
288
+ * - 'any' — no predicate (restore/purge lookups, reconciliation)
289
+ * Ignored entirely for seeds without `softDelete: true`.
290
+ */
291
+ export type TrashedMode = 'active' | 'trashed' | 'any';
292
+ /** The language a read resolves localized branches to (consumed by `buildSelectQuery`). */
293
+ export interface SelectLocale {
294
+ /** Locale that filters and ORDER BY compare in. Must match `LOCALE_CODE_RE`; the builder throws otherwise. */
295
+ readonly code: string;
296
+ readonly config: LocaleConfig;
297
+ }
254
298
  export interface SelectOptions {
255
299
  /** Filter groups. Joined by `filterLogic` (default AND); conditions within a group are always ANDed. */
256
300
  filters?: FilterGroup[];
@@ -268,6 +312,8 @@ export interface SelectOptions {
268
312
  };
269
313
  /** Filters by status. null = no status filter. */
270
314
  status?: string | null;
315
+ /** Soft-delete visibility. Defaults to 'active': omitting it can never leak a trashed row. */
316
+ trashed?: TrashedMode;
271
317
  /** Full-text search — uses FTS5 if the seed has indexable richtext/text branches. */
272
318
  search?: string;
273
319
  /** Column projection. Empty = SELECT *. */
@@ -284,6 +330,12 @@ export interface SelectOptions {
284
330
  * while keeping the join/where clauses intact for accurate counts.
285
331
  */
286
332
  isCount?: boolean;
333
+ /**
334
+ * When set, filters and ORDER BY on localized branches compare the value resolved to `locale.code`
335
+ * (requested → default → first stored translation → legacy raw value) instead of the stored JSON.
336
+ * Absent: every column is compared raw, exactly as before localization existed.
337
+ */
338
+ locale?: SelectLocale;
287
339
  }
288
340
  /** SQL string paired with its ordered parameter bindings, ready for a D1 `.bind(...)` call. */
289
341
  export interface ParameterizedQuery {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/engine/types.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,2CAA2C,CAAA;AAE9E,oDAAoD;AACpD,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAA;AAEjI,+DAA+D;AAC/D,MAAM,WAAW,kBAAkB;IAEjC,gDAAgD;IAChD,MAAM,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAA;IAC1D,uFAAuF;IACvF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAA;IAGlB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAA;IAGnD,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,6BAA6B;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,6DAA6D;AAC7D,MAAM,WAAW,gBAAgB;IAC/B;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;IACnB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAED,sFAAsF;AACtF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,cAAc,GAAG,YAAY,CAAA;AAEtF,wFAAwF;AACxF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,QAAQ,GAAG,eAAe,GAAG,QAAQ,CAAA;IAC3C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;CACd;AAED,4EAA4E;AAC5E,MAAM,WAAW,MAAM;IACrB;;;;;;;;OAQG;IACH,EAAE,EAAE,MAAM,CAAA;IACV,uGAAuG;IACvG,KAAK,EAAE,MAAM,CAAA;IACb,wBAAwB;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,2GAA2G;IAC3G,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,kBAAkB;IAClB,IAAI,EAAE,UAAU,CAAA;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,GAAG,YAAY,CAAA;IAC3E;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;IAClB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,0BAA0B;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE;QACT,2DAA2D;QAC3D,cAAc,CAAC,EAAE,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,cAAc,GAAG,YAAY,CAAA;QAC3F,iDAAiD;QACjD,OAAO,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,GAAG,kBAAkB,GAAG,QAAQ,GAAG,UAAU,GAAG,cAAc,GAAG,YAAY,CAAA;QACnH,mEAAmE;QACnE,UAAU,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAA;QACzC,gFAAgF;QAChF,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,yFAAyF;QACzF,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,uFAAuF;QACvF,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,4EAA4E;QAC5E,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,wIAAwI;QACxI,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAA;IACD,wEAAwE;IACxE,aAAa,CAAC,EAAE,kBAAkB,CAAA;IAClC,oEAAoE;IACpE,WAAW,CAAC,EAAE,gBAAgB,CAAA;IAE9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IAEnB;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,CAAA;IAE9C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IAEjB;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IAEjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,6GAA6G;AAC7G,MAAM,WAAW,mBAAmB;IAClC,uFAAuF;IACvF,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8EAA8E;IAC9E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oDAAoD;IACpD,MAAM,CAAC,EAAE,OAAO,CAAA;IAChB,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE;QACT,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,MAAM,CAAC,EAAE,OAAO,CAAA;QAChB,UAAU,CAAC,EAAE,OAAO,CAAA;KACrB,CAAA;IACD;;;;OAIG;IACH,KAAK,CAAC,EAAE,aAAa,EAAE,CAAA;CACxB;AAED,iDAAiD;AACjD,MAAM,WAAW,IAAI;IACnB,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAA;IACZ,yBAAyB;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,qFAAqF;IACrF,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,6FAA6F;IAC7F,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,8FAA8F;IAC9F,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB;;;OAGG;IACH,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,mBAAmB,CAAA;IAC/B;;wGAEoG;IACpG,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAID,kEAAkE;AAClE,MAAM,MAAM,cAAc,GACtB,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,UAAU,GACV,cAAc,GACd,aAAa,GACb,WAAW,GACX,UAAU,GACV,cAAc,GACd,IAAI,GACJ,QAAQ,GACR,SAAS,GACT,aAAa,GACb,cAAc,CAAA;AAElB,uGAAuG;AACvG,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEvG,wEAAwE;AACxE,MAAM,WAAW,eAAe;IAC9B,yBAAyB;IACzB,EAAE,EAAE,cAAc,CAAA;IAClB,sGAAsG;IACtG,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,MAAM,EAAE,GAAG,MAAM,EAAE,CAAA;CAC9D;AAED,MAAM,WAAW,WAAW;IAC1B,yFAAyF;IACzF,MAAM,EAAE,MAAM,CAAA;IACd,uEAAuE;IACvE,IAAI,EAAE,UAAU,CAAA;IAChB,yDAAyD;IACzD,UAAU,EAAE,eAAe,EAAE,CAAA;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,wGAAwG;IACxG,OAAO,CAAC,EAAE,WAAW,EAAE,CAAA;IACvB,sEAAsE;IACtE,WAAW,CAAC,EAAE,KAAK,GAAG,IAAI,CAAA;IAC1B,oEAAoE;IACpE,OAAO,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,CAAA;IACjD,+BAA+B;IAC/B,UAAU,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC9C,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,qFAAqF;IACrF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,2CAA2C;IAC3C,MAAM,CAAC,EAAE,MAAM,EAAE,CAAA;IACjB;kFAC8E;IAC9E,WAAW,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAA;IACxD;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC,2DAA2D;IAC3D,GAAG,EAAE,MAAM,CAAA;IACX,sDAAsD;IACtD,QAAQ,EAAE,CAAC,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,EAAE,CAAA;CAC/C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/engine/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/cache.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAsB,IAAI,EAAwC,MAAM,aAAa,CAAA;AAEjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAgIjD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAsCjH"}
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/cache.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAsB,IAAI,EAAwC,MAAM,aAAa,CAAA;AAEjG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA2IjD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,eAAe,GAAG,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CA+CjH"}
@@ -1,7 +1,9 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (c) 2024–2026 Flavio De Musso
3
3
  import { z } from 'zod';
4
- import { schemaForBranch } from './schema-builders.js';
4
+ import { isSingleChoiceTags, tagOptionValues } from '../seeds/tag-options.js';
5
+ import { isLocalizedBranch } from '../localization/localization.js';
6
+ import { schemaForBranch, localizedSchema } from './schema-builders.js';
5
7
  /** Max entries kept per cache map before evicting the least recently used. */
6
8
  const SEED_SCHEMA_CACHE_MAX_SIZE = 150;
7
9
  /** Cache storing compiled Zod schemas for relation-free seeds. */
@@ -54,7 +56,8 @@ function buildBranchFingerprint(branch) {
54
56
  a: branch.alias,
55
57
  t: branch.type,
56
58
  f: branch.format ?? null,
57
- m: branch.multiple === true,
59
+ m: branch.type === 'tags' ? !isSingleChoiceTags(branch) : branch.multiple === true,
60
+ o: branch.options ? tagOptionValues(branch) : null,
58
61
  rc: branch.requiredOnCreate === true,
59
62
  ru: branch.requiredOnUpdate === true,
60
63
  n: branch.numberOptions ?? null,
@@ -62,6 +65,7 @@ function buildBranchFingerprint(branch) {
62
65
  mi: branch.minItems ?? null,
63
66
  ma: branch.maxItems ?? null,
64
67
  sub: branch.fields?.map(buildBranchFingerprint) ?? null,
68
+ lo: branch.localized === true,
65
69
  };
66
70
  }
67
71
  /**
@@ -88,6 +92,9 @@ function buildCacheKey(seed, options) {
88
92
  options.allowNull ? '1' : '0',
89
93
  options.enforceRequiredFields ? '1' : '0',
90
94
  String(options.maxTextLength),
95
+ options.localeConfig
96
+ ? `${options.localeConfig.defaultLocale}>${options.localeConfig.locales.join(',')}`
97
+ : '-',
91
98
  ].join('|');
92
99
  }
93
100
  /**
@@ -125,8 +132,17 @@ export function compileSeedSchema(seed, options) {
125
132
  const requiredFlag = options.operation === 'create' ? 'requiredOnCreate' : 'requiredOnUpdate';
126
133
  const shape = {};
127
134
  for (const branch of seed.branches) {
128
- const branchSchema = schemaForBranch(branch, options);
129
- const isRequired = branch[requiredFlag] && options.enforceRequiredFields;
135
+ const branchLocaleConfig = options.localeConfig && isLocalizedBranch(branch) ? options.localeConfig : undefined;
136
+ const branchSchema = branchLocaleConfig
137
+ ? localizedSchema(branch, options, branchLocaleConfig)
138
+ : schemaForBranch(branch, options);
139
+ // A localized branch uses patch semantics on update (buildLocalizedPatch omits it entirely when no
140
+ // locale was touched), so the object schema must accept the key being absent even when required —
141
+ // detectMissingRequired() checks the *value* (default-locale presence/emptiness) with full patch
142
+ // awareness; a non-optional key here would fail the whole payload's parse and silently drop every
143
+ // other submitted field with it.
144
+ const isPatchExempt = branchLocaleConfig != null && options.operation === 'update';
145
+ const isRequired = branch[requiredFlag] && options.enforceRequiredFields && !isPatchExempt;
130
146
  shape[branch.alias] = isRequired ? branchSchema : branchSchema.optional();
131
147
  }
132
148
  const compiled = z.object(shape).strict();
@@ -1,5 +1,8 @@
1
1
  import type { Seed } from '../types.js';
2
2
  import type { IIdGenerator } from '../../common/id-generator.js';
3
+ import { type LocaleConfig } from '../localization/localization.js';
4
+ import { isEffectivelyEmpty } from './emptiness.js';
5
+ export { isEffectivelyEmpty };
3
6
  export { resolveFileOptions } from './file-branch.js';
4
7
  /**
5
8
  * @module Engine/Validation
@@ -58,6 +61,13 @@ export interface ValidateSeedPayloadOptions {
58
61
  * Do NOT pass a concrete class — inject via the middleware / factory.
59
62
  */
60
63
  idGenerator?: IIdGenerator;
64
+ /**
65
+ * Project language configuration. When provided, branches with `localized: true` accept a plain
66
+ * value (stored under `defaultLocale`) or a locale dictionary, and validate to a `LocalizedPatch`.
67
+ * When omitted, localized branches validate exactly like their base type — the pre-localization
68
+ * contract. `requiredOnCreate` / `requiredOnUpdate` then check the default locale only.
69
+ */
70
+ localeConfig?: LocaleConfig;
61
71
  }
62
72
  /**
63
73
  * The structured result returned by the seed payload validation process.
@@ -89,9 +99,10 @@ export type ResolvedOptions = {
89
99
  enforceRequiredFields: boolean;
90
100
  maxTextLength: number;
91
101
  idGenerator: IIdGenerator | undefined;
102
+ localeConfig: LocaleConfig | undefined;
92
103
  };
93
104
  /** Set of valid content status values. */
94
- declare const STATUS_VALUES: readonly ['draft', 'review', 'published'];
105
+ declare const STATUS_VALUES: readonly ["draft", "review", "published"];
95
106
  /**
96
107
  * Validates and sanitizes a seed payload against its schema definition.
97
108
  *
@@ -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,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"}
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;AAChE,OAAO,EAAuC,KAAK,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAExG,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD,OAAO,EAAE,kBAAkB,EAAE,CAAA;AAG7B,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;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B;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;IACrC,YAAY,EAAE,YAAY,GAAG,SAAS,CAAA;CACvC,CAAA;AAKD,0CAA0C;AAC1C,QAAA,MAAM,aAAa,2CAA4C,CAAA;AAyR/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,CA0D3B;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAE5F"}
@@ -1,10 +1,10 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // Copyright (c) 2024–2026 Flavio De Musso
3
3
  import { z } from 'zod';
4
- import { isRichtextEnvelopeV1 } from '../../content/richtext/richtext.js';
5
- import { cleanString, isPlainObject } from './primitives.js';
6
- import { isRichtextDocEmpty } from './richtext-sanitizer.js';
4
+ import { isLocalizedBranch, toLocalizedPatch } from '../localization/localization.js';
7
5
  import { compileSeedSchema } from './cache.js';
6
+ import { isEffectivelyEmpty } from './emptiness.js';
7
+ export { isEffectivelyEmpty };
8
8
  // Re-export the public file-branch symbol so the barrel surface stays complete.
9
9
  export { resolveFileOptions } from './file-branch.js';
10
10
  /** Default maximum character length allowed for text and rich text branches. */
@@ -13,31 +13,6 @@ const DEFAULT_MAX_TEXT_LENGTH = 50_000;
13
13
  const STATUS_VALUES = ['draft', 'review', 'published'];
14
14
  /** Zod schema for content status enum validation. */
15
15
  const statusSchema = z.enum(STATUS_VALUES);
16
- /**
17
- * Checks if a value is effectively empty (e.g. null, undefined, empty string, empty array, or empty rich text).
18
- *
19
- * @param value - The value to check.
20
- * @param branchType - The type of the branch being checked.
21
- * @returns True if effectively empty, false otherwise.
22
- */
23
- function isEffectivelyEmpty(value, branchType) {
24
- if (value === null || value === undefined)
25
- return true;
26
- if (typeof value === 'string')
27
- return cleanString(value).length === 0;
28
- if (Array.isArray(value))
29
- return value.length === 0;
30
- if (isPlainObject(value)) {
31
- // Richtext envelope: delegate to the richtext emptiness check.
32
- if (isRichtextEnvelopeV1(value))
33
- return isRichtextDocEmpty(value.doc);
34
- // Raw TipTap doc root: only delegate for richtext branches to avoid misclassifying generic json fields.
35
- if (branchType === 'richtext' && value.type === 'doc')
36
- return isRichtextDocEmpty(value);
37
- return Object.keys(value).length === 0;
38
- }
39
- return false;
40
- }
41
16
  /**
42
17
  * Detects missing or empty required fields based on whether the operation is create or update.
43
18
  *
@@ -57,7 +32,13 @@ function detectMissingRequired(seed, rawPayload, filtered, parsedData, parseSucc
57
32
  const isRequired = op === 'create' ? branch.requiredOnCreate : branch.requiredOnUpdate;
58
33
  if (!isRequired)
59
34
  continue;
35
+ const isLocalizedUpdate = op === 'update' && options.localeConfig != null && isLocalizedBranch(branch);
60
36
  if (!Object.hasOwn(rawPayload, branch.alias)) {
37
+ // Localized branches use patch semantics on update: the dashboard omits the branch entirely
38
+ // when no locale was touched (see buildLocalizedPatch), which is equivalent to a patch that
39
+ // does not name the default locale — leave it as stored rather than flagging it missing.
40
+ if (isLocalizedUpdate)
41
+ continue;
61
42
  missing.push(branch.alias);
62
43
  details.push({
63
44
  field: branch.alias,
@@ -67,7 +48,18 @@ function detectMissingRequired(seed, rawPayload, filtered, parsedData, parseSucc
67
48
  });
68
49
  continue;
69
50
  }
70
- const candidate = parseSucceeded ? parsedData[branch.alias] : filtered[branch.alias];
51
+ const value = parseSucceeded ? parsedData[branch.alias] : filtered[branch.alias];
52
+ let candidate = value;
53
+ if (options.localeConfig && isLocalizedBranch(branch)) {
54
+ // A localized branch is present when its default locale is; other locales are optional (brief §4).
55
+ const { defaultLocale } = options.localeConfig;
56
+ const patch = toLocalizedPatch(value, options.localeConfig);
57
+ // An update that does not name the default locale leaves it as stored — the write path merges
58
+ // (applyLocalizedPatch) — so saving one translation cannot trip requiredOnUpdate.
59
+ if (op === 'update' && !Object.hasOwn(patch, defaultLocale))
60
+ continue;
61
+ candidate = patch[defaultLocale];
62
+ }
71
63
  if (isEffectivelyEmpty(candidate, branch.type)) {
72
64
  missing.push(branch.alias);
73
65
  details.push({
@@ -241,9 +233,8 @@ function processZodIssues(seed, issues, filtered, options) {
241
233
  continue;
242
234
  }
243
235
  // detectMissingRequired() already reports top-level required-and-missing branches;
244
- // skip the redundant native invalid_type/undefined issue Zod v4 raises for the same field.
245
- if (issue.code === 'invalid_type' &&
246
- options.enforceRequiredFields &&
236
+ // skip redundant Zod issues when a required top-level field is omitted/undefined.
237
+ if (options.enforceRequiredFields &&
247
238
  issuePath.length === 1 &&
248
239
  valByPath === undefined) {
249
240
  continue;
@@ -290,6 +281,7 @@ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
290
281
  enforceRequiredFields: options.enforceRequiredFields ?? true,
291
282
  maxTextLength: options.maxTextLength ?? DEFAULT_MAX_TEXT_LENGTH,
292
283
  idGenerator: options.idGenerator,
284
+ localeConfig: options.localeConfig,
293
285
  };
294
286
  const { filtered, unknown: preUnknown, details: preDetails } = splitUnknownAliases(seed, payload);
295
287
  const schema = compileSeedSchema(seed, resolved);
@@ -299,7 +291,11 @@ export function validateAndSanitizeSeedPayload(seed, payload, options = {}) {
299
291
  const dangerousFields = new Set();
300
292
  let data = {};
301
293
  if (parsed.success) {
302
- data = { ...parsed.data };
294
+ for (const [key, value] of Object.entries(parsed.data)) {
295
+ if (value !== undefined) {
296
+ data[key] = value;
297
+ }
298
+ }
303
299
  }
304
300
  else {
305
301
  const zodOutcome = processZodIssues(seed, parsed.error.issues, filtered, resolved);
@@ -22,6 +22,18 @@ export declare function cleanString(input: string): string;
22
22
  * @returns True if the value is a plain object, false otherwise.
23
23
  */
24
24
  export declare function isPlainObject(input: unknown): input is Record<string, unknown>;
25
+ /**
26
+ * Checks whether a JSON-like value nests deeper than `maxDepth`, without ever recursing
27
+ * past that bound. Safe to call on adversarial input BEFORE any operation (like
28
+ * `JSON.stringify`, which recurses unboundedly) that would otherwise overflow the call
29
+ * stack on a few thousand levels of nesting.
30
+ *
31
+ * @param value - The value to inspect.
32
+ * @param maxDepth - The maximum nesting depth allowed.
33
+ * @param depth - Current recursion depth (internal).
34
+ * @returns True if the value nests deeper than `maxDepth`.
35
+ */
36
+ export declare function exceedsMaxDepth(value: unknown, maxDepth: number, depth?: number): boolean;
25
37
  /**
26
38
  * Measures the real UTF-8 byte length of a string, not UTF-16 code units.
27
39
  * `.length` undercounts multi-byte chars (CJK, emoji) by up to 4x, which matters
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/primitives.ts"],"names":[],"mappings":"AAGA,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,QAAoD,CAAA;AAEpF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9E;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD"}
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../../src/engine/validation/primitives.ts"],"names":[],"mappings":"AAGA,mEAAmE;AACnE,eAAO,MAAM,mBAAmB,QAAoD,CAAA;AAEpF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAI9E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,OAAO,CASpF;AAID;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD"}
@@ -33,6 +33,28 @@ export function isPlainObject(input) {
33
33
  const proto = Object.getPrototypeOf(input);
34
34
  return proto === Object.prototype || proto === null;
35
35
  }
36
+ /**
37
+ * Checks whether a JSON-like value nests deeper than `maxDepth`, without ever recursing
38
+ * past that bound. Safe to call on adversarial input BEFORE any operation (like
39
+ * `JSON.stringify`, which recurses unboundedly) that would otherwise overflow the call
40
+ * stack on a few thousand levels of nesting.
41
+ *
42
+ * @param value - The value to inspect.
43
+ * @param maxDepth - The maximum nesting depth allowed.
44
+ * @param depth - Current recursion depth (internal).
45
+ * @returns True if the value nests deeper than `maxDepth`.
46
+ */
47
+ export function exceedsMaxDepth(value, maxDepth, depth = 0) {
48
+ if (depth > maxDepth)
49
+ return true;
50
+ if (Array.isArray(value)) {
51
+ return value.some((item) => exceedsMaxDepth(item, maxDepth, depth + 1));
52
+ }
53
+ if (isPlainObject(value)) {
54
+ return Object.values(value).some((item) => exceedsMaxDepth(item, maxDepth, depth + 1));
55
+ }
56
+ return false;
57
+ }
36
58
  const byteLengthEncoder = new TextEncoder();
37
59
  /**
38
60
  * Measures the real UTF-8 byte length of a string, not UTF-16 code units.