@object-ui/core 17.3.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +2018 -0
  2. package/README.md +38 -34
  3. package/dist/actions/ActionEngine.d.ts +43 -26
  4. package/dist/actions/ActionEngine.js +67 -54
  5. package/dist/actions/ActionRunner.d.ts +281 -31
  6. package/dist/actions/ActionRunner.js +458 -36
  7. package/dist/actions/actionKeys.d.ts +163 -27
  8. package/dist/actions/actionKeys.js +238 -27
  9. package/dist/actions/actionResponse.js +1 -1
  10. package/dist/actions/index.d.ts +1 -0
  11. package/dist/actions/index.js +1 -0
  12. package/dist/actions/recordIdParam.d.ts +77 -0
  13. package/dist/actions/recordIdParam.js +37 -0
  14. package/dist/actions/serverActionHandler.d.ts +1 -1
  15. package/dist/actions/serverActionHandler.js +1 -1
  16. package/dist/data-scope/ViewDataProvider.d.ts +37 -12
  17. package/dist/data-scope/ViewDataProvider.js +83 -5
  18. package/dist/data-scope/element-data-source.d.ts +113 -0
  19. package/dist/data-scope/element-data-source.js +216 -0
  20. package/dist/data-scope/index.d.ts +2 -1
  21. package/dist/data-scope/index.js +1 -0
  22. package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
  23. package/dist/evaluator/ExpressionEvaluator.js +42 -1
  24. package/dist/evaluator/SafeExpressionParser.js +0 -4
  25. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  26. package/dist/evaluator/declaredPredicate.js +152 -0
  27. package/dist/evaluator/fieldRules.d.ts +35 -0
  28. package/dist/evaluator/fieldRules.js +79 -20
  29. package/dist/evaluator/index.d.ts +2 -0
  30. package/dist/evaluator/index.js +2 -0
  31. package/dist/evaluator/listConditional.d.ts +54 -4
  32. package/dist/evaluator/listConditional.js +125 -19
  33. package/dist/evaluator/optionRules.d.ts +55 -0
  34. package/dist/evaluator/optionRules.js +60 -0
  35. package/dist/evaluator/predicateInput.d.ts +51 -3
  36. package/dist/evaluator/predicateInput.js +62 -5
  37. package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
  38. package/dist/evaluator/rowPredicateCanon.js +208 -0
  39. package/dist/index.d.ts +8 -1
  40. package/dist/index.js +31 -1
  41. package/dist/protocols/DndProtocol.d.ts +42 -1
  42. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  43. package/dist/protocols/SharingProtocol.d.ts +11 -1
  44. package/dist/registry/Registry.d.ts +62 -16
  45. package/dist/theme/ThemeEngine.d.ts +0 -8
  46. package/dist/theme/ThemeEngine.js +22 -103
  47. package/dist/theme/index.d.ts +1 -1
  48. package/dist/theme/index.js +7 -1
  49. package/dist/types/index.d.ts +125 -12
  50. package/dist/utils/chart-presentation.d.ts +205 -0
  51. package/dist/utils/chart-presentation.js +280 -0
  52. package/dist/utils/chart-series.d.ts +390 -23
  53. package/dist/utils/chart-series.js +776 -32
  54. package/dist/utils/column-identity.d.ts +43 -0
  55. package/dist/utils/column-identity.js +47 -1
  56. package/dist/utils/compare-to.d.ts +40 -13
  57. package/dist/utils/compare-to.js +4 -32
  58. package/dist/utils/dashboard-filters.d.ts +54 -8
  59. package/dist/utils/dashboard-filters.js +261 -12
  60. package/dist/utils/dataset-format.d.ts +73 -14
  61. package/dist/utils/dataset-format.js +142 -9
  62. package/dist/utils/dataset-pivot.d.ts +28 -7
  63. package/dist/utils/dataset-pivot.js +27 -6
  64. package/dist/utils/debug.js +7 -0
  65. package/dist/utils/dom-props.d.ts +137 -0
  66. package/dist/utils/dom-props.js +153 -0
  67. package/dist/utils/expand-fields.d.ts +97 -7
  68. package/dist/utils/expand-fields.js +97 -7
  69. package/dist/utils/filter-tokens.js +0 -1
  70. package/dist/utils/freeze-schema.d.ts +38 -1
  71. package/dist/utils/freeze-schema.js +10 -0
  72. package/dist/utils/humanize-label.d.ts +60 -0
  73. package/dist/utils/humanize-label.js +62 -0
  74. package/dist/utils/managedBy.d.ts +27 -2
  75. package/dist/utils/normalize-list-view.d.ts +36 -6
  76. package/dist/utils/normalize-list-view.js +93 -9
  77. package/dist/utils/number-display.d.ts +155 -0
  78. package/dist/utils/number-display.js +71 -0
  79. package/dist/utils/predicate-fields.d.ts +29 -4
  80. package/dist/utils/predicate-fields.js +44 -4
  81. package/dist/utils/resolve-view-id.d.ts +31 -0
  82. package/dist/utils/resolve-view-id.js +45 -0
  83. package/dist/utils/retired-field-types.d.ts +90 -0
  84. package/dist/utils/retired-field-types.js +124 -0
  85. package/dist/utils/sort-query.d.ts +57 -0
  86. package/dist/utils/sort-query.js +35 -0
  87. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  88. package/dist/utils/unmaterialized-fields.js +57 -0
  89. package/dist/validation/index.d.ts +1 -0
  90. package/dist/validation/index.js +1 -0
  91. package/dist/validation/schema-validator.js +105 -0
  92. package/dist/validation/server-owned-value.d.ts +62 -0
  93. package/dist/validation/server-owned-value.js +99 -0
  94. package/dist/validation/validators/object-validation-engine.js +0 -2
  95. package/package.json +5 -5
  96. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  97. package/dist/__benchmarks__/core.bench.js +0 -53
  98. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  99. package/dist/utils/merge-views-into-objects.js +0 -39
@@ -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
+ }
@@ -0,0 +1,57 @@
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
+ * sort-query — lower a block's authored `sort` to the `$orderby` value a
10
+ * `DataSource.find` query carries.
11
+ *
12
+ * Two authored spellings reach every object-bound block, because both are
13
+ * declared: the legacy OData-ish string (`ObjectGridSchema.sort: "name desc"`)
14
+ * and the spec's `SortConfig[]` (`[{ field, order }]`). A read site that hands
15
+ * either straight to `$orderby` works only because `QueryParams.$orderby`
16
+ * accepts four shapes; normalizing here means one shape reaches the wire and a
17
+ * block does not have to know which spelling its author used.
18
+ *
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
+ *
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:
31
+ *
32
+ * - **`order` is optional in `SortConfig`**, so an entry that omits it means
33
+ * ascending (that is what `$orderby`'s own
34
+ * `Array<{ field: string; order?: 'asc' | 'desc' }>` shape says). The private
35
+ * copies required BOTH keys and silently dropped such an entry, which lost an
36
+ * authored sort key instead of ordering by it.
37
+ * - **Nothing usable yields `undefined`, never `{}`.** An empty object is a
38
+ * truthy value that means "no ordering" only by accident of the adapter's
39
+ * serializer; `undefined` says it, so the query simply carries no `$orderby`.
40
+ *
41
+ * Not a lenient alias: an unusable input (a number, an object, an array of
42
+ * strings) yields `undefined` rather than a guess. Only the two spellings the
43
+ * schema types declare are honoured.
44
+ */
45
+ /** A field name paired with a direction — the spec's `SortConfig`. */
46
+ export interface QuerySortEntry {
47
+ field?: string;
48
+ order?: 'asc' | 'desc';
49
+ }
50
+ /**
51
+ * Normalize an authored `sort` into the field→direction map used for
52
+ * `QueryParams.$orderby`.
53
+ *
54
+ * @param sort `"name desc"` (legacy string) or `SortConfig[]`.
55
+ * @returns The ordering map, or `undefined` when nothing orderable was authored.
56
+ */
57
+ export declare function convertSortToQueryParams(sort: string | QuerySortEntry[] | undefined | null): Record<string, 'asc' | 'desc'> | undefined;
@@ -0,0 +1,35 @@
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
+ * Normalize an authored `sort` into the field→direction map used for
10
+ * `QueryParams.$orderby`.
11
+ *
12
+ * @param sort `"name desc"` (legacy string) or `SortConfig[]`.
13
+ * @returns The ordering map, or `undefined` when nothing orderable was authored.
14
+ */
15
+ export function convertSortToQueryParams(sort) {
16
+ if (!sort)
17
+ return undefined;
18
+ // Legacy string clause: "name desc" / "name" (a bare field means ascending).
19
+ if (typeof sort === 'string') {
20
+ const [field, direction] = sort.trim().split(/\s+/);
21
+ if (!field)
22
+ return undefined;
23
+ return { [field]: direction?.toLowerCase() === 'desc' ? 'desc' : 'asc' };
24
+ }
25
+ if (Array.isArray(sort)) {
26
+ const out = {};
27
+ for (const entry of sort) {
28
+ if (!entry || typeof entry.field !== 'string' || entry.field === '')
29
+ continue;
30
+ out[entry.field] = entry.order === 'desc' ? 'desc' : 'asc';
31
+ }
32
+ return Object.keys(out).length > 0 ? out : undefined;
33
+ }
34
+ return undefined;
35
+ }
@@ -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';
@@ -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) {
@@ -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;