@object-ui/core 17.6.0 → 18.0.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 (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
@@ -0,0 +1,49 @@
1
+ /**
2
+ * ObjectUI — the field-read rule for a single record
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * The two members of a loaded permission policy the field-read rule asks.
10
+ *
11
+ * Structural on purpose: `usePermissions()` from `@object-ui/permissions`
12
+ * satisfies it as it is, and so does any caller-supplied policy of the same
13
+ * shape, so this package depends on neither React nor the permissions package.
14
+ */
15
+ interface FieldReadPolicy {
16
+ /** False until a policy has loaded; also false with no provider mounted. */
17
+ readonly isLoaded: boolean;
18
+ /** May the viewer READ `field` on `object`? */
19
+ checkField(object: string, field: string, action: 'read'): boolean;
20
+ }
21
+ /**
22
+ * `record` as the viewer may READ it on `objectName`: every field the loaded
23
+ * `policy` denies is removed, which leaves the row ObjectStack's `FieldMasker`
24
+ * already serves (objectui#10594).
25
+ *
26
+ * The renderer-side field-level-security rulings (objectui#7215 /
27
+ * objectui#7230) say FLS gates the OUTPUT: a value the loaded policy denies is
28
+ * not drawn, even when a backend serves it. Every surface that builds a display
29
+ * value from a whole row (a record title, a lookup label, a search-hit label, a
30
+ * person's name) calls this one function, so a denied field reads as an absent
31
+ * field on every surface at once, and the display ladder falls through to its
32
+ * next source exactly as it does for the stripped row. Field-LIST filters (which
33
+ * columns to head, which relations to `$expand`) are a different shape and are
34
+ * not this function.
35
+ *
36
+ * - `id` and `_id` are never judged, nor is any key in `extraKeep` (a declared
37
+ * id field): an id addresses the record and is not a field value the policy
38
+ * withholds, and the `Record #<id>` floor reads it.
39
+ * - Before a policy loads, with no policy, with no object to judge the fields
40
+ * against, or for a value that is not an object, `record` comes back as it
41
+ * is.
42
+ * - When nothing is withheld, the SAME object comes back, so a caller can tell
43
+ * "nothing withheld" from "something withheld" by identity and a cache keyed
44
+ * on the served row keeps hitting.
45
+ *
46
+ * Pure: it reads `policy.checkField` and never writes to `record`.
47
+ */
48
+ export declare function withoutDeniedFields<T>(record: T, policy: FieldReadPolicy | null | undefined, objectName: string | undefined, extraKeep?: readonly string[]): T;
49
+ export {};
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ObjectUI — the field-read rule for a single record
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * `record` as the viewer may READ it on `objectName`: every field the loaded
10
+ * `policy` denies is removed, which leaves the row ObjectStack's `FieldMasker`
11
+ * already serves (objectui#10594).
12
+ *
13
+ * The renderer-side field-level-security rulings (objectui#7215 /
14
+ * objectui#7230) say FLS gates the OUTPUT: a value the loaded policy denies is
15
+ * not drawn, even when a backend serves it. Every surface that builds a display
16
+ * value from a whole row (a record title, a lookup label, a search-hit label, a
17
+ * person's name) calls this one function, so a denied field reads as an absent
18
+ * field on every surface at once, and the display ladder falls through to its
19
+ * next source exactly as it does for the stripped row. Field-LIST filters (which
20
+ * columns to head, which relations to `$expand`) are a different shape and are
21
+ * not this function.
22
+ *
23
+ * - `id` and `_id` are never judged, nor is any key in `extraKeep` (a declared
24
+ * id field): an id addresses the record and is not a field value the policy
25
+ * withholds, and the `Record #<id>` floor reads it.
26
+ * - Before a policy loads, with no policy, with no object to judge the fields
27
+ * against, or for a value that is not an object, `record` comes back as it
28
+ * is.
29
+ * - When nothing is withheld, the SAME object comes back, so a caller can tell
30
+ * "nothing withheld" from "something withheld" by identity and a cache keyed
31
+ * on the served row keeps hitting.
32
+ *
33
+ * Pure: it reads `policy.checkField` and never writes to `record`.
34
+ */
35
+ export function withoutDeniedFields(record, policy, objectName, extraKeep) {
36
+ if (!policy?.isLoaded || !objectName || !record || typeof record !== 'object')
37
+ return record;
38
+ const shown = {};
39
+ let withheld = false;
40
+ for (const [key, value] of Object.entries(record)) {
41
+ if (key === 'id' ||
42
+ key === '_id' ||
43
+ extraKeep?.includes(key) ||
44
+ policy.checkField(objectName, key, 'read')) {
45
+ shown[key] = value;
46
+ }
47
+ else {
48
+ withheld = true;
49
+ }
50
+ }
51
+ return withheld ? shown : record;
52
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @object-ui/core - Validation Module
3
3
  *
4
- * Phase 3.5: Validation engine
4
+ * Phase 3.5: Validation module
5
5
  *
6
6
  * Object-level validation. The rule vocabulary is owned by
7
7
  * `@objectstack/spec/data` and derived in `@object-ui/types`; canonicity is
@@ -9,11 +9,15 @@
9
9
  *
10
10
  * The object-level rule ENGINE (`validators/`) is @deprecated (#3110) — the
11
11
  * server is the single implementation of rule enforcement. Field/record SHAPE
12
- * validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
13
- * is client-side form validation, not a mirror of a server rule.
12
+ * validation (`schema-validator.js`) is unaffected: it is client-side form
13
+ * validation, not a mirror of a server rule.
14
+ *
15
+ * The field-rule engine that once sat beside it (`ValidationEngine`, a second
16
+ * snake_case rule vocabulary no renderer consumed) was retired under ADR-0049
17
+ * enforce-or-remove (objectui#7659). Client-side field rules are compiled by
18
+ * `buildValidationRules` in `@object-ui/fields`.
14
19
  */
15
20
  export * from './required-presence.js';
16
21
  export * from './server-owned-value.js';
17
- export * from './validation-engine.js';
18
22
  export * from './schema-validator.js';
19
23
  export * from './validators/index.js';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @object-ui/core - Validation Module
3
3
  *
4
- * Phase 3.5: Validation engine
4
+ * Phase 3.5: Validation module
5
5
  *
6
6
  * Object-level validation. The rule vocabulary is owned by
7
7
  * `@objectstack/spec/data` and derived in `@object-ui/types`; canonicity is
@@ -9,11 +9,15 @@
9
9
  *
10
10
  * The object-level rule ENGINE (`validators/`) is @deprecated (#3110) — the
11
11
  * server is the single implementation of rule enforcement. Field/record SHAPE
12
- * validation (`schema-validator.js`, `validation-engine.js`) is unaffected: it
13
- * is client-side form validation, not a mirror of a server rule.
12
+ * validation (`schema-validator.js`) is unaffected: it is client-side form
13
+ * validation, not a mirror of a server rule.
14
+ *
15
+ * The field-rule engine that once sat beside it (`ValidationEngine`, a second
16
+ * snake_case rule vocabulary no renderer consumed) was retired under ADR-0049
17
+ * enforce-or-remove (objectui#7659). Client-side field rules are compiled by
18
+ * `buildValidationRules` in `@object-ui/fields`.
14
19
  */
15
20
  export * from './required-presence.js';
16
21
  export * from './server-owned-value.js';
17
- export * from './validation-engine.js';
18
22
  export * from './schema-validator.js';
19
23
  export * from './validators/index.js';
@@ -69,7 +69,7 @@ export interface SchemaNodeValidationResult {
69
69
  * }
70
70
  * ```
71
71
  */
72
- export declare function validateSchema(schema: any, path?: string): SchemaNodeValidationResult;
72
+ export declare function validateSchema(schema: unknown, path?: string): SchemaNodeValidationResult;
73
73
  /**
74
74
  * Assert that a schema is valid, throwing an error if not
75
75
  *
@@ -5,6 +5,83 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ import { nodeSlotValues, nodeSlotsFor } from '@object-ui/types';
9
+ import { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
10
+ import { isUnevaluablePredicate } from '../evaluator/unevaluablePredicate.js';
11
+ /**
12
+ * The rule for a PREDICATE GATE key (`visible` / `disabled`) — objectui#6505.
13
+ *
14
+ * ## What this rule used to say, and what it cost
15
+ *
16
+ * Both keys read `typeof value === 'boolean'`, message `"<key> must be a
17
+ * boolean"`. Both keys are EXPRESSIONS everywhere else in the system: AGENTS.md
18
+ * §4 declares the protocol as `hidden?: string; // expression` /
19
+ * `disabled?: string; // expression`, `SchemaRenderer` evaluates them through
20
+ * `hasDeclaredPredicate` + `evaluateCondition`, `@objectstack/spec` normalizes
21
+ * every authored predicate into a `{ dialect, source }` envelope, and the
22
+ * objectui#3862 / objectui#3955 rulings are entirely about which EXPRESSION
23
+ * spellings count as declared. So the node the docs teach —
24
+ * `{ type: 'button', disabled: "${record.stage == 'closed'}" }` — was reported
25
+ * `disabled must be a boolean` by the dev-mode validator and its host element
26
+ * got `data-obj-schema-invalid`, the cue apps hang a red outline off.
27
+ *
28
+ * `BASE_SCHEMA_RULES` was the ONE place in the repo that disagreed with the
29
+ * protocol, so this restores declared = enforced rather than widening a
30
+ * contract: the accept set moves to exactly what the runtime already accepts.
31
+ *
32
+ * ## Why `hasDeclaredPredicate` and not a check written here
33
+ *
34
+ * `evaluator/declaredPredicate.ts` is the repo's single definition of "is a
35
+ * predicate gate DECLARED on this value?" (objectui#3850's ruling; nothing in
36
+ * the repo asks that question anywhere else). A second, hand-rolled answer in
37
+ * this table is how the validator and the renderer come to disagree about the
38
+ * same value — which is the defect class this rule was already an instance of.
39
+ * The delegation is behavioural, so it is pinned behaviourally: the drift pin in
40
+ * `__tests__/predicate-valued-gate-rules.test.ts` asserts this rule's verdict
41
+ * equals `boolean || (declared && !unevaluable)` across every probe — the second
42
+ * half since objectui#11358, see below.
43
+ *
44
+ * ## Why the boolean arm survives even though it is subsumed
45
+ *
46
+ * `hasDeclaredPredicate(true)` and `hasDeclaredPredicate(false)` are both
47
+ * `true` today (`toPredicateInput` returns a boolean unchanged), so the first
48
+ * arm changes no verdict. It is kept because it makes this rule's accept set a
49
+ * provable SUPERSET of the one it replaces, locally: a future narrowing on the
50
+ * declaredness side cannot silently start reporting `disabled: false` — the
51
+ * most explicit gate an author can write — as an invalid schema.
52
+ *
53
+ * ## What it still REFUSES (the half that is not negotiable)
54
+ *
55
+ * `null`, `''`, whitespace-only predicate text and the empty / blank-`source`
56
+ * envelope (objectui#3960) — what `hasDeclaredPredicate` calls "not declared" —
57
+ * and a value that IS declared but cannot be evaluated: a number, `{}`, an
58
+ * array, an envelope with no string `source` (`isUnevaluablePredicate`).
59
+ * Every one of those was refused before this change too — the accept set
60
+ * widens and nothing refused becomes accepted. Dropping the two keys from this
61
+ * table was the option this fix was explicitly forbidden to take: an absent
62
+ * rule reports nothing at all, which is gate weakening wearing the same green.
63
+ *
64
+ * ## Why "declared" alone stopped being this rule's answer (objectui#11358)
65
+ *
66
+ * Until objectui#11358 every declared gate was also an evaluable one, so
67
+ * "declared" answered both questions. That ruling made a present value with no
68
+ * evaluable `source` DECLARED (and faulting) so that the runtime fails it in
69
+ * its key's fault direction instead of reading it as no gate — and this rule
70
+ * asks a different question, "is this a predicate the runtime can evaluate?".
71
+ * Reading `hasDeclaredPredicate` alone would have widened the accept set to
72
+ * exactly the shapes that ruling exists to stop. So the rule refuses the
73
+ * unevaluable state by the same internal definition the runtime uses for it,
74
+ * not by a list written here.
75
+ */
76
+ function predicateGateRule(key) {
77
+ return {
78
+ required: false,
79
+ validate: (value) => typeof value === 'boolean' || (!isUnevaluablePredicate(value) && hasDeclaredPredicate(value)),
80
+ message: `${key} must be a boolean or a declared predicate: an expression string ` +
81
+ `(bare, e.g. "record.stage == 'closed'", or the "\${...}" template ` +
82
+ `spelling), or a { dialect, source } expression envelope`,
83
+ };
84
+ }
8
85
  /**
9
86
  * Validation rules for base schema
10
87
  */
@@ -24,31 +101,39 @@ const BASE_SCHEMA_RULES = {
24
101
  validate: (value) => typeof value === 'string',
25
102
  message: 'className must be a string'
26
103
  },
27
- visible: {
28
- required: false,
29
- validate: (value) => typeof value === 'boolean',
30
- message: 'visible must be a boolean'
31
- },
32
- disabled: {
33
- required: false,
34
- validate: (value) => typeof value === 'boolean',
35
- message: 'disabled must be a boolean'
36
- }
104
+ // objectui#6505 — both keys are EXPRESSIONS in the protocol, not booleans.
105
+ // See `predicateGateRule` above for the accept set and what it still refuses.
106
+ visible: predicateGateRule('visible'),
107
+ disabled: predicateGateRule('disabled')
37
108
  };
109
+ /**
110
+ * The ONE narrowing between {@link validateSchema}'s `unknown` parameter and
111
+ * every rule below it (objectui#8416).
112
+ *
113
+ * The VERDICT is the guard this replaces, spelled the same way: the
114
+ * `!schema || typeof schema !== 'object'` test that used to sit at the top of
115
+ * `validateBaseSchema`. `null`, `undefined` and every primitive are refused;
116
+ * arrays and plain objects pass, exactly as before.
117
+ *
118
+ * What changes is WHERE it runs. Only ONE of the four rules ever had it, and
119
+ * `validateSchema` calls all four unconditionally: `validateFormSchema` read
120
+ * `schema.type` and `validateChildren` read `schema.children` with no guard at
121
+ * all, so `validateSchema(null)` and `validateSchema(undefined)` threw a raw
122
+ * `TypeError: Cannot read properties of null (reading 'type')` — measured on
123
+ * 256c709e2 — instead of returning the `INVALID_SCHEMA` result this function's
124
+ * own contract promises, and `isValidSchema(null)` threw instead of answering
125
+ * `false`. `SchemaRenderer` wraps its call in a `try`/`catch` whose comment
126
+ * reads "Validator itself failed — surface but don't crash render"; that catch
127
+ * is where the crash was landing.
128
+ */
129
+ function isSchemaNodeShape(value) {
130
+ return !!value && typeof value === 'object';
131
+ }
38
132
  /**
39
133
  * Validate a schema against base rules
40
134
  */
41
135
  function validateBaseSchema(schema, path = 'schema') {
42
136
  const errors = [];
43
- if (!schema || typeof schema !== 'object') {
44
- errors.push({
45
- path,
46
- message: 'Schema must be an object',
47
- type: 'error',
48
- code: 'INVALID_SCHEMA'
49
- });
50
- return errors;
51
- }
52
137
  // Validate required and optional properties
53
138
  Object.entries(BASE_SCHEMA_RULES).forEach(([key, rule]) => {
54
139
  const value = schema[key];
@@ -72,56 +157,66 @@ function validateBaseSchema(schema, path = 'schema') {
72
157
  return errors;
73
158
  }
74
159
  /**
75
- * Validate CRUD schema specific properties
160
+ * TOMBSTONE table — node `type` spellings this renderer has RETIRED, mapped to
161
+ * the prescription an author must follow instead (ADR-0049 enforce-or-remove).
162
+ *
163
+ * A retired spelling is not merely absent from this file. Absence here means
164
+ * {@link validateSchema} runs `validateBaseSchema` — which only asks that
165
+ * `type` be a non-empty string — finds nothing type-specific to say, and
166
+ * returns `valid: true`. That silence is the failure mode this table exists to
167
+ * prevent: the author is told their schema is fine and then gets the OBJUI-001
168
+ * "Unknown component type" panel at render time, one layer too late to act on.
169
+ * So a retired spelling is refused BY NAME, with the migration in the message.
170
+ *
171
+ * `crud` (objectui#5373, maintainer ruling of 2026-08-20): `CRUDSchema` carried
172
+ * FOUR declaration faces — the TS interface, the zod mirror, a dedicated branch
173
+ * in this very function, and `CRUDBuilder` — and never once had a registered
174
+ * renderer, for the whole life of the key. Every face taught an author (and an
175
+ * AI author reading the published reference page) that the type existed. The
176
+ * branch that used to sit here is what made this validator the loudest of the
177
+ * four lies: it read `schema.type === 'crud'`, checked that `columns` was an
178
+ * array, and returned no error — an affirmative "your CRUD schema is valid"
179
+ * for a node that renders nothing.
180
+ *
181
+ * Keyed by the authored spelling, and quantified over the table rather than
182
+ * written per spelling, so the next retirement closes this face the day it
183
+ * lands here.
76
184
  */
77
- function validateCRUDSchema(schema, path = 'schema') {
78
- const errors = [];
79
- if (schema.type === 'crud') {
80
- // Check required properties for CRUD
81
- if (!schema.columns || !Array.isArray(schema.columns)) {
82
- errors.push({
83
- path: `${path}.columns`,
84
- message: 'CRUD schema requires columns array',
85
- type: 'error',
86
- code: 'MISSING_COLUMNS'
87
- });
88
- }
89
- if (!schema.api && !schema.dataSource) {
90
- errors.push({
91
- path: `${path}.api`,
92
- message: 'CRUD schema requires api or dataSource',
93
- type: 'warning',
94
- code: 'MISSING_DATA_SOURCE'
95
- });
96
- }
97
- // Validate columns
98
- if (schema.columns && Array.isArray(schema.columns)) {
99
- schema.columns.forEach((column, index) => {
100
- if (!column.name) {
101
- errors.push({
102
- path: `${path}.columns[${index}]`,
103
- message: 'Column requires name property',
104
- type: 'error',
105
- code: 'MISSING_COLUMN_NAME'
106
- });
107
- }
108
- });
109
- }
110
- // Validate fields if present
111
- if (schema.fields && Array.isArray(schema.fields)) {
112
- schema.fields.forEach((field, index) => {
113
- if (!field.name) {
114
- errors.push({
115
- path: `${path}.fields[${index}]`,
116
- message: 'Field requires name property',
117
- type: 'error',
118
- code: 'MISSING_FIELD_NAME'
119
- });
120
- }
121
- });
122
- }
123
- }
124
- return errors;
185
+ const RETIRED_NODE_TYPES = Object.freeze({
186
+ crud: "Node type `crud` was RETIRED (objectui#5373, ADR-0049 enforce-or-remove). " +
187
+ "`CRUDSchema` declared it in four places and no renderer ever registered it, " +
188
+ "so a node spelling it painted the OBJUI-001 \"Unknown component type\" panel. " +
189
+ "Compose the shapes that DO render instead: `object-grid` for the record " +
190
+ "table with its toolbar, filters, pagination and row/batch actions, " +
191
+ "`object-form` for the create/edit form, and `detail` for the record view.",
192
+ });
193
+ /**
194
+ * Refuse a retired node-type spelling by name.
195
+ *
196
+ * Severity is `error`, not `warning`, and that is the whole point of the
197
+ * retirement: a warning leaves `result.valid === true`, so `assertValidSchema`
198
+ * would still not throw and `isValidSchema` would still answer `true` — the
199
+ * same silence, one console line louder.
200
+ *
201
+ * `hasOwnProperty` rather than a plain index read: a `type` of `'constructor'`
202
+ * or `'toString'` reaches `Object.prototype` and would answer truthy.
203
+ *
204
+ * Reached for every node in the tree, not just the root — {@link validateSchema}
205
+ * is what `validateChildren` recurses with, so a retired spelling nested inside
206
+ * a `children` array is refused with its own path.
207
+ */
208
+ function validateRetiredNodeType(schema, path = 'schema') {
209
+ const type = schema.type;
210
+ if (typeof type !== 'string')
211
+ return [];
212
+ if (!Object.prototype.hasOwnProperty.call(RETIRED_NODE_TYPES, type))
213
+ return [];
214
+ return [{
215
+ path: `${path}.type`,
216
+ message: RETIRED_NODE_TYPES[type],
217
+ type: 'error',
218
+ code: 'RETIRED_TYPE'
219
+ }];
125
220
  }
126
221
  /**
127
222
  * The ONE namespace a form field's widget id may name — objectui#5254,
@@ -228,8 +323,15 @@ function validateFieldWidgetNamespace(field, path) {
228
323
  function validateFormSchema(schema, path = 'schema') {
229
324
  const errors = [];
230
325
  if (schema.type === 'form') {
231
- if (schema.fields && Array.isArray(schema.fields)) {
232
- schema.fields.forEach((field, index) => {
326
+ const fields = schema.fields;
327
+ if (Array.isArray(fields)) {
328
+ fields.forEach((entry, index) => {
329
+ // Narrowed before it is read, for the same reason the root is: a
330
+ // `fields` array is authored data too, and reading `field.name` off
331
+ // whatever an author put there made `fields: [null]` throw the same raw
332
+ // `TypeError` a `null` root did. A non-object entry keeps its old
333
+ // verdict — every key reads `undefined`, so `MISSING_FIELD_NAME`.
334
+ const field = isSchemaNodeShape(entry) ? entry : {};
233
335
  if (!field.name) {
234
336
  errors.push({
235
337
  path: `${path}.fields[${index}]`,
@@ -245,11 +347,11 @@ function validateFormSchema(schema, path = 'schema') {
245
347
  errors.push(namespaceError);
246
348
  }
247
349
  // Check for duplicate field names
248
- const duplicates = schema.fields.filter((f) => f.name === field.name);
350
+ const duplicates = fields.filter((other) => (isSchemaNodeShape(other) ? other.name : undefined) === field.name);
249
351
  if (duplicates.length > 1) {
250
352
  errors.push({
251
353
  path: `${path}.fields[${index}]`,
252
- message: `Duplicate field name: ${field.name}`,
354
+ message: `Duplicate field name: ${String(field.name)}`,
253
355
  type: 'warning',
254
356
  code: 'DUPLICATE_FIELD_NAME'
255
357
  });
@@ -259,26 +361,53 @@ function validateFormSchema(schema, path = 'schema') {
259
361
  }
260
362
  return errors;
261
363
  }
364
+ /** `schema.items[0].content[1]`: the child's path, spelled the way `children[0]` already is. */
365
+ function spellSlotPath(path, segments) {
366
+ return segments.reduce((spelled, segment) => (typeof segment === 'number' ? `${spelled}[${segment}]` : `${spelled}.${segment}`), path);
367
+ }
262
368
  /**
263
- * Validate child schemas recursively
369
+ * Validate child schemas recursively: what `children` holds on every node,
370
+ * and what the node slots declared for this node's type hold.
264
371
  */
265
372
  function validateChildren(schema, path = 'schema') {
266
373
  const errors = [];
267
- const children = schema.children || schema.body;
374
+ // One child-list spelling. This walker resolved `children || body` for ANY
375
+ // node type, so it outlived every per-registration read of the dialect —
376
+ // objectui#6771 retired it, and a recursive validator that still descended
377
+ // `body` on every node would keep validating a child list no renderer puts
378
+ // on the page.
379
+ const children = schema.children;
268
380
  if (children) {
269
381
  if (Array.isArray(children)) {
270
382
  children.forEach((child, index) => {
271
- if (typeof child === 'object' && child !== null) {
383
+ if (isSchemaNodeShape(child)) {
272
384
  const childResult = validateSchema(child, `${path}.children[${index}]`);
273
385
  errors.push(...childResult.errors, ...childResult.warnings);
274
386
  }
275
387
  });
276
388
  }
277
- else if (typeof children === 'object' && children !== null) {
389
+ else if (isSchemaNodeShape(children)) {
278
390
  const childResult = validateSchema(children, `${path}.children`);
279
391
  errors.push(...childResult.errors, ...childResult.warnings);
280
392
  }
281
393
  }
394
+ // The node slots THIS type's renderer reads — a dialog's `trigger`, a tab
395
+ // item's `content`, a page's `regions[].components` — from the one
396
+ // declaration in `@object-ui/types` (`nodeSlotsFor`, objectui#11170), which
397
+ // the `objectui check` gate and the SDUI parser read too. Per type, never a
398
+ // second generic spelling: `page:card`'s retired `body` is walked because
399
+ // that renderer is measured still painting it; `body` on any other type is
400
+ // not, which is the objectui#6771 line above, kept.
401
+ if (typeof schema.type === 'string') {
402
+ for (const slot of nodeSlotsFor(schema.type)) {
403
+ for (const { segments, value } of nodeSlotValues(schema, slot.path)) {
404
+ if (!isSchemaNodeShape(value))
405
+ continue;
406
+ const childResult = validateSchema(value, spellSlotPath(path, segments));
407
+ errors.push(...childResult.errors, ...childResult.warnings);
408
+ }
409
+ }
410
+ }
282
411
  return errors;
283
412
  }
284
413
  /**
@@ -304,13 +433,26 @@ function validateChildren(schema, path = 'schema') {
304
433
  */
305
434
  export function validateSchema(schema, path = 'schema') {
306
435
  const allErrors = [];
307
- // Validate base schema
308
- allErrors.push(...validateBaseSchema(schema, path));
309
- // Validate type-specific schemas
310
- allErrors.push(...validateCRUDSchema(schema, path));
311
- allErrors.push(...validateFormSchema(schema, path));
312
- // Validate children recursively
313
- allErrors.push(...validateChildren(schema, path));
436
+ // Narrow ONCE, here, before any rule reads a key off the value — see
437
+ // `isSchemaNodeShape`. Every rule below is handed a value already proven to
438
+ // be an object, which is why none of them re-tests it.
439
+ if (!isSchemaNodeShape(schema)) {
440
+ allErrors.push({
441
+ path,
442
+ message: 'Schema must be an object',
443
+ type: 'error',
444
+ code: 'INVALID_SCHEMA'
445
+ });
446
+ }
447
+ else {
448
+ // Validate base schema
449
+ allErrors.push(...validateBaseSchema(schema, path));
450
+ // Validate type-specific schemas
451
+ allErrors.push(...validateRetiredNodeType(schema, path));
452
+ allErrors.push(...validateFormSchema(schema, path));
453
+ // Validate children recursively
454
+ allErrors.push(...validateChildren(schema, path));
455
+ }
314
456
  const errors = allErrors.filter(e => e.type === 'error');
315
457
  const warnings = allErrors.filter(e => e.type === 'warning');
316
458
  return {
@@ -21,7 +21,11 @@
21
21
  * 2. the create-mode static `required` rule (#4069) —
22
22
  * `isRequiredInForm` in `@object-ui/plugin-form`;
23
23
  * 3. the create-mode `requiredWhen` rule (#4085) —
24
- * {@link isServerOwnedValue}, read by `resolveFieldRuleState`.
24
+ * {@link isServerOwnedValue}, read by `resolveFieldRuleState`;
25
+ * 4. the "Set automatically when saved." hint (objectui#12108) — the form
26
+ * renderer reads the same {@link isServerOwnedValue} answer it hands to
27
+ * `resolveFieldRuleState`, so the hint and the lowered `required` are
28
+ * one verdict.
25
29
  */
26
30
  export declare function isRuntimeDefault(v: unknown): boolean;
27
31
  /**
@@ -67,7 +67,11 @@ function isExpressionEnvelope(v) {
67
67
  * 2. the create-mode static `required` rule (#4069) —
68
68
  * `isRequiredInForm` in `@object-ui/plugin-form`;
69
69
  * 3. the create-mode `requiredWhen` rule (#4085) —
70
- * {@link isServerOwnedValue}, read by `resolveFieldRuleState`.
70
+ * {@link isServerOwnedValue}, read by `resolveFieldRuleState`;
71
+ * 4. the "Set automatically when saved." hint (objectui#12108) — the form
72
+ * renderer reads the same {@link isServerOwnedValue} answer it hands to
73
+ * `resolveFieldRuleState`, so the hint and the lowered `required` are
74
+ * one verdict.
71
75
  */
72
76
  export function isRuntimeDefault(v) {
73
77
  return isRuntimeDefaultToken(v) || isExpressionEnvelope(v);
@@ -186,7 +186,7 @@ export declare class ObjectValidationEngine {
186
186
  * - no `initialStates` declared → allow (legacy no-op)
187
187
  * - field absent from the write → allow (requiredness is not our job)
188
188
  * - value empty → allow (ditto)
189
- * - value ∉ initialStates → reject (the FSM entry point, #3165)
189
+ * - value ∉ initialStates → reject (the FSM entry point, objectstack-ai/objectstack#3165)
190
190
  *
191
191
  * UPDATE
192
192
  * - field absent from the write → allow (no transition to police)
@@ -53,7 +53,15 @@ function priorityOf(rule) {
53
53
  * Simple expression evaluator using a simple parser (no dynamic code execution)
54
54
  *
55
55
  * SECURITY: This implementation parses expressions into an AST and evaluates them
56
- * without using eval() or new Function(). It supports:
56
+ * without using eval() or new Function(). That is a remediation, not a style
57
+ * preference: an earlier revision compiled the predicate with
58
+ * `new Function(...contextKeys, "'use strict'; return (" + expression + ")")`,
59
+ * and CodeQL flagged it as "Unsafe code constructed from library input" —
60
+ * expressions reach this class from validation metadata, so that shape is a
61
+ * code-injection vector in any consumer that renders metadata it does not
62
+ * control. Never reintroduce eval(), new Function() or any other dynamic-code
63
+ * path here to buy expressiveness or speed; extend the parser, or inject a
64
+ * richer evaluator through the constructor (see NOT CEL below). Supports:
57
65
  * - Comparison operators: ==, !=, >, <, >=, <=
58
66
  * - Logical operators: &&, ||, !
59
67
  * - Property access: record.field, record['field']
@@ -72,7 +80,10 @@ function priorityOf(rule) {
72
80
  * pre-check agree with the server on richer predicates, pass a CEL-backed
73
81
  * `ValidationExpressionEvaluator` to the constructor.
74
82
  *
75
- * @see https://github.com/objectstack-ai/objectui/blob/main/SECURITY_FIX_SUMMARY.md
83
+ * The write-up this block used to link to (`SECURITY_FIX_SUMMARY.md`) was
84
+ * deleted with no successor; its load-bearing reasoning is inline above, and
85
+ * the original text remains readable at
86
+ * `git show ea72f1886^:SECURITY_FIX_SUMMARY.md`.
76
87
  */
77
88
  class SimpleExpressionEvaluator {
78
89
  evaluate(expression, context) {
@@ -384,7 +395,7 @@ export class ObjectValidationEngine {
384
395
  * - no `initialStates` declared → allow (legacy no-op)
385
396
  * - field absent from the write → allow (requiredness is not our job)
386
397
  * - value empty → allow (ditto)
387
- * - value ∉ initialStates → reject (the FSM entry point, #3165)
398
+ * - value ∉ initialStates → reject (the FSM entry point, objectstack-ai/objectstack#3165)
388
399
  *
389
400
  * UPDATE
390
401
  * - field absent from the write → allow (no transition to police)