@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
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
import { evalFieldPredicate } from './fieldRules.js';
|
|
37
37
|
import { ExpressionEvaluator } from './ExpressionEvaluator.js';
|
|
38
38
|
import { toPredicateRecord } from '../utils/predicate-record.js';
|
|
39
|
+
import { warnNonCanonicalRowSpelling } from './rowPredicateCanon.js';
|
|
39
40
|
/**
|
|
40
41
|
* Syntax that only the legacy JS-dialect evaluator understands and that is NOT
|
|
41
42
|
* valid CEL: `${…}` templates, strict (in)equality, JS optional-chaining
|
|
@@ -62,7 +63,14 @@ function warnLegacyDialect(source) {
|
|
|
62
63
|
}
|
|
63
64
|
const warnedError = new Set();
|
|
64
65
|
/**
|
|
65
|
-
* One-time
|
|
66
|
+
* One-time report for a predicate that could not be evaluated.
|
|
67
|
+
*
|
|
68
|
+
* The message names the caller's `fallback` as "its safe default" without
|
|
69
|
+
* asserting a direction, because the direction is the caller's: row surfaces
|
|
70
|
+
* fail CLOSED (a faulting `visible` hides the action), while a param-collection
|
|
71
|
+
* dialog fails OPEN (objectui#4640 — a silently hidden required param is the
|
|
72
|
+
* undiagnosable failure). Both are loud; only silence was ever ruled out
|
|
73
|
+
* (2026-08-06 on objectui#4051 / objectstack#5149).
|
|
66
74
|
*
|
|
67
75
|
* `reason` is the engine's own description of the failure (`"[runtime] No such
|
|
68
76
|
* key: owner_id"`) — the single most useful line for the author, and precisely
|
|
@@ -129,11 +137,34 @@ function evalCel(pred, record, scope) {
|
|
|
129
137
|
* Evaluate a single boolean predicate against a row record on the canonical CEL
|
|
130
138
|
* engine (with a legacy-dialect fallback — see the module note). The row's
|
|
131
139
|
* fields are bound three ways so every authoring convention resolves:
|
|
132
|
-
* `record.status
|
|
133
|
-
*
|
|
140
|
+
* `record.status`, bare `status` (row-action shorthand), and `data.status`.
|
|
141
|
+
*
|
|
142
|
+
* ⚠️ Those three are NOT peers, and this doc comment used to read as though
|
|
143
|
+
* they were. **The canon is `record.*`** (maintainer ruling 2026-08-20 on
|
|
144
|
+
* objectui#5330, option B); the other two are client tolerances in a
|
|
145
|
+
* deprecation window, kept because stored metadata carries them and warned
|
|
146
|
+
* about — from the CEL path below — by `warnNonCanonicalRowSpelling`. The
|
|
147
|
+
* server accepts `record.*` and NOTHING else: measured on
|
|
148
|
+
* `@objectstack/formula@17.1.0`, `buildScope({ record })` mounts exactly
|
|
149
|
+
* `['record']`, so a bare field faults `Unknown variable: status` there and a
|
|
150
|
+
* `data.*` predicate faults `Unknown variable: data`. See
|
|
151
|
+
* {@link ./rowPredicateCanon.ts} for the full measurement, for why `data.*` is
|
|
152
|
+
* the dangerous one (it is silently ACCEPTED by the server's authoring oracle
|
|
153
|
+
* and still binds nothing at runtime), and for why the deprecation is scoped to
|
|
154
|
+
* this runtime layer rather than declared platform-wide (`data` is the
|
|
155
|
+
* canonical root of a metadata-editing form — ADR-0089 D3).
|
|
156
|
+
*
|
|
157
|
+
* ⛔ No spelling is removed from the binding, and none may be before a
|
|
158
|
+
* stored-metadata survey sizes the window — that is part of the same ruling.
|
|
159
|
+
*
|
|
160
|
+
* The optional `scope` (host predicate scope) is bound
|
|
134
161
|
* alongside so `features.*` / `user.*` predicates keep working — but the row is
|
|
135
162
|
* the subject: `record` and `data` always name THIS row, on both dialect paths,
|
|
136
163
|
* even when the host scope carries keys of those names (objectui#3796).
|
|
164
|
+
*
|
|
165
|
+
* A caller with no row at all passes {@link RowPredicateOptions.rowless}, and
|
|
166
|
+
* then nothing is bound over the scope — see that option for why an absent row
|
|
167
|
+
* must not be spelled as an empty one.
|
|
137
168
|
*/
|
|
138
169
|
export function evalRowPredicate(pred, row, opts = {}) {
|
|
139
170
|
const fallback = opts.fallback ?? false;
|
|
@@ -145,7 +176,10 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
145
176
|
// Relations collapse to the foreign key the server stores, so `record.owner`
|
|
146
177
|
// means one thing whether or not this surface expanded the column
|
|
147
178
|
// (objectui#3501). Returned by reference when there is nothing to collapse.
|
|
148
|
-
|
|
179
|
+
// A `rowless` caller has no row to collapse, so `opts.fields` is moot there.
|
|
180
|
+
const rowObj = opts.rowless
|
|
181
|
+
? {}
|
|
182
|
+
: toPredicateRecord(row && typeof row === 'object' ? row : {}, opts.fields);
|
|
149
183
|
// Bare fields + `data.*` + `record.*` + the host scope, all top-level.
|
|
150
184
|
//
|
|
151
185
|
// `data` AND `record` are pinned AFTER the spread, so a host scope that
|
|
@@ -158,7 +192,28 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
158
192
|
// string happens to contain `===`/`${…}`, which no author is choosing
|
|
159
193
|
// deliberately (objectui#3796). Pinning here fixes both paths at the merge,
|
|
160
194
|
// independently of either engine's precedence.
|
|
161
|
-
|
|
195
|
+
//
|
|
196
|
+
// …UNLESS the caller has no row (`rowless`), in which case there is nothing
|
|
197
|
+
// to pin and the host scope keeps its own `record` / `data` — see the option.
|
|
198
|
+
const scope = opts.rowless
|
|
199
|
+
? { ...(opts.scope ?? {}) }
|
|
200
|
+
: { ...(opts.scope ?? {}), ...rowObj, data: rowObj, record: rowObj };
|
|
201
|
+
// The predicate TEXT for diagnostics: a bare string is itself, an envelope is
|
|
202
|
+
// its `source`. Reported separately from `source` above, which is `undefined`
|
|
203
|
+
// for an envelope *by design* (it drives the dialect routing, and an envelope
|
|
204
|
+
// is never routed to the legacy engine). Reusing it for the warning is what
|
|
205
|
+
// made every faulting envelope report as the literal `"(expression)"` — no
|
|
206
|
+
// predicate text for the author, and, because the one-time key is
|
|
207
|
+
// (label, source), the FIRST faulting envelope on a given label silenced every
|
|
208
|
+
// other one under it (objectui#4640: measured on `evalRowPredicate` — a second
|
|
209
|
+
// envelope with a different, also-broken source warned zero times). The
|
|
210
|
+
// envelope is not an exotic spelling: `@objectstack/spec`'s
|
|
211
|
+
// `ExpressionInputSchema` normalizes every authored predicate into one, so it
|
|
212
|
+
// is the likeliest shape in served metadata and was the least diagnosable.
|
|
213
|
+
const predicateText = source ??
|
|
214
|
+
(pred && typeof pred === 'object' && typeof pred.source === 'string'
|
|
215
|
+
? pred.source
|
|
216
|
+
: '(expression)');
|
|
162
217
|
// Legacy-dialect *strings* route to the legacy engine (back-compat). The
|
|
163
218
|
// `{ dialect: 'cel', source }` envelope is never routed here — it is CEL.
|
|
164
219
|
if (source !== undefined && isLegacyDialectSource(source)) {
|
|
@@ -176,6 +231,15 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
176
231
|
return fallback;
|
|
177
232
|
}
|
|
178
233
|
}
|
|
234
|
+
// CEL path — everything reaching here is CEL, which is what makes this the
|
|
235
|
+
// right place for the objectui#5330 spelling warning: in the legacy `${…}`
|
|
236
|
+
// dialect above, `data.*` is the NORMAL spelling, so reporting it there would
|
|
237
|
+
// be a false positive on every legacy predicate. `data` names THIS row unless
|
|
238
|
+
// the caller is `rowless` (then the host scope keeps its own — see the
|
|
239
|
+
// option), which is exactly the condition the detector needs.
|
|
240
|
+
if (predicateText !== '(expression)') {
|
|
241
|
+
warnNonCanonicalRowSpelling(predicateText, rowObj, !opts.rowless, opts.label);
|
|
242
|
+
}
|
|
179
243
|
// CEL path. The fault-aware `evalCel` costs two evaluations (to tell a fault
|
|
180
244
|
// from a genuine `false`), so only pay it when a caller wants the labelled
|
|
181
245
|
// fail-closed warning — the formatting hot path takes the single-eval fast
|
|
@@ -187,7 +251,7 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
187
251
|
}
|
|
188
252
|
const verdict = evalCel(pred, rowObj, scope);
|
|
189
253
|
if (!verdict.ok) {
|
|
190
|
-
warnEvalError(
|
|
254
|
+
warnEvalError(predicateText, opts.label, verdict.reason);
|
|
191
255
|
return fallback;
|
|
192
256
|
}
|
|
193
257
|
return verdict.value;
|
|
@@ -97,3 +97,58 @@ export interface CascadingOptions<T extends OptionLike> {
|
|
|
97
97
|
* purely by `visibleWhen`). Callers format their own hint from `dependsOnFields`.
|
|
98
98
|
*/
|
|
99
99
|
export declare function resolveCascadingOptions<T extends OptionLike>(rawOptions: readonly T[] | undefined | null, record: Record<string, unknown>, dependsOn: DependsOnInput, scope?: Record<string, unknown>): CascadingOptions<T>;
|
|
100
|
+
/**
|
|
101
|
+
* Widget keys whose OFFERED option set is re-resolved against a live record by
|
|
102
|
+
* {@link resolveCascadingOptions} — i.e. the allow-list of widgets a surface
|
|
103
|
+
* must thread its live record to (`dependentValues`). Each of their options may
|
|
104
|
+
* carry a `visibleWhen` predicate read against `record.*`, and the field may
|
|
105
|
+
* declare a `dependsOn` gate (ADR-0058 / objectui#2284 / objectui#1583).
|
|
106
|
+
* Without the record a cascading select never sees its controlling field and
|
|
107
|
+
* stays permanently gated on the "select the parent first" hint.
|
|
108
|
+
*
|
|
109
|
+
* ## One definition, three surfaces (objectui#4770)
|
|
110
|
+
*
|
|
111
|
+
* Three surfaces feed this one evaluator, and each carried its own private copy
|
|
112
|
+
* of these four keys until this constant existed — with no gate able to notice
|
|
113
|
+
* them drifting apart on what "the record" means:
|
|
114
|
+
*
|
|
115
|
+
* - the object form — `CASCADE_OPTION_FIELD_TYPES` in
|
|
116
|
+
* `components/src/renderers/form/form.tsx` (the original);
|
|
117
|
+
* - the single-record action dialog — `CASCADE_OPTION_WIDGET_TYPES` in
|
|
118
|
+
* `app-shell/src/views/ActionParamDialog.tsx` (objectui#3765 / PR #4756);
|
|
119
|
+
* - the bulk action dialog — the same name in
|
|
120
|
+
* `plugin-grid/src/components/BulkActionDialog.tsx` (objectui#4757 / PR #4763).
|
|
121
|
+
*
|
|
122
|
+
* The set lives in `@object-ui/core`, next to the evaluator it describes,
|
|
123
|
+
* because core is the one package all three already depend on — the form layer
|
|
124
|
+
* cannot reach `@object-ui/fields` (that dependency runs the other way). Same
|
|
125
|
+
* shape and same reason as `EXPANDABLE_FIELD_TYPES` in
|
|
126
|
+
* `core/src/utils/expand-fields.ts`, the in-repo precedent for a form-layer
|
|
127
|
+
* allow-table more than one surface reads. `@object-ui/fields` re-exports it
|
|
128
|
+
* next to `resolveFormWidgetType` for discoverability — a re-export, never a
|
|
129
|
+
* second definition.
|
|
130
|
+
*
|
|
131
|
+
* ## An ALLOW-LIST, not a blanket pass-through
|
|
132
|
+
*
|
|
133
|
+
* `dependentValues` is also the channel two widget-hint pickers read a specific
|
|
134
|
+
* SIBLING KEY from (`filter-condition` reads `object_name`, `recipient-picker`
|
|
135
|
+
* reads `recipient_type`), and the lookup family filters its query by it. All
|
|
136
|
+
* three surfaces above agree today in NOT feeding those from action / bulk
|
|
137
|
+
* dialog params, but that is a different wiring question from the one ruled
|
|
138
|
+
* here and it has never been decided either way. Stated here so the three
|
|
139
|
+
* copies of the statement become one — the boundary itself stays open:
|
|
140
|
+
* objectui#4771.
|
|
141
|
+
*
|
|
142
|
+
* ## Normalization stays with the consumer
|
|
143
|
+
*
|
|
144
|
+
* The members are WIDGET keys — `resolveFormWidgetType` output in the two
|
|
145
|
+
* dialogs, `normalizeFieldType` output (the `field:` prefix stripped) in the
|
|
146
|
+
* form — not authored `type` spellings. Each consumer keeps its own
|
|
147
|
+
* normalization; the two normalizations agree on these four members, which is
|
|
148
|
+
* what makes one shared set safe to read from both.
|
|
149
|
+
*
|
|
150
|
+
* `select` + `multiple: true` is not listed separately: `SelectField` delegates
|
|
151
|
+
* to `MultiSelectField` with the whole prop object, so the key it resolves
|
|
152
|
+
* under (`select`) is the one that must carry the record.
|
|
153
|
+
*/
|
|
154
|
+
export declare const CASCADE_OPTION_WIDGET_TYPES: ReadonlySet<string>;
|
|
@@ -77,3 +77,63 @@ export function resolveCascadingOptions(rawOptions, record, dependsOn, scope) {
|
|
|
77
77
|
const options = gated ? [] : resolveVisibleOptions(rawOptions, record, scope);
|
|
78
78
|
return { options, gated, dependsOnFields };
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Widget keys whose OFFERED option set is re-resolved against a live record by
|
|
82
|
+
* {@link resolveCascadingOptions} — i.e. the allow-list of widgets a surface
|
|
83
|
+
* must thread its live record to (`dependentValues`). Each of their options may
|
|
84
|
+
* carry a `visibleWhen` predicate read against `record.*`, and the field may
|
|
85
|
+
* declare a `dependsOn` gate (ADR-0058 / objectui#2284 / objectui#1583).
|
|
86
|
+
* Without the record a cascading select never sees its controlling field and
|
|
87
|
+
* stays permanently gated on the "select the parent first" hint.
|
|
88
|
+
*
|
|
89
|
+
* ## One definition, three surfaces (objectui#4770)
|
|
90
|
+
*
|
|
91
|
+
* Three surfaces feed this one evaluator, and each carried its own private copy
|
|
92
|
+
* of these four keys until this constant existed — with no gate able to notice
|
|
93
|
+
* them drifting apart on what "the record" means:
|
|
94
|
+
*
|
|
95
|
+
* - the object form — `CASCADE_OPTION_FIELD_TYPES` in
|
|
96
|
+
* `components/src/renderers/form/form.tsx` (the original);
|
|
97
|
+
* - the single-record action dialog — `CASCADE_OPTION_WIDGET_TYPES` in
|
|
98
|
+
* `app-shell/src/views/ActionParamDialog.tsx` (objectui#3765 / PR #4756);
|
|
99
|
+
* - the bulk action dialog — the same name in
|
|
100
|
+
* `plugin-grid/src/components/BulkActionDialog.tsx` (objectui#4757 / PR #4763).
|
|
101
|
+
*
|
|
102
|
+
* The set lives in `@object-ui/core`, next to the evaluator it describes,
|
|
103
|
+
* because core is the one package all three already depend on — the form layer
|
|
104
|
+
* cannot reach `@object-ui/fields` (that dependency runs the other way). Same
|
|
105
|
+
* shape and same reason as `EXPANDABLE_FIELD_TYPES` in
|
|
106
|
+
* `core/src/utils/expand-fields.ts`, the in-repo precedent for a form-layer
|
|
107
|
+
* allow-table more than one surface reads. `@object-ui/fields` re-exports it
|
|
108
|
+
* next to `resolveFormWidgetType` for discoverability — a re-export, never a
|
|
109
|
+
* second definition.
|
|
110
|
+
*
|
|
111
|
+
* ## An ALLOW-LIST, not a blanket pass-through
|
|
112
|
+
*
|
|
113
|
+
* `dependentValues` is also the channel two widget-hint pickers read a specific
|
|
114
|
+
* SIBLING KEY from (`filter-condition` reads `object_name`, `recipient-picker`
|
|
115
|
+
* reads `recipient_type`), and the lookup family filters its query by it. All
|
|
116
|
+
* three surfaces above agree today in NOT feeding those from action / bulk
|
|
117
|
+
* dialog params, but that is a different wiring question from the one ruled
|
|
118
|
+
* here and it has never been decided either way. Stated here so the three
|
|
119
|
+
* copies of the statement become one — the boundary itself stays open:
|
|
120
|
+
* objectui#4771.
|
|
121
|
+
*
|
|
122
|
+
* ## Normalization stays with the consumer
|
|
123
|
+
*
|
|
124
|
+
* The members are WIDGET keys — `resolveFormWidgetType` output in the two
|
|
125
|
+
* dialogs, `normalizeFieldType` output (the `field:` prefix stripped) in the
|
|
126
|
+
* form — not authored `type` spellings. Each consumer keeps its own
|
|
127
|
+
* normalization; the two normalizations agree on these four members, which is
|
|
128
|
+
* what makes one shared set safe to read from both.
|
|
129
|
+
*
|
|
130
|
+
* `select` + `multiple: true` is not listed separately: `SelectField` delegates
|
|
131
|
+
* to `MultiSelectField` with the whole prop object, so the key it resolves
|
|
132
|
+
* under (`select`) is the one that must carry the record.
|
|
133
|
+
*/
|
|
134
|
+
export const CASCADE_OPTION_WIDGET_TYPES = new Set([
|
|
135
|
+
'select',
|
|
136
|
+
'multiselect',
|
|
137
|
+
'radio',
|
|
138
|
+
'checkboxes',
|
|
139
|
+
]);
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
/** The one canonical row-predicate root. */
|
|
9
|
+
export declare const ROW_PREDICATE_CANONICAL_ROOT = "record";
|
|
10
|
+
/** A non-canonical row-predicate spelling, and the edit that fixes it. */
|
|
11
|
+
export type NonCanonicalRowSpelling = {
|
|
12
|
+
/** Bare shorthand: `status == 'active'` — a field of THIS row, unrooted. */
|
|
13
|
+
kind: 'bare-shorthand';
|
|
14
|
+
/** The offending identifier (`status`). */
|
|
15
|
+
identifier: string;
|
|
16
|
+
/** What the author should write instead (`record.status`). */
|
|
17
|
+
canonical: string;
|
|
18
|
+
} | {
|
|
19
|
+
/** Wrong-layer root: `data.status == 'active'` on a runtime record surface. */
|
|
20
|
+
kind: 'metadata-layer-root';
|
|
21
|
+
identifier: string;
|
|
22
|
+
canonical: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Detect a non-canonical spelling in ONE row predicate, or `null` when the
|
|
26
|
+
* predicate is already canonical (or is not this module's verdict to give).
|
|
27
|
+
*
|
|
28
|
+
* Deliberately conservative in both arms — every condition below can only
|
|
29
|
+
* REMOVE a finding, never invent one, because a false deprecation warning sends
|
|
30
|
+
* an author to rewrite a predicate that was correct:
|
|
31
|
+
*
|
|
32
|
+
* - **Unparseable source** → `null`. Syntax is another gate's verdict.
|
|
33
|
+
* - **Bare shorthand** is reported only when the undeclared identifier is an own
|
|
34
|
+
* key of THIS row. That is what makes it unambiguously the #4075 shorthand
|
|
35
|
+
* rather than a host-scope global this module cannot see (a deployment key
|
|
36
|
+
* that is simply not in `SCOPE_ROOTS` would otherwise read as a bare field).
|
|
37
|
+
* - **`data.*`** is reported only when the caller confirms `data` names the row
|
|
38
|
+
* (`dataNamesRow`). A surface whose `data` is the host scope's own — a
|
|
39
|
+
* `rowless` dialog, a metadata-editing form — is a legitimate `data.*` site
|
|
40
|
+
* and is left alone.
|
|
41
|
+
*
|
|
42
|
+
* Only the FIRST finding is returned, and the bare-shorthand arm is checked
|
|
43
|
+
* first: it is the arm the server refuses outright (`visibility-bare-identifier`
|
|
44
|
+
* is an `error` there, while a mis-layered root is a `warning`), so when a
|
|
45
|
+
* predicate manages both it is the one the author must fix to have anything
|
|
46
|
+
* evaluate at all.
|
|
47
|
+
*
|
|
48
|
+
* @param source The predicate's CEL text. Callers must have already
|
|
49
|
+
* routed legacy `${…}`-dialect strings elsewhere — in that
|
|
50
|
+
* dialect `data.*` is the NORMAL spelling, and reporting it
|
|
51
|
+
* here would be a false positive on every legacy predicate.
|
|
52
|
+
* @param row The row the predicate is bound against.
|
|
53
|
+
* @param dataNamesRow Whether `data` is bound to that same row on this surface.
|
|
54
|
+
*/
|
|
55
|
+
export declare function detectNonCanonicalRowSpelling(source: string, row: Record<string, unknown> | null | undefined, dataNamesRow: boolean): NonCanonicalRowSpelling | null;
|
|
56
|
+
/** Reset the row-predicate spelling warn-once memo. Exported for tests. */
|
|
57
|
+
export declare function resetRowPredicateCanonWarnings(): void;
|
|
58
|
+
/**
|
|
59
|
+
* Report a non-canonical row-predicate spelling once, in dev.
|
|
60
|
+
*
|
|
61
|
+
* Phase 1 of the objectui#5330 window: the binding is UNCHANGED and every
|
|
62
|
+
* spelling still resolves — this only says so out loud, so the stored
|
|
63
|
+
* population stops growing and a later survey has something to count. It is a
|
|
64
|
+
* warning and deliberately not a refusal: turning it into one would move the
|
|
65
|
+
* accept/reject set, which this card is explicitly not entitled to do.
|
|
66
|
+
*/
|
|
67
|
+
export declare function warnNonCanonicalRowSpelling(source: string, row: Record<string, unknown> | null | undefined, dataNamesRow: boolean, label?: string): void;
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
* The row-predicate spelling CANON, and the Phase-1 deprecation warning for the
|
|
10
|
+
* two spellings that are not it (objectui#5330).
|
|
11
|
+
*
|
|
12
|
+
* ## The canon (maintainer ruling, 2026-08-20 — option B)
|
|
13
|
+
*
|
|
14
|
+
* A row predicate — `visible` / `disabled` / `enabled` on an action renderer, a
|
|
15
|
+
* row scope, a `record:alert` — binds the row THREE ways today: canonical
|
|
16
|
+
* `record.status`, bare shorthand `status`, and legacy `data.status`
|
|
17
|
+
* (objectui#4075 / PR #4079 bound all four action renderers all three ways to
|
|
18
|
+
* restore consistency, deliberately without deciding which of them is
|
|
19
|
+
* CONTRACT). This module is where that decision now lives:
|
|
20
|
+
*
|
|
21
|
+
* > **The canon is `record.*`.** The bare shorthand and `data.*` are
|
|
22
|
+
* > client-side tolerances in a deprecation window, kept because stored
|
|
23
|
+
* > metadata carries them, warned about here, and removable only after a
|
|
24
|
+
* > stored-metadata survey sizes the window (⛔ no removal before the survey).
|
|
25
|
+
*
|
|
26
|
+
* It mirrors the objectstack#7917 option-② precedent for the identical shape (a
|
|
27
|
+
* renderer tolerance quietly becoming a second de-facto contract — AGENTS.md
|
|
28
|
+
* #0.1), whose objectui half is `utils/dashboard-filters.ts`' bare-string
|
|
29
|
+
* `options` shorthand: same three phases, same reason the warning is not
|
|
30
|
+
* decoration (ADR-0078 — nothing silently inert; a tolerance nothing ever
|
|
31
|
+
* reports can never be retired, because nothing would ever show that the last
|
|
32
|
+
* document carrying it is gone).
|
|
33
|
+
*
|
|
34
|
+
* ## The canon states the SERVER's accept set, not this client's
|
|
35
|
+
*
|
|
36
|
+
* The ruling made that the dev's first measurement, because a canon that only
|
|
37
|
+
* describes the renderer would be the very thing it exists to end. Measured
|
|
38
|
+
* against `@objectstack/formula@17.1.0` — the engine the server evaluates with,
|
|
39
|
+
* and the one `fieldRules.ts` already delegates to:
|
|
40
|
+
*
|
|
41
|
+
* | spelling | server runtime (`buildScope` + `celEngine`) | server authoring oracle |
|
|
42
|
+
* |---|---|---|
|
|
43
|
+
* | `record.status` | ✅ `{ ok: true, value: true }` | ✅ accepted |
|
|
44
|
+
* | bare `status` | ❌ `Unknown variable: status` | ❌ refused (`'status'`) |
|
|
45
|
+
* | `data.status` | ❌ `Unknown variable: data` | ⚠️ **silently accepted** |
|
|
46
|
+
*
|
|
47
|
+
* `buildScope({ record })` mounts exactly `['record']` — `data` is never bound
|
|
48
|
+
* and the row's fields are never flattened to top level. So **the server
|
|
49
|
+
* accepts `record.*` and nothing else**; both other spellings fault there. The
|
|
50
|
+
* three-way binding is a client tolerance with no server counterpart, which is
|
|
51
|
+
* precisely why it is the client's job to warn.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ The `data.*` row is the dangerous one and the reason this warning exists
|
|
54
|
+
* at all. `data` IS in `@objectstack/formula`'s `SCOPE_ROOTS`, so the server's
|
|
55
|
+
* bare-identifier oracle waves `data.status` through — that list is a
|
|
56
|
+
* deliberately generous "never faults" LINT BASELINE, not the runtime accept
|
|
57
|
+
* set. A `data.*` row predicate therefore passes every authoring gate the
|
|
58
|
+
* platform has and then binds nothing at runtime: it is not an error, it is a
|
|
59
|
+
* constant `false`, and a `visible` that is constantly false is a button that
|
|
60
|
+
* silently never appears. That is the #4075 fail-closed family's exact
|
|
61
|
+
* signature, and this client is the only layer positioned to catch it.
|
|
62
|
+
*
|
|
63
|
+
* ## `data.*` is DEPRECATED HERE, not everywhere — the canon is layer-scoped
|
|
64
|
+
*
|
|
65
|
+
* `data` is the CANONICAL root one layer over, in a metadata-editing form (the
|
|
66
|
+
* row under edit): objectstack's `CANONICAL_ROOT_BY_LAYER` reads
|
|
67
|
+
* `{ runtime: 'record', metadata: 'data' }` (ADR-0089 D3), and objectui's own
|
|
68
|
+
* `app-shell` metadata-admin `SchemaForm` binds `{ data: row }` on purpose. So
|
|
69
|
+
* `data.*` is not a legacy alias to be deprecated platform-wide — it is a
|
|
70
|
+
* WRONG-LAYER paste on a runtime record surface, and only that is what this
|
|
71
|
+
* module reports. Stating the deprecation unqualified would contradict
|
|
72
|
+
* ADR-0089 D3 and break the metadata-editing layer's own contract.
|
|
73
|
+
*
|
|
74
|
+
* ## Why the detection reuses the server's oracle instead of a regex
|
|
75
|
+
*
|
|
76
|
+
* `collectCelRootIdentifiers` and `firstUndeclaredReference` are
|
|
77
|
+
* `@objectstack/formula` exports — the same two oracles objectstack's
|
|
78
|
+
* `visibility-bare-identifier` gate uses. Reading roots off the canonical AST
|
|
79
|
+
* rather than pattern-matching the source is what keeps this from inventing a
|
|
80
|
+
* second dialect judgement client-side: a string this module cannot parse is
|
|
81
|
+
* NOT this module's verdict to give (syntax belongs to the gates that own it),
|
|
82
|
+
* and it stands down rather than guessing.
|
|
83
|
+
*/
|
|
84
|
+
import { collectCelRootIdentifiers, firstUndeclaredReference } from '@objectstack/formula';
|
|
85
|
+
/** The one canonical row-predicate root. */
|
|
86
|
+
export const ROW_PREDICATE_CANONICAL_ROOT = 'record';
|
|
87
|
+
/**
|
|
88
|
+
* The metadata-editing-form root (ADR-0089 D3). Canonical on THAT layer, and a
|
|
89
|
+
* wrong-layer paste on a runtime record surface — see the module note.
|
|
90
|
+
*/
|
|
91
|
+
const METADATA_LAYER_ROOT = 'data';
|
|
92
|
+
/**
|
|
93
|
+
* Detect a non-canonical spelling in ONE row predicate, or `null` when the
|
|
94
|
+
* predicate is already canonical (or is not this module's verdict to give).
|
|
95
|
+
*
|
|
96
|
+
* Deliberately conservative in both arms — every condition below can only
|
|
97
|
+
* REMOVE a finding, never invent one, because a false deprecation warning sends
|
|
98
|
+
* an author to rewrite a predicate that was correct:
|
|
99
|
+
*
|
|
100
|
+
* - **Unparseable source** → `null`. Syntax is another gate's verdict.
|
|
101
|
+
* - **Bare shorthand** is reported only when the undeclared identifier is an own
|
|
102
|
+
* key of THIS row. That is what makes it unambiguously the #4075 shorthand
|
|
103
|
+
* rather than a host-scope global this module cannot see (a deployment key
|
|
104
|
+
* that is simply not in `SCOPE_ROOTS` would otherwise read as a bare field).
|
|
105
|
+
* - **`data.*`** is reported only when the caller confirms `data` names the row
|
|
106
|
+
* (`dataNamesRow`). A surface whose `data` is the host scope's own — a
|
|
107
|
+
* `rowless` dialog, a metadata-editing form — is a legitimate `data.*` site
|
|
108
|
+
* and is left alone.
|
|
109
|
+
*
|
|
110
|
+
* Only the FIRST finding is returned, and the bare-shorthand arm is checked
|
|
111
|
+
* first: it is the arm the server refuses outright (`visibility-bare-identifier`
|
|
112
|
+
* is an `error` there, while a mis-layered root is a `warning`), so when a
|
|
113
|
+
* predicate manages both it is the one the author must fix to have anything
|
|
114
|
+
* evaluate at all.
|
|
115
|
+
*
|
|
116
|
+
* @param source The predicate's CEL text. Callers must have already
|
|
117
|
+
* routed legacy `${…}`-dialect strings elsewhere — in that
|
|
118
|
+
* dialect `data.*` is the NORMAL spelling, and reporting it
|
|
119
|
+
* here would be a false positive on every legacy predicate.
|
|
120
|
+
* @param row The row the predicate is bound against.
|
|
121
|
+
* @param dataNamesRow Whether `data` is bound to that same row on this surface.
|
|
122
|
+
*/
|
|
123
|
+
export function detectNonCanonicalRowSpelling(source, row, dataNamesRow) {
|
|
124
|
+
if (typeof source !== 'string' || !source.trim())
|
|
125
|
+
return null;
|
|
126
|
+
const roots = collectCelRootIdentifiers(source);
|
|
127
|
+
// Not parseable through the canonical front end — not our verdict.
|
|
128
|
+
if (!roots || roots.ok !== true)
|
|
129
|
+
return null;
|
|
130
|
+
// (1) Bare shorthand — the arm the server refuses outright.
|
|
131
|
+
const bare = firstUndeclaredReference(source);
|
|
132
|
+
if (typeof bare === 'string' &&
|
|
133
|
+
row != null &&
|
|
134
|
+
typeof row === 'object' &&
|
|
135
|
+
Object.prototype.hasOwnProperty.call(row, bare)) {
|
|
136
|
+
return {
|
|
137
|
+
kind: 'bare-shorthand',
|
|
138
|
+
identifier: bare,
|
|
139
|
+
canonical: `${ROW_PREDICATE_CANONICAL_ROOT}.${bare}`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// (2) Wrong-layer `data.*` root — silent at runtime on the server.
|
|
143
|
+
if (dataNamesRow && Array.isArray(roots.roots) && roots.roots.includes(METADATA_LAYER_ROOT)) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'metadata-layer-root',
|
|
146
|
+
identifier: METADATA_LAYER_ROOT,
|
|
147
|
+
canonical: ROW_PREDICATE_CANONICAL_ROOT,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Dev-mode gate, matching `utils/dashboard-filters.ts` and `actions/actionKeys.ts`
|
|
154
|
+
* — a deprecation warning that floods a production console is a warning that
|
|
155
|
+
* gets muted.
|
|
156
|
+
*/
|
|
157
|
+
const isDev = () => globalThis.process?.env
|
|
158
|
+
?.NODE_ENV !== 'production';
|
|
159
|
+
/**
|
|
160
|
+
* Warn-once memo, keyed by the `(label, predicate source)` pair — the same
|
|
161
|
+
* identity `warnEvalError` uses, and JSON-encoded for the same reason: the
|
|
162
|
+
* separator that boundary once used was a raw U+0000, which made the file
|
|
163
|
+
* carrying it binary to grep (objectstack#5450). Keying on the source alone
|
|
164
|
+
* would report the first surface carrying a shorthand `status` predicate and
|
|
165
|
+
* stay silent about every other one; the label is what sends the author to the
|
|
166
|
+
* right screen.
|
|
167
|
+
*
|
|
168
|
+
* Module scope, not per-call: these predicates are re-evaluated on every row of
|
|
169
|
+
* every render, so per-call state would warn once per frame — the flood the
|
|
170
|
+
* dedupe exists to prevent.
|
|
171
|
+
*/
|
|
172
|
+
const warnedSpellings = new Set();
|
|
173
|
+
/** Reset the row-predicate spelling warn-once memo. Exported for tests. */
|
|
174
|
+
export function resetRowPredicateCanonWarnings() {
|
|
175
|
+
warnedSpellings.clear();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Report a non-canonical row-predicate spelling once, in dev.
|
|
179
|
+
*
|
|
180
|
+
* Phase 1 of the objectui#5330 window: the binding is UNCHANGED and every
|
|
181
|
+
* spelling still resolves — this only says so out loud, so the stored
|
|
182
|
+
* population stops growing and a later survey has something to count. It is a
|
|
183
|
+
* warning and deliberately not a refusal: turning it into one would move the
|
|
184
|
+
* accept/reject set, which this card is explicitly not entitled to do.
|
|
185
|
+
*/
|
|
186
|
+
export function warnNonCanonicalRowSpelling(source, row, dataNamesRow, label) {
|
|
187
|
+
if (!isDev())
|
|
188
|
+
return;
|
|
189
|
+
const finding = detectNonCanonicalRowSpelling(source, row, dataNamesRow);
|
|
190
|
+
if (!finding)
|
|
191
|
+
return;
|
|
192
|
+
const key = JSON.stringify([label ?? '', source, finding.kind]);
|
|
193
|
+
if (warnedSpellings.has(key))
|
|
194
|
+
return;
|
|
195
|
+
warnedSpellings.add(key);
|
|
196
|
+
const where = label ? ` (${label})` : '';
|
|
197
|
+
const detail = finding.kind === 'bare-shorthand'
|
|
198
|
+
? `it references the bare field ${JSON.stringify(finding.identifier)}; ` +
|
|
199
|
+
`the server refuses that spelling outright ("Unknown variable: ${finding.identifier}")`
|
|
200
|
+
: 'it is rooted at `data.`, which is the metadata-editing-form root — on a ' +
|
|
201
|
+
'record surface the server binds no `data` at all, so the predicate is a ' +
|
|
202
|
+
'constant false there rather than an error';
|
|
203
|
+
console.warn(`[object-ui] A row predicate${where} uses a DEPRECATED spelling: ` +
|
|
204
|
+
`${JSON.stringify(source)} — ${detail}. The canon is \`record.*\` ` +
|
|
205
|
+
`(objectui#5330, ruled 2026-08-20): write \`${finding.canonical}\`. ` +
|
|
206
|
+
'This still evaluates here for now; the tolerance retires after a ' +
|
|
207
|
+
'stored-metadata survey.');
|
|
208
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export * from './utils/filter-converter.js';
|
|
|
18
18
|
export * from './utils/managedBy.js';
|
|
19
19
|
export * from './utils/extract-records.js';
|
|
20
20
|
export * from './utils/expand-fields.js';
|
|
21
|
+
export * from './utils/retired-field-types.js';
|
|
22
|
+
export * from './utils/unmaterialized-fields.js';
|
|
21
23
|
export * from './utils/column-identity.js';
|
|
22
24
|
export * from './utils/sort-values.js';
|
|
23
25
|
export * from './utils/sort-query.js';
|
|
@@ -49,6 +51,7 @@ export * from './utils/dashboard-filters.js';
|
|
|
49
51
|
export * from './utils/merge-filters.js';
|
|
50
52
|
export * from './utils/compare-to.js';
|
|
51
53
|
export * from './utils/chart-series.js';
|
|
54
|
+
export * from './utils/chart-presentation.js';
|
|
52
55
|
export * from './utils/number-display.js';
|
|
53
56
|
export * from './utils/dataset-format.js';
|
|
54
57
|
export * from './utils/dataset-pivot.js';
|
|
@@ -58,3 +61,4 @@ export * from './utils/reference-keys.js';
|
|
|
58
61
|
export * from './utils/predicate-record.js';
|
|
59
62
|
export * from './utils/predicate-fields.js';
|
|
60
63
|
export * from './utils/normalize-list-view.js';
|
|
64
|
+
export * from './utils/humanize-label.js';
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,13 @@ export * from './utils/filter-converter.js';
|
|
|
22
22
|
export * from './utils/managedBy.js';
|
|
23
23
|
export * from './utils/extract-records.js';
|
|
24
24
|
export * from './utils/expand-fields.js';
|
|
25
|
+
// The RETIREMENT gate (objectui#4914, maintainer ruling B). Homed here rather
|
|
26
|
+
// than in `@object-ui/fields` because `@object-ui/components` is one of its six
|
|
27
|
+
// consumers and `fields` depends on `components` — see the module's docblock
|
|
28
|
+
// for why a second copy was not an option. `@object-ui/fields` re-exports every
|
|
29
|
+
// name, so its published surface is unchanged.
|
|
30
|
+
export * from './utils/retired-field-types.js';
|
|
31
|
+
export * from './utils/unmaterialized-fields.js';
|
|
25
32
|
export * from './utils/column-identity.js';
|
|
26
33
|
export * from './utils/sort-values.js';
|
|
27
34
|
export * from './utils/sort-query.js';
|
|
@@ -56,6 +63,10 @@ export * from './utils/dashboard-filters.js';
|
|
|
56
63
|
export * from './utils/merge-filters.js';
|
|
57
64
|
export * from './utils/compare-to.js';
|
|
58
65
|
export * from './utils/chart-series.js';
|
|
66
|
+
// The AUTHORED half of a dataset-bound chart (objectui#4229's data/presentation
|
|
67
|
+
// split), shared by the dashboard widget and the report's embedded chart so the
|
|
68
|
+
// same spec keys are lowered identically on both (objectui#4877).
|
|
69
|
+
export * from './utils/chart-presentation.js';
|
|
59
70
|
// The ONE number-display formatter (objectui#4033) — grouping policy, display
|
|
60
71
|
// locale and the percent convention. It lived in `@object-ui/i18n` until
|
|
61
72
|
// objectui#4576; it is pure, and living above `core` was what kept
|
|
@@ -79,3 +90,9 @@ export * from './utils/predicate-record.js';
|
|
|
79
90
|
// which the column-derived `$select` never asked the server for.
|
|
80
91
|
export * from './utils/predicate-fields.js';
|
|
81
92
|
export * from './utils/normalize-list-view.js';
|
|
93
|
+
// The single home for the VALUE fallback prettifier (a stored value becomes a
|
|
94
|
+
// display string when nothing resolves it). `@object-ui/fields` and
|
|
95
|
+
// `@object-ui/plugin-charts` each carried a byte-identical private copy;
|
|
96
|
+
// both now re-export this one (objectui#5444). Its docstring also records why
|
|
97
|
+
// it stays distinct from `humanizeFieldKey`, the KEY fallback.
|
|
98
|
+
export * from './utils/humanize-label.js';
|