@object-ui/core 17.5.0 → 17.7.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 (157) hide show
  1. package/CHANGELOG.md +7404 -0
  2. package/README.md +127 -26
  3. package/dist/actions/ActionEngine.d.ts +5 -3
  4. package/dist/actions/ActionEngine.js +14 -10
  5. package/dist/actions/ActionRunner.d.ts +363 -30
  6. package/dist/actions/ActionRunner.js +544 -58
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +75 -13
  9. package/dist/actions/actionKeys.js +131 -16
  10. package/dist/actions/actionResponse.js +3 -3
  11. package/dist/actions/index.d.ts +2 -0
  12. package/dist/actions/index.js +2 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/actions/recordIdParam.d.ts +77 -0
  16. package/dist/actions/recordIdParam.js +37 -0
  17. package/dist/actions/serverActionHandler.d.ts +1 -1
  18. package/dist/actions/serverActionHandler.js +1 -1
  19. package/dist/adapters/ValueDataSource.d.ts +6 -1
  20. package/dist/adapters/ValueDataSource.js +1090 -98
  21. package/dist/adapters/batchTransaction.d.ts +1 -1
  22. package/dist/adapters/batchTransaction.js +1 -1
  23. package/dist/builder/schema-builder.d.ts +38 -81
  24. package/dist/builder/schema-builder.js +33 -152
  25. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  26. package/dist/data-scope/ViewDataProvider.js +21 -1
  27. package/dist/data-scope/element-data-source.d.ts +261 -7
  28. package/dist/data-scope/element-data-source.js +342 -52
  29. package/dist/data-scope/index.d.ts +9 -4
  30. package/dist/data-scope/index.js +9 -4
  31. package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
  32. package/dist/evaluator/ExpressionEvaluator.js +226 -13
  33. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  34. package/dist/evaluator/FormulaFunctions.js +166 -27
  35. package/dist/evaluator/SafeExpressionParser.js +0 -4
  36. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  37. package/dist/evaluator/declaredPredicate.js +118 -17
  38. package/dist/evaluator/fieldRules.d.ts +107 -4
  39. package/dist/evaluator/fieldRules.js +231 -35
  40. package/dist/evaluator/index.d.ts +2 -0
  41. package/dist/evaluator/index.js +2 -0
  42. package/dist/evaluator/listConditional.d.ts +125 -9
  43. package/dist/evaluator/listConditional.js +167 -26
  44. package/dist/evaluator/optionRules.d.ts +126 -0
  45. package/dist/evaluator/optionRules.js +141 -4
  46. package/dist/evaluator/predicateInput.d.ts +15 -4
  47. package/dist/evaluator/predicateInput.js +35 -2
  48. package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
  49. package/dist/evaluator/rowPredicateCanon.js +158 -0
  50. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  51. package/dist/evaluator/subjectPermissions.js +44 -0
  52. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  53. package/dist/evaluator/unevaluablePredicate.js +70 -0
  54. package/dist/index.d.ts +18 -0
  55. package/dist/index.js +97 -2
  56. package/dist/protocols/index.d.ts +0 -1
  57. package/dist/protocols/index.js +9 -1
  58. package/dist/registry/Registry.d.ts +397 -48
  59. package/dist/registry/Registry.js +263 -11
  60. package/dist/registry/WidgetRegistry.js +7 -3
  61. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  62. package/dist/registry/html-tier-intrinsics.js +110 -0
  63. package/dist/registry/public-blocks.d.ts +8 -0
  64. package/dist/registry/public-blocks.js +57 -0
  65. package/dist/theme/ThemeEngine.js +1 -1
  66. package/dist/theme/index.d.ts +1 -1
  67. package/dist/theme/index.js +1 -1
  68. package/dist/types/index.d.ts +92 -13
  69. package/dist/utils/chart-category-key.d.ts +82 -0
  70. package/dist/utils/chart-category-key.js +84 -0
  71. package/dist/utils/chart-measure-key.d.ts +64 -0
  72. package/dist/utils/chart-measure-key.js +65 -0
  73. package/dist/utils/chart-presentation.d.ts +188 -0
  74. package/dist/utils/chart-presentation.js +345 -0
  75. package/dist/utils/chart-series.d.ts +271 -31
  76. package/dist/utils/chart-series.js +478 -55
  77. package/dist/utils/column-identity.d.ts +43 -0
  78. package/dist/utils/column-identity.js +47 -1
  79. package/dist/utils/column-sortability.d.ts +157 -0
  80. package/dist/utils/column-sortability.js +217 -0
  81. package/dist/utils/dashboard-filters.d.ts +38 -6
  82. package/dist/utils/dashboard-filters.js +107 -61
  83. package/dist/utils/dataset-format.d.ts +211 -30
  84. package/dist/utils/dataset-format.js +407 -52
  85. package/dist/utils/date-display.d.ts +354 -0
  86. package/dist/utils/date-display.js +463 -0
  87. package/dist/utils/date-macros.js +7 -0
  88. package/dist/utils/debug.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 +171 -11
  94. package/dist/utils/expand-fields.js +171 -11
  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 +152 -4
  98. package/dist/utils/filter-converter.js +1092 -22
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -34
  101. package/dist/utils/freeze-schema.d.ts +38 -1
  102. package/dist/utils/freeze-schema.js +10 -0
  103. package/dist/utils/grouping-fields.d.ts +61 -0
  104. package/dist/utils/grouping-fields.js +84 -0
  105. package/dist/utils/humanize-label.d.ts +60 -0
  106. package/dist/utils/humanize-label.js +62 -0
  107. package/dist/utils/managedBy.d.ts +30 -5
  108. package/dist/utils/managedBy.js +5 -5
  109. package/dist/utils/native-date-value.d.ts +48 -0
  110. package/dist/utils/native-date-value.js +167 -0
  111. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  112. package/dist/utils/non-grid-row-ceiling.js +141 -0
  113. package/dist/utils/normalize-list-view.d.ts +131 -1
  114. package/dist/utils/normalize-list-view.js +248 -6
  115. package/dist/utils/number-display.d.ts +70 -17
  116. package/dist/utils/number-display.js +34 -12
  117. package/dist/utils/object-aggregate-query.d.ts +113 -0
  118. package/dist/utils/object-aggregate-query.js +50 -0
  119. package/dist/utils/parent-scope.d.ts +123 -0
  120. package/dist/utils/parent-scope.js +140 -0
  121. package/dist/utils/predicate-fields.d.ts +62 -4
  122. package/dist/utils/predicate-fields.js +169 -4
  123. package/dist/utils/record-color.d.ts +75 -0
  124. package/dist/utils/record-color.js +65 -0
  125. package/dist/utils/record-source.d.ts +298 -0
  126. package/dist/utils/record-source.js +331 -0
  127. package/dist/utils/record-title.d.ts +148 -8
  128. package/dist/utils/record-title.js +220 -26
  129. package/dist/utils/reference-keys.d.ts +21 -22
  130. package/dist/utils/reference-keys.js +532 -29
  131. package/dist/utils/retired-field-types.d.ts +90 -0
  132. package/dist/utils/retired-field-types.js +124 -0
  133. package/dist/utils/sort-query.d.ts +128 -26
  134. package/dist/utils/sort-query.js +240 -13
  135. package/dist/utils/text-comparand.d.ts +92 -0
  136. package/dist/utils/text-comparand.js +99 -0
  137. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  138. package/dist/utils/unmaterialized-fields.js +57 -0
  139. package/dist/utils/without-denied-fields.d.ts +49 -0
  140. package/dist/utils/without-denied-fields.js +52 -0
  141. package/dist/validation/index.d.ts +9 -4
  142. package/dist/validation/index.js +9 -4
  143. package/dist/validation/schema-validator.d.ts +1 -1
  144. package/dist/validation/schema-validator.js +305 -82
  145. package/dist/validation/server-owned-value.d.ts +62 -0
  146. package/dist/validation/server-owned-value.js +99 -0
  147. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  148. package/dist/validation/validators/object-validation-engine.js +14 -5
  149. package/package.json +8 -9
  150. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  151. package/dist/__benchmarks__/core.bench.js +0 -53
  152. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  153. package/dist/data-scope/DataScopeManager.js +0 -229
  154. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  155. package/dist/protocols/ResponsiveProtocol.js +0 -158
  156. package/dist/validation/validation-engine.d.ts +0 -88
  157. package/dist/validation/validation-engine.js +0 -434
@@ -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,10 +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
- export * from './validation-engine.js';
21
+ export * from './server-owned-value.js';
17
22
  export * from './schema-validator.js';
18
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,10 +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
- export * from './validation-engine.js';
21
+ export * from './server-owned-value.js';
17
22
  export * from './schema-validator.js';
18
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,82 @@
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 { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
9
+ import { isUnevaluablePredicate } from '../evaluator/unevaluablePredicate.js';
10
+ /**
11
+ * The rule for a PREDICATE GATE key (`visible` / `disabled`) — objectui#6505.
12
+ *
13
+ * ## What this rule used to say, and what it cost
14
+ *
15
+ * Both keys read `typeof value === 'boolean'`, message `"<key> must be a
16
+ * boolean"`. Both keys are EXPRESSIONS everywhere else in the system: AGENTS.md
17
+ * §4 declares the protocol as `hidden?: string; // expression` /
18
+ * `disabled?: string; // expression`, `SchemaRenderer` evaluates them through
19
+ * `hasDeclaredPredicate` + `evaluateCondition`, `@objectstack/spec` normalizes
20
+ * every authored predicate into a `{ dialect, source }` envelope, and the
21
+ * objectui#3862 / objectui#3955 rulings are entirely about which EXPRESSION
22
+ * spellings count as declared. So the node the docs teach —
23
+ * `{ type: 'button', disabled: "${record.stage == 'closed'}" }` — was reported
24
+ * `disabled must be a boolean` by the dev-mode validator and its host element
25
+ * got `data-obj-schema-invalid`, the cue apps hang a red outline off.
26
+ *
27
+ * `BASE_SCHEMA_RULES` was the ONE place in the repo that disagreed with the
28
+ * protocol, so this restores declared = enforced rather than widening a
29
+ * contract: the accept set moves to exactly what the runtime already accepts.
30
+ *
31
+ * ## Why `hasDeclaredPredicate` and not a check written here
32
+ *
33
+ * `evaluator/declaredPredicate.ts` is the repo's single definition of "is a
34
+ * predicate gate DECLARED on this value?" (objectui#3850's ruling; nothing in
35
+ * the repo asks that question anywhere else). A second, hand-rolled answer in
36
+ * this table is how the validator and the renderer come to disagree about the
37
+ * same value — which is the defect class this rule was already an instance of.
38
+ * The delegation is behavioural, so it is pinned behaviourally: the drift pin in
39
+ * `__tests__/predicate-valued-gate-rules.test.ts` asserts this rule's verdict
40
+ * equals `boolean || (declared && !unevaluable)` across every probe — the second
41
+ * half since objectui#11358, see below.
42
+ *
43
+ * ## Why the boolean arm survives even though it is subsumed
44
+ *
45
+ * `hasDeclaredPredicate(true)` and `hasDeclaredPredicate(false)` are both
46
+ * `true` today (`toPredicateInput` returns a boolean unchanged), so the first
47
+ * arm changes no verdict. It is kept because it makes this rule's accept set a
48
+ * provable SUPERSET of the one it replaces, locally: a future narrowing on the
49
+ * declaredness side cannot silently start reporting `disabled: false` — the
50
+ * most explicit gate an author can write — as an invalid schema.
51
+ *
52
+ * ## What it still REFUSES (the half that is not negotiable)
53
+ *
54
+ * `null`, `''`, whitespace-only predicate text and the empty / blank-`source`
55
+ * envelope (objectui#3960) — what `hasDeclaredPredicate` calls "not declared" —
56
+ * and a value that IS declared but cannot be evaluated: a number, `{}`, an
57
+ * array, an envelope with no string `source` (`isUnevaluablePredicate`).
58
+ * Every one of those was refused before this change too — the accept set
59
+ * widens and nothing refused becomes accepted. Dropping the two keys from this
60
+ * table was the option this fix was explicitly forbidden to take: an absent
61
+ * rule reports nothing at all, which is gate weakening wearing the same green.
62
+ *
63
+ * ## Why "declared" alone stopped being this rule's answer (objectui#11358)
64
+ *
65
+ * Until objectui#11358 every declared gate was also an evaluable one, so
66
+ * "declared" answered both questions. That ruling made a present value with no
67
+ * evaluable `source` DECLARED (and faulting) so that the runtime fails it in
68
+ * its key's fault direction instead of reading it as no gate — and this rule
69
+ * asks a different question, "is this a predicate the runtime can evaluate?".
70
+ * Reading `hasDeclaredPredicate` alone would have widened the accept set to
71
+ * exactly the shapes that ruling exists to stop. So the rule refuses the
72
+ * unevaluable state by the same internal definition the runtime uses for it,
73
+ * not by a list written here.
74
+ */
75
+ function predicateGateRule(key) {
76
+ return {
77
+ required: false,
78
+ validate: (value) => typeof value === 'boolean' || (!isUnevaluablePredicate(value) && hasDeclaredPredicate(value)),
79
+ message: `${key} must be a boolean or a declared predicate: an expression string ` +
80
+ `(bare, e.g. "record.stage == 'closed'", or the "\${...}" template ` +
81
+ `spelling), or a { dialect, source } expression envelope`,
82
+ };
83
+ }
8
84
  /**
9
85
  * Validation rules for base schema
10
86
  */
@@ -24,31 +100,39 @@ const BASE_SCHEMA_RULES = {
24
100
  validate: (value) => typeof value === 'string',
25
101
  message: 'className must be a string'
26
102
  },
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
- }
103
+ // objectui#6505 — both keys are EXPRESSIONS in the protocol, not booleans.
104
+ // See `predicateGateRule` above for the accept set and what it still refuses.
105
+ visible: predicateGateRule('visible'),
106
+ disabled: predicateGateRule('disabled')
37
107
  };
108
+ /**
109
+ * The ONE narrowing between {@link validateSchema}'s `unknown` parameter and
110
+ * every rule below it (objectui#8416).
111
+ *
112
+ * The VERDICT is the guard this replaces, spelled the same way: the
113
+ * `!schema || typeof schema !== 'object'` test that used to sit at the top of
114
+ * `validateBaseSchema`. `null`, `undefined` and every primitive are refused;
115
+ * arrays and plain objects pass, exactly as before.
116
+ *
117
+ * What changes is WHERE it runs. Only ONE of the four rules ever had it, and
118
+ * `validateSchema` calls all four unconditionally: `validateFormSchema` read
119
+ * `schema.type` and `validateChildren` read `schema.children` with no guard at
120
+ * all, so `validateSchema(null)` and `validateSchema(undefined)` threw a raw
121
+ * `TypeError: Cannot read properties of null (reading 'type')` — measured on
122
+ * 256c709e2 — instead of returning the `INVALID_SCHEMA` result this function's
123
+ * own contract promises, and `isValidSchema(null)` threw instead of answering
124
+ * `false`. `SchemaRenderer` wraps its call in a `try`/`catch` whose comment
125
+ * reads "Validator itself failed — surface but don't crash render"; that catch
126
+ * is where the crash was landing.
127
+ */
128
+ function isSchemaNodeShape(value) {
129
+ return !!value && typeof value === 'object';
130
+ }
38
131
  /**
39
132
  * Validate a schema against base rules
40
133
  */
41
134
  function validateBaseSchema(schema, path = 'schema') {
42
135
  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
136
  // Validate required and optional properties
53
137
  Object.entries(BASE_SCHEMA_RULES).forEach(([key, rule]) => {
54
138
  const value = schema[key];
@@ -72,56 +156,165 @@ function validateBaseSchema(schema, path = 'schema') {
72
156
  return errors;
73
157
  }
74
158
  /**
75
- * Validate CRUD schema specific properties
159
+ * TOMBSTONE table — node `type` spellings this renderer has RETIRED, mapped to
160
+ * the prescription an author must follow instead (ADR-0049 enforce-or-remove).
161
+ *
162
+ * A retired spelling is not merely absent from this file. Absence here means
163
+ * {@link validateSchema} runs `validateBaseSchema` — which only asks that
164
+ * `type` be a non-empty string — finds nothing type-specific to say, and
165
+ * returns `valid: true`. That silence is the failure mode this table exists to
166
+ * prevent: the author is told their schema is fine and then gets the OBJUI-001
167
+ * "Unknown component type" panel at render time, one layer too late to act on.
168
+ * So a retired spelling is refused BY NAME, with the migration in the message.
169
+ *
170
+ * `crud` (objectui#5373, maintainer ruling of 2026-08-20): `CRUDSchema` carried
171
+ * FOUR declaration faces — the TS interface, the zod mirror, a dedicated branch
172
+ * in this very function, and `CRUDBuilder` — and never once had a registered
173
+ * renderer, for the whole life of the key. Every face taught an author (and an
174
+ * AI author reading the published reference page) that the type existed. The
175
+ * branch that used to sit here is what made this validator the loudest of the
176
+ * four lies: it read `schema.type === 'crud'`, checked that `columns` was an
177
+ * array, and returned no error — an affirmative "your CRUD schema is valid"
178
+ * for a node that renders nothing.
179
+ *
180
+ * Keyed by the authored spelling, and quantified over the table rather than
181
+ * written per spelling, so the next retirement closes this face the day it
182
+ * lands here.
76
183
  */
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;
184
+ const RETIRED_NODE_TYPES = Object.freeze({
185
+ crud: "Node type `crud` was RETIRED (objectui#5373, ADR-0049 enforce-or-remove). " +
186
+ "`CRUDSchema` declared it in four places and no renderer ever registered it, " +
187
+ "so a node spelling it painted the OBJUI-001 \"Unknown component type\" panel. " +
188
+ "Compose the shapes that DO render instead: `object-grid` for the record " +
189
+ "table with its toolbar, filters, pagination and row/batch actions, " +
190
+ "`object-form` for the create/edit form, and `detail` for the record view.",
191
+ });
192
+ /**
193
+ * Refuse a retired node-type spelling by name.
194
+ *
195
+ * Severity is `error`, not `warning`, and that is the whole point of the
196
+ * retirement: a warning leaves `result.valid === true`, so `assertValidSchema`
197
+ * would still not throw and `isValidSchema` would still answer `true` — the
198
+ * same silence, one console line louder.
199
+ *
200
+ * `hasOwnProperty` rather than a plain index read: a `type` of `'constructor'`
201
+ * or `'toString'` reaches `Object.prototype` and would answer truthy.
202
+ *
203
+ * Reached for every node in the tree, not just the root — {@link validateSchema}
204
+ * is what `validateChildren` recurses with, so a retired spelling nested inside
205
+ * a `children` array is refused with its own path.
206
+ */
207
+ function validateRetiredNodeType(schema, path = 'schema') {
208
+ const type = schema.type;
209
+ if (typeof type !== 'string')
210
+ return [];
211
+ if (!Object.prototype.hasOwnProperty.call(RETIRED_NODE_TYPES, type))
212
+ return [];
213
+ return [{
214
+ path: `${path}.type`,
215
+ message: RETIRED_NODE_TYPES[type],
216
+ type: 'error',
217
+ code: 'RETIRED_TYPE'
218
+ }];
219
+ }
220
+ /**
221
+ * The ONE namespace a form field's widget id may name — objectui#5254,
222
+ * maintainer ruling of 2026-08-19.
223
+ *
224
+ * A form field's `type` either names a `field:`-namespaced widget or takes the
225
+ * renderer's declared `default` input branch. It never resolves a plain SDUI
226
+ * node renderer: `renderFieldComponent` (`@object-ui/components`) resolves a
227
+ * colon-qualified id ONLY under this prefix, and `resolveFieldLabelling` /
228
+ * `resolvesToRegisteredFieldWidget` there mirror it exactly.
229
+ */
230
+ const FIELD_WIDGET_NAMESPACE = 'field:';
231
+ /**
232
+ * The widget id a form field will actually be RENDERED as.
233
+ *
234
+ * Mirrors the renderer's own precedence (`resolvedType` in
235
+ * `packages/components/src/renderers/form/form.tsx`: explicit form-config
236
+ * `widget`, then the resolved field metadata's widget hint, then the bare
237
+ * `type`). Only the two keys an AUTHORED schema carries are read here —
238
+ * `field.field?.widget` is a resolved metadata OBJECT that a hand-authored
239
+ * standalone `FormSchema` does not have.
240
+ *
241
+ * The mirroring is the point, not a convenience: validator and renderer must
242
+ * not be able to disagree about which component will actually render, which is
243
+ * the same discipline `resolveFieldLabelling` states on the renderer side.
244
+ */
245
+ function resolveAuthoredFieldWidgetId(field) {
246
+ const id = field.widget ?? field.type;
247
+ return typeof id === 'string' ? id : undefined;
248
+ }
249
+ /**
250
+ * A form field's widget id names a namespace that resolves NO field widget —
251
+ * objectui#5375, maintainer ruling of 2026-08-20 (option C, the primary).
252
+ *
253
+ * ## What this refuses, and why it is an error rather than a warning
254
+ *
255
+ * Measured on `main` at f2e11ae6f, the real `form` renderer on the built-in
256
+ * path (no `registerAllFields()`):
257
+ *
258
+ * type registry hit rendered type
259
+ * ui:password TRUE text
260
+ * secret false text
261
+ * field:secret false text
262
+ *
263
+ * `ui:password` **is** registered — as an SDUI node renderer for a top-level
264
+ * `{ type: 'email' }`-style node — so an author who checks whether it resolves
265
+ * gets a YES, and still gets a clear-text box on the field path. That is the
266
+ * shape where verifying does not protect you, and it is why the class, not
267
+ * another entry in a renderer-side table, is what gets closed here: the table
268
+ * fix answers today's spellings and leaves the next invented id to find the
269
+ * same silent degrade. AI-authored metadata invents plausible-looking widget
270
+ * ids constantly; this makes inventing one an ERROR at authoring time instead
271
+ * of a text box that looks like it worked.
272
+ *
273
+ * A warning was explicitly ruled out — a warning IS the silent degrade this
274
+ * check exists to kill, in a new spelling.
275
+ *
276
+ * ## Why only the NAMESPACED spelling
277
+ *
278
+ * `field:` is allowed whether or not the widget is registered: registration is
279
+ * a RUNTIME fact (`registerAllFields()`, a lazily loaded plugin) that an
280
+ * authoring-time validator cannot see, and the renderer already answers an
281
+ * unregistered `field:` secret with a visible refusal (objectui#5322). A BARE
282
+ * name is likewise allowed: it is an open set — every registered `field:<name>`
283
+ * widget, third-party ones included, is reachable by its short name.
284
+ *
285
+ * What is decidable statically is the namespace, and it is a CLOSED set of one.
286
+ * Any other `<ns>:<name>` resolves nothing on the field path by the #5254
287
+ * ruling and falls to the renderer's plain-text `default` arm.
288
+ *
289
+ * ## Census (the ruling made arming this conditional on one)
290
+ *
291
+ * 4,397 authored files in this repo, 649 form-field `type`/`widget` literals,
292
+ * 98 structurally parsed form-field entries in `examples/schema-catalog`:
293
+ * SEVEN colon-qualified ids on the form-field path, five of them `field:*`.
294
+ * The two remaining occurrences are one source line naming `ref:component`,
295
+ * which is a metadata-admin designer `SchemaForm` widget resolved by its own
296
+ * `WIDGETS` table — a different shape (`{ field, widget }` under
297
+ * `sections[].fields[]`, node type `simple`) that this function, gated on
298
+ * `type === 'form'`, never reaches. Zero occurrences of `ui:password`,
299
+ * `secret` or `field:secret` anywhere on the field path.
300
+ */
301
+ function validateFieldWidgetNamespace(field, path) {
302
+ const id = resolveAuthoredFieldWidgetId(field);
303
+ if (!id || !id.includes(':') || id.startsWith(FIELD_WIDGET_NAMESPACE))
304
+ return undefined;
305
+ const key = typeof field.widget === 'string' ? 'widget' : 'type';
306
+ return {
307
+ path: `${path}.${key}`,
308
+ message: `'${id}' is not a form field widget: a namespaced field widget id must name the ` +
309
+ `\`field:\` namespace (objectui#5254), so this id resolves NOTHING on the field path ` +
310
+ `and the renderer would fall through to a plain text box — putting a secret on screen ` +
311
+ `in clear text when the field holds one (objectui#5375). Write the built-in spelling ` +
312
+ `(e.g. \`password\`), or the field widget id \`field:${id.slice(id.indexOf(':') + 1)}\`. ` +
313
+ `Resolving in the registry is not proof it resolves HERE — \`ui:password\` is a ` +
314
+ `registered SDUI node renderer and still renders clear text as a field.`,
315
+ type: 'error',
316
+ code: 'UNRESOLVABLE_FIELD_WIDGET_NAMESPACE',
317
+ };
125
318
  }
126
319
  /**
127
320
  * Validate form schema specific properties
@@ -129,8 +322,15 @@ function validateCRUDSchema(schema, path = 'schema') {
129
322
  function validateFormSchema(schema, path = 'schema') {
130
323
  const errors = [];
131
324
  if (schema.type === 'form') {
132
- if (schema.fields && Array.isArray(schema.fields)) {
133
- schema.fields.forEach((field, index) => {
325
+ const fields = schema.fields;
326
+ if (Array.isArray(fields)) {
327
+ fields.forEach((entry, index) => {
328
+ // Narrowed before it is read, for the same reason the root is: a
329
+ // `fields` array is authored data too, and reading `field.name` off
330
+ // whatever an author put there made `fields: [null]` throw the same raw
331
+ // `TypeError` a `null` root did. A non-object entry keeps its old
332
+ // verdict — every key reads `undefined`, so `MISSING_FIELD_NAME`.
333
+ const field = isSchemaNodeShape(entry) ? entry : {};
134
334
  if (!field.name) {
135
335
  errors.push({
136
336
  path: `${path}.fields[${index}]`,
@@ -139,12 +339,18 @@ function validateFormSchema(schema, path = 'schema') {
139
339
  code: 'MISSING_FIELD_NAME'
140
340
  });
141
341
  }
342
+ // objectui#5375 — an invented namespaced widget id is an ERROR here,
343
+ // not a silent clear-text box at render time. See the helper's doc.
344
+ const namespaceError = validateFieldWidgetNamespace(field, `${path}.fields[${index}]`);
345
+ if (namespaceError) {
346
+ errors.push(namespaceError);
347
+ }
142
348
  // Check for duplicate field names
143
- const duplicates = schema.fields.filter((f) => f.name === field.name);
349
+ const duplicates = fields.filter((other) => (isSchemaNodeShape(other) ? other.name : undefined) === field.name);
144
350
  if (duplicates.length > 1) {
145
351
  errors.push({
146
352
  path: `${path}.fields[${index}]`,
147
- message: `Duplicate field name: ${field.name}`,
353
+ message: `Duplicate field name: ${String(field.name)}`,
148
354
  type: 'warning',
149
355
  code: 'DUPLICATE_FIELD_NAME'
150
356
  });
@@ -159,17 +365,21 @@ function validateFormSchema(schema, path = 'schema') {
159
365
  */
160
366
  function validateChildren(schema, path = 'schema') {
161
367
  const errors = [];
162
- const children = schema.children || schema.body;
368
+ // One spelling. This walker resolved `children || body` for ANY node type,
369
+ // so it outlived every per-registration read of the dialect — objectui#6771
370
+ // retired it, and a recursive validator that still descended `body` would
371
+ // keep validating a child list no renderer puts on the page.
372
+ const children = schema.children;
163
373
  if (children) {
164
374
  if (Array.isArray(children)) {
165
375
  children.forEach((child, index) => {
166
- if (typeof child === 'object' && child !== null) {
376
+ if (isSchemaNodeShape(child)) {
167
377
  const childResult = validateSchema(child, `${path}.children[${index}]`);
168
378
  errors.push(...childResult.errors, ...childResult.warnings);
169
379
  }
170
380
  });
171
381
  }
172
- else if (typeof children === 'object' && children !== null) {
382
+ else if (isSchemaNodeShape(children)) {
173
383
  const childResult = validateSchema(children, `${path}.children`);
174
384
  errors.push(...childResult.errors, ...childResult.warnings);
175
385
  }
@@ -199,13 +409,26 @@ function validateChildren(schema, path = 'schema') {
199
409
  */
200
410
  export function validateSchema(schema, path = 'schema') {
201
411
  const allErrors = [];
202
- // Validate base schema
203
- allErrors.push(...validateBaseSchema(schema, path));
204
- // Validate type-specific schemas
205
- allErrors.push(...validateCRUDSchema(schema, path));
206
- allErrors.push(...validateFormSchema(schema, path));
207
- // Validate children recursively
208
- allErrors.push(...validateChildren(schema, path));
412
+ // Narrow ONCE, here, before any rule reads a key off the value — see
413
+ // `isSchemaNodeShape`. Every rule below is handed a value already proven to
414
+ // be an object, which is why none of them re-tests it.
415
+ if (!isSchemaNodeShape(schema)) {
416
+ allErrors.push({
417
+ path,
418
+ message: 'Schema must be an object',
419
+ type: 'error',
420
+ code: 'INVALID_SCHEMA'
421
+ });
422
+ }
423
+ else {
424
+ // Validate base schema
425
+ allErrors.push(...validateBaseSchema(schema, path));
426
+ // Validate type-specific schemas
427
+ allErrors.push(...validateRetiredNodeType(schema, path));
428
+ allErrors.push(...validateFormSchema(schema, path));
429
+ // Validate children recursively
430
+ allErrors.push(...validateChildren(schema, path));
431
+ }
209
432
  const errors = allErrors.filter(e => e.type === 'error');
210
433
  const warnings = allErrors.filter(e => e.type === 'warning');
211
434
  return {
@@ -0,0 +1,62 @@
1
+ /**
2
+ * ObjectUI
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
+ * Is this declared `defaultValue` a RUNTIME INSTRUCTION the server resolves per
10
+ * insert, rather than a literal value?
11
+ *
12
+ * True for the `DEFAULT_VALUE_TOKENS` family (`'NOW()'` / `'current_user'`) and
13
+ * for CEL/template Expression envelopes. This is THE classifier for "the
14
+ * producer owns this field's create value", and every consumer reads THIS one
15
+ * — a second copy would be free to disagree about, say, a CEL envelope, and
16
+ * then a form would seed a field it also refuses to submit. Today's consumers:
17
+ *
18
+ * 1. seeding (#4047 / #4068) — such a default is not seeded, because putting
19
+ * the literal text `NOW()` into a datetime input and submitting it
20
+ * suppresses the very resolution the declaration asked for;
21
+ * 2. the create-mode static `required` rule (#4069) —
22
+ * `isRequiredInForm` in `@object-ui/plugin-form`;
23
+ * 3. the create-mode `requiredWhen` rule (#4085) —
24
+ * {@link isServerOwnedValue}, read by `resolveFieldRuleState`.
25
+ */
26
+ export declare function isRuntimeDefault(v: unknown): boolean;
27
+ /**
28
+ * A runtime `FormField` as far as this module is concerned: the resolved
29
+ * object-field metadata stash, and whatever else the field carries.
30
+ *
31
+ * Stated structurally rather than as `@object-ui/types`' `FormField` so the
32
+ * one key this module reads is visible in the signature — and with an index
33
+ * signature so a caller (or a test) may hand over an object literal carrying
34
+ * the rest of the field without tripping TypeScript's excess-property check.
35
+ */
36
+ export interface FieldWithDeclaredDefault {
37
+ field?: {
38
+ defaultValue?: unknown;
39
+ } | null;
40
+ [key: string]: unknown;
41
+ }
42
+ /**
43
+ * Does the PRODUCER supply this field's value for the submit this form is
44
+ * about to make? (#4085)
45
+ *
46
+ * The single derivation of that fact, so the three call sites that feed
47
+ * `resolveFieldRuleState` cannot each answer "where does the declared default
48
+ * live" differently. It reads the runtime FormField's `field` slot — the
49
+ * resolved object-field metadata the object-bound builders stash there
50
+ * (`ObjectForm`, `sectionFields`, and the containers' no-sections paths), and
51
+ * the only spelling of a declared `defaultValue` that reaches the renderer.
52
+ * A hand-authored FormField carries no object metadata, so it is never
53
+ * server-owned and its rules are untouched.
54
+ *
55
+ * `isCreateForm` is the CALLER's fact, never inferred here: the form renderer
56
+ * reads it off `FormSchema.previousValues` (whose absence *is* the declared
57
+ * "this is an insert" signal, objectui#3484), while the wizard's step gate
58
+ * reads its own `mode`. An evaluator guessing from a `previous` argument would
59
+ * call an edit-mode caller that simply passes no `previous` a create form, and
60
+ * silently drop a `requiredWhen` the author meant to enforce.
61
+ */
62
+ export declare function isServerOwnedValue(field: FieldWithDeclaredDefault | null | undefined, isCreateForm: boolean): boolean;