@object-ui/core 17.5.0 → 17.6.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.
- package/CHANGELOG.md +794 -0
- package/README.md +36 -22
- package/dist/actions/ActionEngine.d.ts +1 -1
- package/dist/actions/ActionEngine.js +1 -2
- package/dist/actions/ActionRunner.d.ts +82 -10
- package/dist/actions/ActionRunner.js +128 -22
- package/dist/actions/actionKeys.d.ts +68 -8
- package/dist/actions/actionKeys.js +81 -9
- package/dist/actions/actionResponse.js +1 -1
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordIdParam.d.ts +77 -0
- package/dist/actions/recordIdParam.js +37 -0
- package/dist/actions/serverActionHandler.d.ts +1 -1
- package/dist/actions/serverActionHandler.js +1 -1
- package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
- package/dist/evaluator/ExpressionEvaluator.js +42 -1
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.js +1 -1
- package/dist/evaluator/fieldRules.d.ts +19 -0
- package/dist/evaluator/fieldRules.js +30 -6
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +49 -2
- package/dist/evaluator/listConditional.js +70 -6
- package/dist/evaluator/optionRules.d.ts +55 -0
- package/dist/evaluator/optionRules.js +60 -0
- package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
- package/dist/evaluator/rowPredicateCanon.js +208 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17 -0
- package/dist/registry/Registry.d.ts +41 -19
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/types/index.d.ts +92 -13
- package/dist/utils/chart-presentation.d.ts +205 -0
- package/dist/utils/chart-presentation.js +280 -0
- package/dist/utils/chart-series.d.ts +140 -26
- package/dist/utils/chart-series.js +380 -39
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/dataset-format.js +18 -6
- package/dist/utils/debug.js +7 -0
- package/dist/utils/expand-fields.d.ts +97 -7
- package/dist/utils/expand-fields.js +97 -7
- package/dist/utils/filter-tokens.js +0 -1
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +27 -2
- package/dist/utils/normalize-list-view.d.ts +10 -0
- package/dist/utils/normalize-list-view.js +63 -1
- package/dist/utils/number-display.d.ts +7 -3
- package/dist/utils/predicate-fields.d.ts +29 -4
- package/dist/utils/predicate-fields.js +44 -4
- package/dist/utils/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +12 -10
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/dist/validation/server-owned-value.d.ts +62 -0
- package/dist/validation/server-owned-value.js +99 -0
- package/dist/validation/validators/object-validation-engine.js +0 -2
- package/package.json +4 -4
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
* more column and no extra round trip. (A relation that IS displayed still gets
|
|
31
31
|
* expanded for its label, and `toPredicateRecord` collapses it back for the
|
|
32
32
|
* predicate.)
|
|
33
|
+
*
|
|
34
|
+
* Since objectstack#8018 the same reader also carries the non-predicate row keys
|
|
35
|
+
* a view's actions read — today just `recordIdField`. The name kept the
|
|
36
|
+
* "predicate" spelling because the mechanism is identical (harvest a name, gate
|
|
37
|
+
* it against the declared fields, add it to `$select`); see
|
|
38
|
+
* {@link listViewPredicates} for what is in the set and why.
|
|
33
39
|
*/
|
|
34
40
|
/**
|
|
35
41
|
* The columns the framework provisions on EVERY business object
|
|
@@ -71,13 +77,32 @@ export declare function isProjectableField(name: string, declaredFields: Record<
|
|
|
71
77
|
*/
|
|
72
78
|
export declare function collectPredicateFieldRefs(predicates: readonly unknown[]): string[];
|
|
73
79
|
/**
|
|
74
|
-
* Every
|
|
80
|
+
* Every record-field reference a list-ish view carries, flattened for
|
|
75
81
|
* {@link collectPredicateFieldRefs}.
|
|
76
82
|
*
|
|
77
83
|
* One reader so the projection cannot fall behind the surfaces: each entry here
|
|
78
|
-
* is a place a
|
|
79
|
-
*
|
|
80
|
-
*
|
|
84
|
+
* is a place a row field is actually READ — conditional formatting, the row
|
|
85
|
+
* kebab (custom defs AND the built-in Edit/Delete overrides), the selection bar,
|
|
86
|
+
* and the object's declared actions.
|
|
87
|
+
*
|
|
88
|
+
* Most entries are predicates. Two are not, and both are spelled as a synthetic
|
|
89
|
+
* `record.<name>` so the one harvester handles them: conditional formatting's
|
|
90
|
+
* native `{ field, operator, value }` shape, and an action's `recordIdField`
|
|
91
|
+
* (objectstack#8018). The `recordIdField` case is the same *class* of bug the
|
|
92
|
+
* predicate harvest exists to close, one surface over — the action runtime reads
|
|
93
|
+
* `rowRecord[action.recordIdField]` to seed `recordIdParam`, so a key outside the
|
|
94
|
+
* listView columns arrived absent and the injection was silently skipped, which
|
|
95
|
+
* turns a record-scoped mutation into one that names no record while still
|
|
96
|
+
* reporting success. The default (`id`) is already projected unconditionally, so
|
|
97
|
+
* only an explicit declaration adds anything here.
|
|
98
|
+
*
|
|
99
|
+
* A `recordIdField` that is not a bare identifier is dropped here, and one naming
|
|
100
|
+
* a field the object does not declare is dropped by the caller's
|
|
101
|
+
* `isProjectableField` guard. Both drops are the safe direction: such a name is
|
|
102
|
+
* not a column anywhere, and an unknown key in `$select` is not ignored by every
|
|
103
|
+
* backend. The identifier gate is not decoration — without it the harvester's
|
|
104
|
+
* regex would read a PREFIX of a malformed name (`record.not a field` → `not`)
|
|
105
|
+
* and contribute a plausible wrong field instead of nothing.
|
|
81
106
|
*
|
|
82
107
|
* `objectActions` is deliberately the object's WHOLE action set rather than
|
|
83
108
|
* only the ones this view promotes. Narrowing it would mean re-running the
|
|
@@ -30,6 +30,12 @@
|
|
|
30
30
|
* more column and no extra round trip. (A relation that IS displayed still gets
|
|
31
31
|
* expanded for its label, and `toPredicateRecord` collapses it back for the
|
|
32
32
|
* predicate.)
|
|
33
|
+
*
|
|
34
|
+
* Since objectstack#8018 the same reader also carries the non-predicate row keys
|
|
35
|
+
* a view's actions read — today just `recordIdField`. The name kept the
|
|
36
|
+
* "predicate" spelling because the mechanism is identical (harvest a name, gate
|
|
37
|
+
* it against the declared fields, add it to `$select`); see
|
|
38
|
+
* {@link listViewPredicates} for what is in the set and why.
|
|
33
39
|
*/
|
|
34
40
|
/**
|
|
35
41
|
* The columns the framework provisions on EVERY business object
|
|
@@ -74,6 +80,14 @@ export function isProjectableField(name, declaredFields) {
|
|
|
74
80
|
* record-header path), so this covers the documented spelling.
|
|
75
81
|
*/
|
|
76
82
|
const RECORD_REF = /\b(?:record|data)\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
83
|
+
/**
|
|
84
|
+
* A whole bare identifier — what a field name is, and the only shape that can be
|
|
85
|
+
* spelled as `record.<name>` without the harvester's regex reading a PREFIX of it
|
|
86
|
+
* as the name (`record.not a field` would otherwise yield `not`). Used to gate
|
|
87
|
+
* the non-predicate names folded into {@link listViewPredicates}, so a malformed
|
|
88
|
+
* declaration contributes nothing instead of contributing a plausible wrong name.
|
|
89
|
+
*/
|
|
90
|
+
const BARE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
77
91
|
/** Pull the CEL source out of any of the shapes a predicate is authored in. */
|
|
78
92
|
function predicateSource(pred) {
|
|
79
93
|
if (typeof pred === 'string')
|
|
@@ -117,13 +131,32 @@ export function collectPredicateFieldRefs(predicates) {
|
|
|
117
131
|
return out;
|
|
118
132
|
}
|
|
119
133
|
/**
|
|
120
|
-
* Every
|
|
134
|
+
* Every record-field reference a list-ish view carries, flattened for
|
|
121
135
|
* {@link collectPredicateFieldRefs}.
|
|
122
136
|
*
|
|
123
137
|
* One reader so the projection cannot fall behind the surfaces: each entry here
|
|
124
|
-
* is a place a
|
|
125
|
-
*
|
|
126
|
-
*
|
|
138
|
+
* is a place a row field is actually READ — conditional formatting, the row
|
|
139
|
+
* kebab (custom defs AND the built-in Edit/Delete overrides), the selection bar,
|
|
140
|
+
* and the object's declared actions.
|
|
141
|
+
*
|
|
142
|
+
* Most entries are predicates. Two are not, and both are spelled as a synthetic
|
|
143
|
+
* `record.<name>` so the one harvester handles them: conditional formatting's
|
|
144
|
+
* native `{ field, operator, value }` shape, and an action's `recordIdField`
|
|
145
|
+
* (objectstack#8018). The `recordIdField` case is the same *class* of bug the
|
|
146
|
+
* predicate harvest exists to close, one surface over — the action runtime reads
|
|
147
|
+
* `rowRecord[action.recordIdField]` to seed `recordIdParam`, so a key outside the
|
|
148
|
+
* listView columns arrived absent and the injection was silently skipped, which
|
|
149
|
+
* turns a record-scoped mutation into one that names no record while still
|
|
150
|
+
* reporting success. The default (`id`) is already projected unconditionally, so
|
|
151
|
+
* only an explicit declaration adds anything here.
|
|
152
|
+
*
|
|
153
|
+
* A `recordIdField` that is not a bare identifier is dropped here, and one naming
|
|
154
|
+
* a field the object does not declare is dropped by the caller's
|
|
155
|
+
* `isProjectableField` guard. Both drops are the safe direction: such a name is
|
|
156
|
+
* not a column anywhere, and an unknown key in `$select` is not ignored by every
|
|
157
|
+
* backend. The identifier gate is not decoration — without it the harvester's
|
|
158
|
+
* regex would read a PREFIX of a malformed name (`record.not a field` → `not`)
|
|
159
|
+
* and contribute a plausible wrong field instead of nothing.
|
|
127
160
|
*
|
|
128
161
|
* `objectActions` is deliberately the object's WHOLE action set rather than
|
|
129
162
|
* only the ones this view promotes. Narrowing it would mean re-running the
|
|
@@ -151,6 +184,13 @@ export function listViewPredicates(view) {
|
|
|
151
184
|
continue;
|
|
152
185
|
const d = def;
|
|
153
186
|
preds.push(d.visible, d.disabled);
|
|
187
|
+
// The row key this action identifies its record by — a field the runtime
|
|
188
|
+
// READS off the row, so the projection owes it exactly as it owes a
|
|
189
|
+
// predicate's operands. Spelled as a `record.` reference so the one
|
|
190
|
+
// harvester handles it (see the doc above).
|
|
191
|
+
if (typeof d.recordIdField === 'string' && BARE_IDENTIFIER.test(d.recordIdField)) {
|
|
192
|
+
preds.push(`record.${d.recordIdField}`);
|
|
193
|
+
}
|
|
154
194
|
}
|
|
155
195
|
}
|
|
156
196
|
for (const override of Object.values(view.userActions ?? {})) {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — retired field-type spellings
|
|
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
|
+
* TOMBSTONE table — field-type spellings this renderer has RETIRED, mapped to
|
|
10
|
+
* the prescription an author must follow instead (ADR-0049 enforce-or-remove).
|
|
11
|
+
*
|
|
12
|
+
* A retired spelling is not merely absent: absence here means
|
|
13
|
+
* `mapFieldTypeToFormType`'s `|| 'field:text'` tail would hand back a
|
|
14
|
+
* working plain text input, which is the failure mode this table exists to
|
|
15
|
+
* prevent. An author who writes a retired name — or an AI author who copies one
|
|
16
|
+
* out of a stale doc — must be TOLD, not quietly given a text box that looks
|
|
17
|
+
* like it worked. So each entry resolves to a registered tombstone widget that
|
|
18
|
+
* renders a visible refusal naming the migration
|
|
19
|
+
* (`packages/fields/src/index.tsx`), while {@link reportRetiredFieldType}
|
|
20
|
+
* writes the same prescription to the console.
|
|
21
|
+
*
|
|
22
|
+
* `owner` (objectui#4814, ruling A′): a synonym for `user` with zero behavioral
|
|
23
|
+
* delta — both resolved to the SAME `UserField` widget — and absent from the
|
|
24
|
+
* spec's closed 48-member `FieldType`, so no object schema could ever declare
|
|
25
|
+
* it; it was reachable only through hand-written SDUI. Three code faces had
|
|
26
|
+
* already drifted apart on this one word (the form's data-source rule excluded
|
|
27
|
+
* it while plugin-grid's bulk dialog included it), which is the standing
|
|
28
|
+
* evidence that a second spelling for one concept is a drift channel, not a
|
|
29
|
+
* convenience. The idiom survives verbatim as `{ type: 'user', name: 'owner' }`.
|
|
30
|
+
*
|
|
31
|
+
* ## Why this table lives in `@object-ui/core` and not in `@object-ui/fields`
|
|
32
|
+
*
|
|
33
|
+
* It was born in `packages/fields/src/field-type-alias.ts`, next to the widget
|
|
34
|
+
* map that is its first consumer, and `@object-ui/fields` still exports every
|
|
35
|
+
* name in this module so no consumer of that surface had to move. It was HOISTED
|
|
36
|
+
* here by objectui#4914, and for a measured reason rather than a tidiness one:
|
|
37
|
+
* the retirement gate has to run ahead of six predicate faces, and one of them
|
|
38
|
+
* — `operatorsForFieldType` / the filter row's control choice in
|
|
39
|
+
* `packages/components/src/custom/filter-builder.tsx` — lives in
|
|
40
|
+
* `@object-ui/components`, which `@object-ui/fields` DEPENDS ON (54 source
|
|
41
|
+
* files import from it). A `components → fields` import is a package cycle, so
|
|
42
|
+
* the six faces could not all read one table while that table lived in
|
|
43
|
+
* `fields`.
|
|
44
|
+
*
|
|
45
|
+
* The alternative was a second copy of the table inside `components`, and that
|
|
46
|
+
* is precisely what this file exists to make impossible: two tables means two
|
|
47
|
+
* `reportedRetiredTypes` sets, so one retired spelling logs its prescription
|
|
48
|
+
* TWICE — and the "fires once" half of the ruling would be false the day it
|
|
49
|
+
* landed. `@object-ui/core` is below all six faces' packages (it imports only
|
|
50
|
+
* `@object-ui/types`), and already owns the sibling cross-package field-type
|
|
51
|
+
* vocabulary — {@link EXPANDABLE_FIELD_TYPES} in `./expand-fields.js`, whose
|
|
52
|
+
* own docblock records what happened the last time this kind of table was
|
|
53
|
+
* hand-copied per consumer. One table, one home, seven consumers.
|
|
54
|
+
*/
|
|
55
|
+
export declare const RETIRED_FIELD_TYPES: Readonly<Record<string, string>>;
|
|
56
|
+
/**
|
|
57
|
+
* THE retirement gate — true when a spelling has been retired by this renderer.
|
|
58
|
+
*
|
|
59
|
+
* Ruled into existence by the maintainer on 2026-08-18 (objectui#4914, option
|
|
60
|
+
* B) as the ONE question every field-type predicate asks before it answers:
|
|
61
|
+
* "a retired spelling fires `reportRetiredFieldType` once and takes the
|
|
62
|
+
* established disposition, instead of riding the lookup-family paths or
|
|
63
|
+
* silently degrading."
|
|
64
|
+
*
|
|
65
|
+
* Quantified over the whole table rather than written per spelling, which is
|
|
66
|
+
* the entire point of having a gate at all: the next retirement closes every
|
|
67
|
+
* face the day it lands in {@link RETIRED_FIELD_TYPES}, instead of leaving each
|
|
68
|
+
* consumer open until someone notices it. `owner` survived objectui#4814 in
|
|
69
|
+
* thirteen more faces exactly because each one had hand-written its own
|
|
70
|
+
* membership list.
|
|
71
|
+
*
|
|
72
|
+
* `hasOwnProperty`, not `RETIRED_FIELD_TYPES[type]`: a `type` of
|
|
73
|
+
* `'constructor'` or `'toString'` reaches `Object.prototype` and would answer
|
|
74
|
+
* truthy through a plain index read.
|
|
75
|
+
*
|
|
76
|
+
* @param type - The spelling an author wrote (or a backend column declared).
|
|
77
|
+
* @returns `true` when the spelling is retired. Pure — it does NOT log; pair it
|
|
78
|
+
* with {@link reportRetiredFieldType} at the face that wants to be loud.
|
|
79
|
+
*/
|
|
80
|
+
export declare function isRetiredFieldType(type: string | undefined | null): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Report a retired field-type spelling loudly, once per spelling.
|
|
83
|
+
*
|
|
84
|
+
* @param fieldType - The spelling the author wrote.
|
|
85
|
+
* @returns `true` when `fieldType` is retired (whether or not this call was the
|
|
86
|
+
* one that logged), so callers can branch on it without a second table lookup.
|
|
87
|
+
*/
|
|
88
|
+
export declare function reportRetiredFieldType(fieldType: string): boolean;
|
|
89
|
+
/** Test seam — forget which spellings have been reported. */
|
|
90
|
+
export declare function resetRetiredFieldTypeReports(): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — retired field-type spellings
|
|
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
|
+
* TOMBSTONE table — field-type spellings this renderer has RETIRED, mapped to
|
|
10
|
+
* the prescription an author must follow instead (ADR-0049 enforce-or-remove).
|
|
11
|
+
*
|
|
12
|
+
* A retired spelling is not merely absent: absence here means
|
|
13
|
+
* `mapFieldTypeToFormType`'s `|| 'field:text'` tail would hand back a
|
|
14
|
+
* working plain text input, which is the failure mode this table exists to
|
|
15
|
+
* prevent. An author who writes a retired name — or an AI author who copies one
|
|
16
|
+
* out of a stale doc — must be TOLD, not quietly given a text box that looks
|
|
17
|
+
* like it worked. So each entry resolves to a registered tombstone widget that
|
|
18
|
+
* renders a visible refusal naming the migration
|
|
19
|
+
* (`packages/fields/src/index.tsx`), while {@link reportRetiredFieldType}
|
|
20
|
+
* writes the same prescription to the console.
|
|
21
|
+
*
|
|
22
|
+
* `owner` (objectui#4814, ruling A′): a synonym for `user` with zero behavioral
|
|
23
|
+
* delta — both resolved to the SAME `UserField` widget — and absent from the
|
|
24
|
+
* spec's closed 48-member `FieldType`, so no object schema could ever declare
|
|
25
|
+
* it; it was reachable only through hand-written SDUI. Three code faces had
|
|
26
|
+
* already drifted apart on this one word (the form's data-source rule excluded
|
|
27
|
+
* it while plugin-grid's bulk dialog included it), which is the standing
|
|
28
|
+
* evidence that a second spelling for one concept is a drift channel, not a
|
|
29
|
+
* convenience. The idiom survives verbatim as `{ type: 'user', name: 'owner' }`.
|
|
30
|
+
*
|
|
31
|
+
* ## Why this table lives in `@object-ui/core` and not in `@object-ui/fields`
|
|
32
|
+
*
|
|
33
|
+
* It was born in `packages/fields/src/field-type-alias.ts`, next to the widget
|
|
34
|
+
* map that is its first consumer, and `@object-ui/fields` still exports every
|
|
35
|
+
* name in this module so no consumer of that surface had to move. It was HOISTED
|
|
36
|
+
* here by objectui#4914, and for a measured reason rather than a tidiness one:
|
|
37
|
+
* the retirement gate has to run ahead of six predicate faces, and one of them
|
|
38
|
+
* — `operatorsForFieldType` / the filter row's control choice in
|
|
39
|
+
* `packages/components/src/custom/filter-builder.tsx` — lives in
|
|
40
|
+
* `@object-ui/components`, which `@object-ui/fields` DEPENDS ON (54 source
|
|
41
|
+
* files import from it). A `components → fields` import is a package cycle, so
|
|
42
|
+
* the six faces could not all read one table while that table lived in
|
|
43
|
+
* `fields`.
|
|
44
|
+
*
|
|
45
|
+
* The alternative was a second copy of the table inside `components`, and that
|
|
46
|
+
* is precisely what this file exists to make impossible: two tables means two
|
|
47
|
+
* `reportedRetiredTypes` sets, so one retired spelling logs its prescription
|
|
48
|
+
* TWICE — and the "fires once" half of the ruling would be false the day it
|
|
49
|
+
* landed. `@object-ui/core` is below all six faces' packages (it imports only
|
|
50
|
+
* `@object-ui/types`), and already owns the sibling cross-package field-type
|
|
51
|
+
* vocabulary — {@link EXPANDABLE_FIELD_TYPES} in `./expand-fields.js`, whose
|
|
52
|
+
* own docblock records what happened the last time this kind of table was
|
|
53
|
+
* hand-copied per consumer. One table, one home, seven consumers.
|
|
54
|
+
*/
|
|
55
|
+
export const RETIRED_FIELD_TYPES = Object.freeze({
|
|
56
|
+
owner: "[object-ui] Field type `owner` was RETIRED (objectui#4814). It was a synonym " +
|
|
57
|
+
"for `user` with no behavioral difference, and it is not a member of " +
|
|
58
|
+
"`@objectstack/spec`'s FieldType. Write the record-owner field as " +
|
|
59
|
+
"`{ type: 'user', name: 'owner' }` — the field NAME carries the ownership " +
|
|
60
|
+
"meaning, the type carries the widget. The `widget: 'field:owner'` spelling " +
|
|
61
|
+
"is retired with it.",
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* THE retirement gate — true when a spelling has been retired by this renderer.
|
|
65
|
+
*
|
|
66
|
+
* Ruled into existence by the maintainer on 2026-08-18 (objectui#4914, option
|
|
67
|
+
* B) as the ONE question every field-type predicate asks before it answers:
|
|
68
|
+
* "a retired spelling fires `reportRetiredFieldType` once and takes the
|
|
69
|
+
* established disposition, instead of riding the lookup-family paths or
|
|
70
|
+
* silently degrading."
|
|
71
|
+
*
|
|
72
|
+
* Quantified over the whole table rather than written per spelling, which is
|
|
73
|
+
* the entire point of having a gate at all: the next retirement closes every
|
|
74
|
+
* face the day it lands in {@link RETIRED_FIELD_TYPES}, instead of leaving each
|
|
75
|
+
* consumer open until someone notices it. `owner` survived objectui#4814 in
|
|
76
|
+
* thirteen more faces exactly because each one had hand-written its own
|
|
77
|
+
* membership list.
|
|
78
|
+
*
|
|
79
|
+
* `hasOwnProperty`, not `RETIRED_FIELD_TYPES[type]`: a `type` of
|
|
80
|
+
* `'constructor'` or `'toString'` reaches `Object.prototype` and would answer
|
|
81
|
+
* truthy through a plain index read.
|
|
82
|
+
*
|
|
83
|
+
* @param type - The spelling an author wrote (or a backend column declared).
|
|
84
|
+
* @returns `true` when the spelling is retired. Pure — it does NOT log; pair it
|
|
85
|
+
* with {@link reportRetiredFieldType} at the face that wants to be loud.
|
|
86
|
+
*/
|
|
87
|
+
export function isRetiredFieldType(type) {
|
|
88
|
+
if (typeof type !== 'string' || !type)
|
|
89
|
+
return false;
|
|
90
|
+
return Object.prototype.hasOwnProperty.call(RETIRED_FIELD_TYPES, type);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Spellings already reported this session, so a retired type inside a rendered
|
|
94
|
+
* list logs its prescription ONCE instead of once per row. The message is a
|
|
95
|
+
* fix instruction for an author, not a per-render event — a 1000-row grid
|
|
96
|
+
* repeating it 1000 times buries the very thing it is trying to surface.
|
|
97
|
+
*
|
|
98
|
+
* Module state, and that is load bearing now that SEVEN faces call the reporter
|
|
99
|
+
* (objectui#4914): the dedupe is per SPELLING, not per face, so a filter row
|
|
100
|
+
* whose column is refused by `operatorsForFieldType`, by the control chooser
|
|
101
|
+
* and by `normalizeFieldType` on the same render still prints one line.
|
|
102
|
+
*/
|
|
103
|
+
const reportedRetiredTypes = new Set();
|
|
104
|
+
/**
|
|
105
|
+
* Report a retired field-type spelling loudly, once per spelling.
|
|
106
|
+
*
|
|
107
|
+
* @param fieldType - The spelling the author wrote.
|
|
108
|
+
* @returns `true` when `fieldType` is retired (whether or not this call was the
|
|
109
|
+
* one that logged), so callers can branch on it without a second table lookup.
|
|
110
|
+
*/
|
|
111
|
+
export function reportRetiredFieldType(fieldType) {
|
|
112
|
+
const prescription = isRetiredFieldType(fieldType) ? RETIRED_FIELD_TYPES[fieldType] : undefined;
|
|
113
|
+
if (!prescription)
|
|
114
|
+
return false;
|
|
115
|
+
if (!reportedRetiredTypes.has(fieldType)) {
|
|
116
|
+
reportedRetiredTypes.add(fieldType);
|
|
117
|
+
console.error(prescription);
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
/** Test seam — forget which spellings have been reported. */
|
|
122
|
+
export function resetRetiredFieldTypeReports() {
|
|
123
|
+
reportedRetiredTypes.clear();
|
|
124
|
+
}
|
|
@@ -16,21 +16,23 @@
|
|
|
16
16
|
* accepts four shapes; normalizing here means one shape reaches the wire and a
|
|
17
17
|
* block does not have to know which spelling its author used.
|
|
18
18
|
*
|
|
19
|
-
* This is the
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* how the conversions start disagreeing.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
19
|
+
* This is now the ONLY definition in the repo. It was hoisted here because
|
|
20
|
+
* objectstack#7137 added two more read sites (`object-timeline`,
|
|
21
|
+
* `record:line_items`) next to three sibling blocks that each inlined a
|
|
22
|
+
* byte-identical private copy (`ObjectGantt` / `ObjectMap` / `ObjectCalendar`),
|
|
23
|
+
* and a fifth and sixth copy is how the conversions start disagreeing. Those
|
|
24
|
+
* three copies were deliberately left alone by #7137 and collapsed onto this
|
|
25
|
+
* function by objectui#4022; every block now imports it from here.
|
|
26
26
|
*
|
|
27
|
-
* Two deliberate differences from those private copies, both of which
|
|
28
|
-
* function more faithful to the declared contract rather than adding
|
|
27
|
+
* Two deliberate differences from those retired private copies, both of which
|
|
28
|
+
* make this function more faithful to the declared contract rather than adding
|
|
29
|
+
* tolerance — and both are BEHAVIOUR CHANGES the migration delivered, not pure
|
|
30
|
+
* refactor:
|
|
29
31
|
*
|
|
30
32
|
* - **`order` is optional in `SortConfig`**, so an entry that omits it means
|
|
31
33
|
* ascending (that is what `$orderby`'s own
|
|
32
34
|
* `Array<{ field: string; order?: 'asc' | 'desc' }>` shape says). The private
|
|
33
|
-
* copies
|
|
35
|
+
* copies required BOTH keys and silently dropped such an entry, which lost an
|
|
34
36
|
* authored sort key instead of ordering by it.
|
|
35
37
|
* - **Nothing usable yields `undefined`, never `{}`.** An empty object is a
|
|
36
38
|
* truthy value that means "no ordering" only by accident of the adapter's
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — unmaterialized-fields utility
|
|
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
|
+
* Field types no driver materialises a stored column for — the value is
|
|
10
|
+
* computed on read (objectui#3950).
|
|
11
|
+
*
|
|
12
|
+
* A STORAGE fact, not a taste judgement, and it decides what the SERVER can be
|
|
13
|
+
* asked to do with such a field. All three query axes have now closed on it:
|
|
14
|
+
*
|
|
15
|
+
* - SORT — refused since objectstack#6994 (`400 INVALID_SORT`). Before that it
|
|
16
|
+
* degraded in silence: the response carried the very values it was asked to
|
|
17
|
+
* order by, out of order, under a `200`, with ascending and descending
|
|
18
|
+
* byte-identical (measured on a real SQL driver).
|
|
19
|
+
* - FILTER — refused since objectstack#8296 (`400 INVALID_FIELD`); previously
|
|
20
|
+
* `200` with zero rows whichever way the predicate pointed.
|
|
21
|
+
* - SEARCH — refused by name since objectstack#6674.
|
|
22
|
+
*
|
|
23
|
+
* The membership is bound to `@objectstack/spec` instead of being written out
|
|
24
|
+
* here, on the reasoning the platform's own filter door applies to itself: a
|
|
25
|
+
* layer that re-decided "which types have a column" drifts from the drivers,
|
|
26
|
+
* and then either withholds an affordance that works or offers one that 400s.
|
|
27
|
+
* The spec pins the set to `formula` alone.
|
|
28
|
+
*
|
|
29
|
+
* Deliberately NOT `COMPUTED_VALUE_TYPES` (`formula` / `summary` /
|
|
30
|
+
* `autonumber`) — that is the WRITE contract ("never client-written"). A
|
|
31
|
+
* `summary` and an `autonumber` each get a real maintained column and order,
|
|
32
|
+
* filter and search correctly, so widening to that set would withhold
|
|
33
|
+
* affordances from two types that work. The platform's own conformance suite
|
|
34
|
+
* pins that trap by name.
|
|
35
|
+
*
|
|
36
|
+
* SCOPE on the sort axis: this rules out a sort the SERVER performs. A
|
|
37
|
+
* client-side sort keys off the value the cell renders — which the server
|
|
38
|
+
* hydrates on read — so it orders by exactly what the user sees and stays
|
|
39
|
+
* honest. Consumers therefore apply this where they already apply the
|
|
40
|
+
* relational carve-out (see {@link EXPANDABLE_FIELD_TYPES}): under server
|
|
41
|
+
* sorting only.
|
|
42
|
+
*/
|
|
43
|
+
export declare const UNMATERIALIZED_FIELD_TYPES: ReadonlySet<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Whether a field definition's type materialises no stored column.
|
|
46
|
+
*
|
|
47
|
+
* Shaped like {@link isExpandableFieldType}: only `type` is read, so it answers
|
|
48
|
+
* for any field-metadata shape an object schema resolves to, and an unreadable
|
|
49
|
+
* definition is not a verdict — `null` / `undefined` / a bare type string all
|
|
50
|
+
* answer `false`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function isUnmaterializedFieldType(fieldDef: unknown): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — unmaterialized-fields utility
|
|
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
|
+
import { SEARCH_VIRTUAL_TYPES } from '@objectstack/spec/data';
|
|
9
|
+
/**
|
|
10
|
+
* Field types no driver materialises a stored column for — the value is
|
|
11
|
+
* computed on read (objectui#3950).
|
|
12
|
+
*
|
|
13
|
+
* A STORAGE fact, not a taste judgement, and it decides what the SERVER can be
|
|
14
|
+
* asked to do with such a field. All three query axes have now closed on it:
|
|
15
|
+
*
|
|
16
|
+
* - SORT — refused since objectstack#6994 (`400 INVALID_SORT`). Before that it
|
|
17
|
+
* degraded in silence: the response carried the very values it was asked to
|
|
18
|
+
* order by, out of order, under a `200`, with ascending and descending
|
|
19
|
+
* byte-identical (measured on a real SQL driver).
|
|
20
|
+
* - FILTER — refused since objectstack#8296 (`400 INVALID_FIELD`); previously
|
|
21
|
+
* `200` with zero rows whichever way the predicate pointed.
|
|
22
|
+
* - SEARCH — refused by name since objectstack#6674.
|
|
23
|
+
*
|
|
24
|
+
* The membership is bound to `@objectstack/spec` instead of being written out
|
|
25
|
+
* here, on the reasoning the platform's own filter door applies to itself: a
|
|
26
|
+
* layer that re-decided "which types have a column" drifts from the drivers,
|
|
27
|
+
* and then either withholds an affordance that works or offers one that 400s.
|
|
28
|
+
* The spec pins the set to `formula` alone.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately NOT `COMPUTED_VALUE_TYPES` (`formula` / `summary` /
|
|
31
|
+
* `autonumber`) — that is the WRITE contract ("never client-written"). A
|
|
32
|
+
* `summary` and an `autonumber` each get a real maintained column and order,
|
|
33
|
+
* filter and search correctly, so widening to that set would withhold
|
|
34
|
+
* affordances from two types that work. The platform's own conformance suite
|
|
35
|
+
* pins that trap by name.
|
|
36
|
+
*
|
|
37
|
+
* SCOPE on the sort axis: this rules out a sort the SERVER performs. A
|
|
38
|
+
* client-side sort keys off the value the cell renders — which the server
|
|
39
|
+
* hydrates on read — so it orders by exactly what the user sees and stays
|
|
40
|
+
* honest. Consumers therefore apply this where they already apply the
|
|
41
|
+
* relational carve-out (see {@link EXPANDABLE_FIELD_TYPES}): under server
|
|
42
|
+
* sorting only.
|
|
43
|
+
*/
|
|
44
|
+
export const UNMATERIALIZED_FIELD_TYPES = SEARCH_VIRTUAL_TYPES;
|
|
45
|
+
/**
|
|
46
|
+
* Whether a field definition's type materialises no stored column.
|
|
47
|
+
*
|
|
48
|
+
* Shaped like {@link isExpandableFieldType}: only `type` is read, so it answers
|
|
49
|
+
* for any field-metadata shape an object schema resolves to, and an unreadable
|
|
50
|
+
* definition is not a verdict — `null` / `undefined` / a bare type string all
|
|
51
|
+
* answer `false`.
|
|
52
|
+
*/
|
|
53
|
+
export function isUnmaterializedFieldType(fieldDef) {
|
|
54
|
+
return (!!fieldDef &&
|
|
55
|
+
typeof fieldDef === 'object' &&
|
|
56
|
+
UNMATERIALIZED_FIELD_TYPES.has(fieldDef.type));
|
|
57
|
+
}
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* is client-side form validation, not a mirror of a server rule.
|
|
14
14
|
*/
|
|
15
15
|
export * from './required-presence.js';
|
|
16
|
+
export * from './server-owned-value.js';
|
|
16
17
|
export * from './validation-engine.js';
|
|
17
18
|
export * from './schema-validator.js';
|
|
18
19
|
export * from './validators/index.js';
|
package/dist/validation/index.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* is client-side form validation, not a mirror of a server rule.
|
|
14
14
|
*/
|
|
15
15
|
export * from './required-presence.js';
|
|
16
|
+
export * from './server-owned-value.js';
|
|
16
17
|
export * from './validation-engine.js';
|
|
17
18
|
export * from './schema-validator.js';
|
|
18
19
|
export * from './validators/index.js';
|
|
@@ -123,6 +123,105 @@ function validateCRUDSchema(schema, path = 'schema') {
|
|
|
123
123
|
}
|
|
124
124
|
return errors;
|
|
125
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* The ONE namespace a form field's widget id may name — objectui#5254,
|
|
128
|
+
* maintainer ruling of 2026-08-19.
|
|
129
|
+
*
|
|
130
|
+
* A form field's `type` either names a `field:`-namespaced widget or takes the
|
|
131
|
+
* renderer's declared `default` input branch. It never resolves a plain SDUI
|
|
132
|
+
* node renderer: `renderFieldComponent` (`@object-ui/components`) resolves a
|
|
133
|
+
* colon-qualified id ONLY under this prefix, and `resolveFieldLabelling` /
|
|
134
|
+
* `resolvesToRegisteredFieldWidget` there mirror it exactly.
|
|
135
|
+
*/
|
|
136
|
+
const FIELD_WIDGET_NAMESPACE = 'field:';
|
|
137
|
+
/**
|
|
138
|
+
* The widget id a form field will actually be RENDERED as.
|
|
139
|
+
*
|
|
140
|
+
* Mirrors the renderer's own precedence (`resolvedType` in
|
|
141
|
+
* `packages/components/src/renderers/form/form.tsx`: explicit form-config
|
|
142
|
+
* `widget`, then the resolved field metadata's widget hint, then the bare
|
|
143
|
+
* `type`). Only the two keys an AUTHORED schema carries are read here —
|
|
144
|
+
* `field.field?.widget` is a resolved metadata OBJECT that a hand-authored
|
|
145
|
+
* standalone `FormSchema` does not have.
|
|
146
|
+
*
|
|
147
|
+
* The mirroring is the point, not a convenience: validator and renderer must
|
|
148
|
+
* not be able to disagree about which component will actually render, which is
|
|
149
|
+
* the same discipline `resolveFieldLabelling` states on the renderer side.
|
|
150
|
+
*/
|
|
151
|
+
function resolveAuthoredFieldWidgetId(field) {
|
|
152
|
+
const id = field.widget ?? field.type;
|
|
153
|
+
return typeof id === 'string' ? id : undefined;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* A form field's widget id names a namespace that resolves NO field widget —
|
|
157
|
+
* objectui#5375, maintainer ruling of 2026-08-20 (option C, the primary).
|
|
158
|
+
*
|
|
159
|
+
* ## What this refuses, and why it is an error rather than a warning
|
|
160
|
+
*
|
|
161
|
+
* Measured on `main` at f2e11ae6f, the real `form` renderer on the built-in
|
|
162
|
+
* path (no `registerAllFields()`):
|
|
163
|
+
*
|
|
164
|
+
* type registry hit rendered type
|
|
165
|
+
* ui:password TRUE text
|
|
166
|
+
* secret false text
|
|
167
|
+
* field:secret false text
|
|
168
|
+
*
|
|
169
|
+
* `ui:password` **is** registered — as an SDUI node renderer for a top-level
|
|
170
|
+
* `{ type: 'email' }`-style node — so an author who checks whether it resolves
|
|
171
|
+
* gets a YES, and still gets a clear-text box on the field path. That is the
|
|
172
|
+
* shape where verifying does not protect you, and it is why the class, not
|
|
173
|
+
* another entry in a renderer-side table, is what gets closed here: the table
|
|
174
|
+
* fix answers today's spellings and leaves the next invented id to find the
|
|
175
|
+
* same silent degrade. AI-authored metadata invents plausible-looking widget
|
|
176
|
+
* ids constantly; this makes inventing one an ERROR at authoring time instead
|
|
177
|
+
* of a text box that looks like it worked.
|
|
178
|
+
*
|
|
179
|
+
* A warning was explicitly ruled out — a warning IS the silent degrade this
|
|
180
|
+
* check exists to kill, in a new spelling.
|
|
181
|
+
*
|
|
182
|
+
* ## Why only the NAMESPACED spelling
|
|
183
|
+
*
|
|
184
|
+
* `field:` is allowed whether or not the widget is registered: registration is
|
|
185
|
+
* a RUNTIME fact (`registerAllFields()`, a lazily loaded plugin) that an
|
|
186
|
+
* authoring-time validator cannot see, and the renderer already answers an
|
|
187
|
+
* unregistered `field:` secret with a visible refusal (objectui#5322). A BARE
|
|
188
|
+
* name is likewise allowed: it is an open set — every registered `field:<name>`
|
|
189
|
+
* widget, third-party ones included, is reachable by its short name.
|
|
190
|
+
*
|
|
191
|
+
* What is decidable statically is the namespace, and it is a CLOSED set of one.
|
|
192
|
+
* Any other `<ns>:<name>` resolves nothing on the field path by the #5254
|
|
193
|
+
* ruling and falls to the renderer's plain-text `default` arm.
|
|
194
|
+
*
|
|
195
|
+
* ## Census (the ruling made arming this conditional on one)
|
|
196
|
+
*
|
|
197
|
+
* 4,397 authored files in this repo, 649 form-field `type`/`widget` literals,
|
|
198
|
+
* 98 structurally parsed form-field entries in `examples/schema-catalog`:
|
|
199
|
+
* SEVEN colon-qualified ids on the form-field path, five of them `field:*`.
|
|
200
|
+
* The two remaining occurrences are one source line naming `ref:component`,
|
|
201
|
+
* which is a metadata-admin designer `SchemaForm` widget resolved by its own
|
|
202
|
+
* `WIDGETS` table — a different shape (`{ field, widget }` under
|
|
203
|
+
* `sections[].fields[]`, node type `simple`) that this function, gated on
|
|
204
|
+
* `type === 'form'`, never reaches. Zero occurrences of `ui:password`,
|
|
205
|
+
* `secret` or `field:secret` anywhere on the field path.
|
|
206
|
+
*/
|
|
207
|
+
function validateFieldWidgetNamespace(field, path) {
|
|
208
|
+
const id = resolveAuthoredFieldWidgetId(field);
|
|
209
|
+
if (!id || !id.includes(':') || id.startsWith(FIELD_WIDGET_NAMESPACE))
|
|
210
|
+
return undefined;
|
|
211
|
+
const key = typeof field.widget === 'string' ? 'widget' : 'type';
|
|
212
|
+
return {
|
|
213
|
+
path: `${path}.${key}`,
|
|
214
|
+
message: `'${id}' is not a form field widget: a namespaced field widget id must name the ` +
|
|
215
|
+
`\`field:\` namespace (objectui#5254), so this id resolves NOTHING on the field path ` +
|
|
216
|
+
`and the renderer would fall through to a plain text box — putting a secret on screen ` +
|
|
217
|
+
`in clear text when the field holds one (objectui#5375). Write the built-in spelling ` +
|
|
218
|
+
`(e.g. \`password\`), or the field widget id \`field:${id.slice(id.indexOf(':') + 1)}\`. ` +
|
|
219
|
+
`Resolving in the registry is not proof it resolves HERE — \`ui:password\` is a ` +
|
|
220
|
+
`registered SDUI node renderer and still renders clear text as a field.`,
|
|
221
|
+
type: 'error',
|
|
222
|
+
code: 'UNRESOLVABLE_FIELD_WIDGET_NAMESPACE',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
126
225
|
/**
|
|
127
226
|
* Validate form schema specific properties
|
|
128
227
|
*/
|
|
@@ -139,6 +238,12 @@ function validateFormSchema(schema, path = 'schema') {
|
|
|
139
238
|
code: 'MISSING_FIELD_NAME'
|
|
140
239
|
});
|
|
141
240
|
}
|
|
241
|
+
// objectui#5375 — an invented namespaced widget id is an ERROR here,
|
|
242
|
+
// not a silent clear-text box at render time. See the helper's doc.
|
|
243
|
+
const namespaceError = validateFieldWidgetNamespace(field, `${path}.fields[${index}]`);
|
|
244
|
+
if (namespaceError) {
|
|
245
|
+
errors.push(namespaceError);
|
|
246
|
+
}
|
|
142
247
|
// Check for duplicate field names
|
|
143
248
|
const duplicates = schema.fields.filter((f) => f.name === field.name);
|
|
144
249
|
if (duplicates.length > 1) {
|