@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.
- package/CHANGELOG.md +2018 -0
- package/README.md +38 -34
- package/dist/actions/ActionEngine.d.ts +43 -26
- package/dist/actions/ActionEngine.js +67 -54
- package/dist/actions/ActionRunner.d.ts +281 -31
- package/dist/actions/ActionRunner.js +458 -36
- package/dist/actions/actionKeys.d.ts +163 -27
- package/dist/actions/actionKeys.js +238 -27
- 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/data-scope/ViewDataProvider.d.ts +37 -12
- package/dist/data-scope/ViewDataProvider.js +83 -5
- package/dist/data-scope/element-data-source.d.ts +113 -0
- package/dist/data-scope/element-data-source.js +216 -0
- package/dist/data-scope/index.d.ts +2 -1
- package/dist/data-scope/index.js +1 -0
- 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.d.ts +119 -0
- package/dist/evaluator/declaredPredicate.js +152 -0
- package/dist/evaluator/fieldRules.d.ts +35 -0
- package/dist/evaluator/fieldRules.js +79 -20
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +54 -4
- package/dist/evaluator/listConditional.js +125 -19
- package/dist/evaluator/optionRules.d.ts +55 -0
- package/dist/evaluator/optionRules.js +60 -0
- package/dist/evaluator/predicateInput.d.ts +51 -3
- package/dist/evaluator/predicateInput.js +62 -5
- package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
- package/dist/evaluator/rowPredicateCanon.js +208 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +31 -1
- package/dist/protocols/DndProtocol.d.ts +42 -1
- package/dist/protocols/KeyboardProtocol.d.ts +31 -1
- package/dist/protocols/SharingProtocol.d.ts +11 -1
- package/dist/registry/Registry.d.ts +62 -16
- package/dist/theme/ThemeEngine.d.ts +0 -8
- package/dist/theme/ThemeEngine.js +22 -103
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +7 -1
- package/dist/types/index.d.ts +125 -12
- package/dist/utils/chart-presentation.d.ts +205 -0
- package/dist/utils/chart-presentation.js +280 -0
- package/dist/utils/chart-series.d.ts +390 -23
- package/dist/utils/chart-series.js +776 -32
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/compare-to.d.ts +40 -13
- package/dist/utils/compare-to.js +4 -32
- package/dist/utils/dashboard-filters.d.ts +54 -8
- package/dist/utils/dashboard-filters.js +261 -12
- package/dist/utils/dataset-format.d.ts +73 -14
- package/dist/utils/dataset-format.js +142 -9
- package/dist/utils/dataset-pivot.d.ts +28 -7
- package/dist/utils/dataset-pivot.js +27 -6
- package/dist/utils/debug.js +7 -0
- package/dist/utils/dom-props.d.ts +137 -0
- package/dist/utils/dom-props.js +153 -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 +36 -6
- package/dist/utils/normalize-list-view.js +93 -9
- package/dist/utils/number-display.d.ts +155 -0
- package/dist/utils/number-display.js +71 -0
- package/dist/utils/predicate-fields.d.ts +29 -4
- package/dist/utils/predicate-fields.js +44 -4
- package/dist/utils/resolve-view-id.d.ts +31 -0
- package/dist/utils/resolve-view-id.js +45 -0
- 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 +57 -0
- package/dist/utils/sort-query.js +35 -0
- 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 +5 -5
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
- package/dist/utils/merge-views-into-objects.d.ts +0 -19
- 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';
|
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) {
|
|
@@ -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;
|