@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
|
@@ -44,12 +44,37 @@ export interface RowPredicateOptions {
|
|
|
44
44
|
* row-action `visible`/`disabled`; `false` (no style) for formatting. */
|
|
45
45
|
fallback?: boolean;
|
|
46
46
|
/** Extra top-level scope merged alongside the row — e.g. the global predicate
|
|
47
|
-
* scope (`features` / `user` / `app`) a host shell provides.
|
|
47
|
+
* scope (`features` / `user` / `app`) a host shell provides. The row wins on
|
|
48
|
+
* collision: a `record` or `data` key here never shadows the row. */
|
|
48
49
|
scope?: Record<string, unknown>;
|
|
49
50
|
/** When true, log a one-time warning if a *present* predicate faults. */
|
|
50
51
|
warnOnError?: boolean;
|
|
51
52
|
/** Label used in warning messages (e.g. an action name or rule index). */
|
|
52
53
|
label?: string;
|
|
54
|
+
/**
|
|
55
|
+
* This surface has **no row of its own** — bind NOTHING for the row instead
|
|
56
|
+
* of binding an empty one, so a `record` / `data` the HOST SCOPE carries
|
|
57
|
+
* survives to the predicate (objectui#4640).
|
|
58
|
+
*
|
|
59
|
+
* The default (`false`) is this function's whole subject rule: the row is
|
|
60
|
+
* pinned over the scope, on both dialect paths, so `record` / `data` always
|
|
61
|
+
* name THIS row even when the host scope carries keys of those names
|
|
62
|
+
* (objectui#3796). That is right for every row surface — and exactly wrong
|
|
63
|
+
* for a surface that has no row, because the pin then writes an EMPTY
|
|
64
|
+
* `record` / `data` over the host's real ones and every `record.*` predicate
|
|
65
|
+
* faults with "No such key". "This surface has no row of its own" and "this
|
|
66
|
+
* surface's row is empty" are different facts; only the latter is entitled to
|
|
67
|
+
* shadow the scope. Same distinction, same words, as `usePredicateRecordContext`
|
|
68
|
+
* in `@object-ui/react` (objectui#4075) — which is the BINDING half of this
|
|
69
|
+
* rule for the `useCondition` tier; this is the evaluation-entry half.
|
|
70
|
+
*
|
|
71
|
+
* Set it where the predicate is scoped to a *dialog / shell / page* rather
|
|
72
|
+
* than to a record — `ActionParamDialog`'s param `visible` gates are the
|
|
73
|
+
* first caller (objectui#4640): their scope IS the host predicate scope,
|
|
74
|
+
* `data` and all. `opts.fields` is meaningless here (there is no row to
|
|
75
|
+
* collapse relations on) and is ignored.
|
|
76
|
+
*/
|
|
77
|
+
rowless?: boolean;
|
|
53
78
|
/**
|
|
54
79
|
* The object's field definitions (`objectSchema.fields`). Supplying them lets
|
|
55
80
|
* a relation field be bound as the FOREIGN KEY the server stores, rather than
|
|
@@ -63,9 +88,34 @@ export interface RowPredicateOptions {
|
|
|
63
88
|
* Evaluate a single boolean predicate against a row record on the canonical CEL
|
|
64
89
|
* engine (with a legacy-dialect fallback — see the module note). The row's
|
|
65
90
|
* fields are bound three ways so every authoring convention resolves:
|
|
66
|
-
* `record.status
|
|
67
|
-
*
|
|
68
|
-
*
|
|
91
|
+
* `record.status`, bare `status` (row-action shorthand), and `data.status`.
|
|
92
|
+
*
|
|
93
|
+
* ⚠️ Those three are NOT peers, and this doc comment used to read as though
|
|
94
|
+
* they were. **The canon is `record.*`** (maintainer ruling 2026-08-20 on
|
|
95
|
+
* objectui#5330, option B); the other two are client tolerances in a
|
|
96
|
+
* deprecation window, kept because stored metadata carries them and warned
|
|
97
|
+
* about — from the CEL path below — by `warnNonCanonicalRowSpelling`. The
|
|
98
|
+
* server accepts `record.*` and NOTHING else: measured on
|
|
99
|
+
* `@objectstack/formula@17.1.0`, `buildScope({ record })` mounts exactly
|
|
100
|
+
* `['record']`, so a bare field faults `Unknown variable: status` there and a
|
|
101
|
+
* `data.*` predicate faults `Unknown variable: data`. See
|
|
102
|
+
* {@link ./rowPredicateCanon.ts} for the full measurement, for why `data.*` is
|
|
103
|
+
* the dangerous one (it is silently ACCEPTED by the server's authoring oracle
|
|
104
|
+
* and still binds nothing at runtime), and for why the deprecation is scoped to
|
|
105
|
+
* this runtime layer rather than declared platform-wide (`data` is the
|
|
106
|
+
* canonical root of a metadata-editing form — ADR-0089 D3).
|
|
107
|
+
*
|
|
108
|
+
* ⛔ No spelling is removed from the binding, and none may be before a
|
|
109
|
+
* stored-metadata survey sizes the window — that is part of the same ruling.
|
|
110
|
+
*
|
|
111
|
+
* The optional `scope` (host predicate scope) is bound
|
|
112
|
+
* alongside so `features.*` / `user.*` predicates keep working — but the row is
|
|
113
|
+
* the subject: `record` and `data` always name THIS row, on both dialect paths,
|
|
114
|
+
* even when the host scope carries keys of those names (objectui#3796).
|
|
115
|
+
*
|
|
116
|
+
* A caller with no row at all passes {@link RowPredicateOptions.rowless}, and
|
|
117
|
+
* then nothing is bound over the scope — see that option for why an absent row
|
|
118
|
+
* must not be spelled as an empty one.
|
|
69
119
|
*/
|
|
70
120
|
export declare function evalRowPredicate(pred: FieldRulePredicate | undefined | null, row: Record<string, unknown> | null | undefined, opts?: RowPredicateOptions): boolean;
|
|
71
121
|
/** A conditional-formatting rule in any of the three historical shapes. */
|
|
@@ -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
|
|
@@ -54,27 +55,54 @@ function warnLegacyDialect(source) {
|
|
|
54
55
|
if (warnedLegacy.has(source))
|
|
55
56
|
return;
|
|
56
57
|
warnedLegacy.add(source);
|
|
57
|
-
console.warn('[object-ui] A
|
|
58
|
+
console.warn('[object-ui] A conditional predicate uses the legacy expression dialect ' +
|
|
58
59
|
'and is not portable to the server CEL engine: ' +
|
|
59
60
|
JSON.stringify(source) +
|
|
60
61
|
'. Rewrite it as CEL (use "==" not "===", "x in [..]" for membership, ' +
|
|
61
62
|
'".contains()" not ".includes()", and drop template interpolation).');
|
|
62
63
|
}
|
|
63
64
|
const warnedError = new Set();
|
|
64
|
-
|
|
65
|
+
/**
|
|
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).
|
|
74
|
+
*
|
|
75
|
+
* `reason` is the engine's own description of the failure (`"[runtime] No such
|
|
76
|
+
* key: owner_id"`) — the single most useful line for the author, and precisely
|
|
77
|
+
* what this path used to discard: the fault-probing CEL route silences the
|
|
78
|
+
* canonical helper's warning to avoid double-reporting, so before objectui#3792
|
|
79
|
+
* the more safety-critical the surface, the LESS it said. It is optional
|
|
80
|
+
* because a caller may genuinely have no reason to pass (nothing fabricates
|
|
81
|
+
* one).
|
|
82
|
+
*
|
|
83
|
+
* The wording is deliberately surface-neutral: this function is not
|
|
84
|
+
* list-specific and has not been for a long time — row kebabs, the bulk
|
|
85
|
+
* selection bar, kanban conditional formatting and (since objectui#3521)
|
|
86
|
+
* `page:header` all report through it, and telling a page-header author that
|
|
87
|
+
* "a list conditional predicate" failed sends them to the wrong screen.
|
|
88
|
+
*/
|
|
89
|
+
function warnEvalError(source, label, reason) {
|
|
65
90
|
// One-time-warning identity for the (label, source) pair. JSON-encoded rather
|
|
66
91
|
// than joined on a character the two halves "cannot contain" — that separator
|
|
67
92
|
// was a raw U+0000, which made this whole file binary to grep (objectstack#5450).
|
|
68
93
|
// JSON needs no impossible character, so the boundary is unambiguous for any
|
|
69
94
|
// label and any predicate source, and the literal is plain ASCII.
|
|
95
|
+
// `reason` is deliberately NOT part of the key: one predicate text has one
|
|
96
|
+
// reason, so keying on it could only ever weaken the warn-once contract.
|
|
70
97
|
const key = JSON.stringify([label ?? '', source]);
|
|
71
98
|
if (warnedError.has(key))
|
|
72
99
|
return;
|
|
73
100
|
warnedError.add(key);
|
|
74
|
-
console.warn('[object-ui] A
|
|
101
|
+
console.warn('[object-ui] A conditional predicate failed to evaluate' +
|
|
75
102
|
(label ? ` (${label})` : '') +
|
|
76
103
|
' and was treated as its safe default: ' +
|
|
77
104
|
JSON.stringify(source) +
|
|
105
|
+
(reason ? `. Reason: ${reason}` : '') +
|
|
78
106
|
'. Check the field names and CEL syntax.');
|
|
79
107
|
}
|
|
80
108
|
/**
|
|
@@ -91,20 +119,52 @@ function evalCel(pred, record, scope) {
|
|
|
91
119
|
// verbatim — no second engine — so CEL semantics never drift from B2.
|
|
92
120
|
// `warn: false`: this caller reports the fault itself (the labelled
|
|
93
121
|
// `warnEvalError` in `evalRowPredicate`) — one warning per broken predicate,
|
|
94
|
-
// not two (#5149).
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
// not two (#5149). `onFault` carries the engine's reason across that silence
|
|
123
|
+
// so the labelled report can still name it (objectui#3792); both probes fault
|
|
124
|
+
// identically, so the first reason is the reason.
|
|
125
|
+
let reason;
|
|
126
|
+
const onFault = (r) => {
|
|
127
|
+
reason ?? (reason = r);
|
|
128
|
+
};
|
|
129
|
+
const diagnostic = { warn: false, onFault };
|
|
130
|
+
const asTrue = evalFieldPredicate(pred, record, true, undefined, scope, diagnostic);
|
|
131
|
+
const asFalse = evalFieldPredicate(pred, record, false, undefined, scope, diagnostic);
|
|
97
132
|
if (asTrue !== asFalse)
|
|
98
|
-
return { ok: false, value: false };
|
|
133
|
+
return { ok: false, value: false, reason };
|
|
99
134
|
return { ok: true, value: asTrue };
|
|
100
135
|
}
|
|
101
136
|
/**
|
|
102
137
|
* Evaluate a single boolean predicate against a row record on the canonical CEL
|
|
103
138
|
* engine (with a legacy-dialect fallback — see the module note). The row's
|
|
104
139
|
* fields are bound three ways so every authoring convention resolves:
|
|
105
|
-
* `record.status
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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
|
|
161
|
+
* alongside so `features.*` / `user.*` predicates keep working — but the row is
|
|
162
|
+
* the subject: `record` and `data` always name THIS row, on both dialect paths,
|
|
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.
|
|
108
168
|
*/
|
|
109
169
|
export function evalRowPredicate(pred, row, opts = {}) {
|
|
110
170
|
const fallback = opts.fallback ?? false;
|
|
@@ -116,24 +176,70 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
116
176
|
// Relations collapse to the foreign key the server stores, so `record.owner`
|
|
117
177
|
// means one thing whether or not this surface expanded the column
|
|
118
178
|
// (objectui#3501). Returned by reference when there is nothing to collapse.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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);
|
|
183
|
+
// Bare fields + `data.*` + `record.*` + the host scope, all top-level.
|
|
184
|
+
//
|
|
185
|
+
// `data` AND `record` are pinned AFTER the spread, so a host scope that
|
|
186
|
+
// happens to carry either key is background and the ROW stays the subject of
|
|
187
|
+
// this function — on BOTH dialect paths. `data` always had that protection;
|
|
188
|
+
// `record` did not, and relied instead on each engine's own binding, which
|
|
189
|
+
// disagreed: the legacy evaluator re-pinned `record` (row won) while the CEL
|
|
190
|
+
// engine takes `extra` over its `record` binding (host scope won). Same
|
|
191
|
+
// function, same predicate text, opposite subjects — decided by whether the
|
|
192
|
+
// string happens to contain `===`/`${…}`, which no author is choosing
|
|
193
|
+
// deliberately (objectui#3796). Pinning here fixes both paths at the merge,
|
|
194
|
+
// independently of either engine's precedence.
|
|
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)');
|
|
123
217
|
// Legacy-dialect *strings* route to the legacy engine (back-compat). The
|
|
124
218
|
// `{ dialect: 'cel', source }` envelope is never routed here — it is CEL.
|
|
125
219
|
if (source !== undefined && isLegacyDialectSource(source)) {
|
|
126
220
|
warnLegacyDialect(source);
|
|
127
221
|
try {
|
|
128
|
-
const evaluator = new ExpressionEvaluator(
|
|
222
|
+
const evaluator = new ExpressionEvaluator(scope);
|
|
129
223
|
return evaluator.evaluateCondition(source, { throwOnError: true });
|
|
130
224
|
}
|
|
131
|
-
catch {
|
|
132
|
-
|
|
133
|
-
|
|
225
|
+
catch (err) {
|
|
226
|
+
// The legacy engine reports by throwing, so its message IS the reason —
|
|
227
|
+
// the fail-closed report carries it for the same reason the CEL path does.
|
|
228
|
+
if (opts.warnOnError) {
|
|
229
|
+
warnEvalError(source, opts.label, `[legacy] ${err instanceof Error ? err.message : String(err)}`);
|
|
230
|
+
}
|
|
134
231
|
return fallback;
|
|
135
232
|
}
|
|
136
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
|
+
}
|
|
137
243
|
// CEL path. The fault-aware `evalCel` costs two evaluations (to tell a fault
|
|
138
244
|
// from a genuine `false`), so only pay it when a caller wants the labelled
|
|
139
245
|
// fail-closed warning — the formatting hot path takes the single-eval fast
|
|
@@ -145,7 +251,7 @@ export function evalRowPredicate(pred, row, opts = {}) {
|
|
|
145
251
|
}
|
|
146
252
|
const verdict = evalCel(pred, rowObj, scope);
|
|
147
253
|
if (!verdict.ok) {
|
|
148
|
-
warnEvalError(
|
|
254
|
+
warnEvalError(predicateText, opts.label, verdict.reason);
|
|
149
255
|
return fallback;
|
|
150
256
|
}
|
|
151
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
|
+
]);
|
|
@@ -44,7 +44,9 @@ export type EvaluatorPredicateInput = string | boolean | {
|
|
|
44
44
|
*
|
|
45
45
|
* Accepts:
|
|
46
46
|
* - `boolean` → returned as-is (predicate evaluation short-circuits).
|
|
47
|
-
* - `string` → wrapped as `${string}` (legacy DX
|
|
47
|
+
* - `string` → a BARE expression is wrapped as `${string}` (legacy DX
|
|
48
|
+
* shorthand); a string that is already a `${…}` template is ALREADY in the
|
|
49
|
+
* normalized shape and is returned untouched (objectui#3871, below).
|
|
48
50
|
* - `Expression` envelope `{ dialect, source }` (the normalized form
|
|
49
51
|
* `@objectstack/spec`'s `ExpressionInputSchema` produces for every
|
|
50
52
|
* authored predicate, including bare strings) → a `cel` dialect keeps
|
|
@@ -73,8 +75,54 @@ export type EvaluatorPredicateInput = string | boolean | {
|
|
|
73
75
|
* site. `@object-ui/react`'s `toPredicateInput` is a re-export of this function
|
|
74
76
|
* (since #3367; it used to be an independent twin held in step by a 14-shape
|
|
75
77
|
* normalization parity table, which is exactly the arrangement the paragraph
|
|
76
|
-
* above describes the failure mode of).
|
|
77
|
-
*
|
|
78
|
+
* above describes the failure mode of).
|
|
79
|
+
*
|
|
80
|
+
* ## Why an already-`${…}` string is NOT wrapped again (objectui#3871)
|
|
81
|
+
*
|
|
82
|
+
* `${…}` is one of the spellings this repo documents for a predicate
|
|
83
|
+
* (AGENTS.md §4: `hidden?: string; // expression: "${data.role != 'admin'}"`)
|
|
84
|
+
* AND one of the shapes {@link EvaluatorPredicateInput} lists as a normalized
|
|
85
|
+
* OUTPUT. Wrapping unconditionally therefore corrupted every value that was
|
|
86
|
+
* already normalized: `'${x}'` became `'${${x}}'`, which no longer matches
|
|
87
|
+
* `ExpressionEvaluator`'s single-template fast path (`/^\$\{([^}]+)\}$/` —
|
|
88
|
+
* `[^}]+` cannot cross the inner `}`), so it fell to the global replace, whose
|
|
89
|
+
* inner expression `'${x'` does not parse. The verdict then stopped depending
|
|
90
|
+
* on the predicate at all, in a direction set by the CALLER's error policy:
|
|
91
|
+
*
|
|
92
|
+
* - fail-soft callers (every `disabled` / `enabled` leg, and the `visible`
|
|
93
|
+
* legs of `action:group` / `action:icon` / `RelatedList`) got the original
|
|
94
|
+
* non-empty string back from the evaluator's `catch { return match }`, so
|
|
95
|
+
* `Boolean(…)` was a constant `true` — `disabled` greyed out forever,
|
|
96
|
+
* `visible` shown forever, `enabled` never disabling.
|
|
97
|
+
* - fail-closed callers (`throwOnError: true` — `action:button` / `action:bar`
|
|
98
|
+
* / `action:menu` / `DeclaredActionsBar` `visible`, and
|
|
99
|
+
* `ActionEngine.getActionsForLocation`) got a THROW, so the constant was
|
|
100
|
+
* the other one: the action was hidden forever, including when its
|
|
101
|
+
* predicate held.
|
|
102
|
+
*
|
|
103
|
+
* Either way the author's expression was never consulted. The guard is
|
|
104
|
+
* `includes('${')` — "does this string already carry template syntax", the
|
|
105
|
+
* same question `evaluateCondition` itself asks before choosing the template
|
|
106
|
+
* path — not an anchored full match: a multi-part string like `'a ${x} b'` is
|
|
107
|
+
* not a predicate in any spelling, and wrapping it produced the same constant
|
|
108
|
+
* verdict, so the narrower guard would buy nothing and would still corrupt a
|
|
109
|
+
* value the evaluator can read.
|
|
110
|
+
*
|
|
111
|
+
* The same guard covers the envelope branch below, where the unwrapped
|
|
112
|
+
* `source` reaches the identical wrap one line later. It is one defect with two
|
|
113
|
+
* spellings, and `{ dialect: 'template', source: '${x}' }` is the shape a
|
|
114
|
+
* producer that compiles an authored template emits. A bare `source` (what
|
|
115
|
+
* `@objectstack/spec`'s `ExpressionInputSchema` produces today, pinned in
|
|
116
|
+
* `packages/react/src/hooks/__tests__/useExpression.test.ts`) is unaffected.
|
|
117
|
+
*
|
|
118
|
+
* What this does NOT do is decide whether `${…}` SHOULD be authorable on an
|
|
119
|
+
* action predicate: `@objectstack/spec`'s `PredicateInput` models a bare string
|
|
120
|
+
* and a dialect envelope, not the template spelling. Rejecting it loudly at
|
|
121
|
+
* publish time is a spec-side ruling (deferred to the objectstack seat by the
|
|
122
|
+
* objectui#3871 dispatch); until then this normalizer must not silently invent
|
|
123
|
+
* a verdict for a spelling both the docs and this file's own output type bless.
|
|
124
|
+
*
|
|
125
|
+
* What pins the single implementation is the identity assertion in
|
|
78
126
|
* `packages/react/src/hooks/__tests__/actionPredicate.parity.test.tsx` — the
|
|
79
127
|
* react export must BE this function object — alongside the engine-path vs
|
|
80
128
|
* renderer-path verdict parity suite in the same file, which is a separate
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
* This source code is licensed under the MIT license found in the
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* Wrap a BARE expression as a `${…}` template, and leave a string that already
|
|
10
|
+
* carries template syntax alone (objectui#3871 — rationale in
|
|
11
|
+
* {@link toPredicateInput}'s doc below). One rule, so the two string spellings
|
|
12
|
+
* inside the normalizer cannot drift apart.
|
|
13
|
+
*/
|
|
14
|
+
function wrapIfBare(expression) {
|
|
15
|
+
return expression.includes('${') ? expression : `\${${expression}}`;
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
|
|
10
19
|
* `hidden`) into the form {@link ExpressionEvaluator.evaluateCondition}
|
|
@@ -12,7 +21,9 @@
|
|
|
12
21
|
*
|
|
13
22
|
* Accepts:
|
|
14
23
|
* - `boolean` → returned as-is (predicate evaluation short-circuits).
|
|
15
|
-
* - `string` → wrapped as `${string}` (legacy DX
|
|
24
|
+
* - `string` → a BARE expression is wrapped as `${string}` (legacy DX
|
|
25
|
+
* shorthand); a string that is already a `${…}` template is ALREADY in the
|
|
26
|
+
* normalized shape and is returned untouched (objectui#3871, below).
|
|
16
27
|
* - `Expression` envelope `{ dialect, source }` (the normalized form
|
|
17
28
|
* `@objectstack/spec`'s `ExpressionInputSchema` produces for every
|
|
18
29
|
* authored predicate, including bare strings) → a `cel` dialect keeps
|
|
@@ -41,8 +52,54 @@
|
|
|
41
52
|
* site. `@object-ui/react`'s `toPredicateInput` is a re-export of this function
|
|
42
53
|
* (since #3367; it used to be an independent twin held in step by a 14-shape
|
|
43
54
|
* normalization parity table, which is exactly the arrangement the paragraph
|
|
44
|
-
* above describes the failure mode of).
|
|
45
|
-
*
|
|
55
|
+
* above describes the failure mode of).
|
|
56
|
+
*
|
|
57
|
+
* ## Why an already-`${…}` string is NOT wrapped again (objectui#3871)
|
|
58
|
+
*
|
|
59
|
+
* `${…}` is one of the spellings this repo documents for a predicate
|
|
60
|
+
* (AGENTS.md §4: `hidden?: string; // expression: "${data.role != 'admin'}"`)
|
|
61
|
+
* AND one of the shapes {@link EvaluatorPredicateInput} lists as a normalized
|
|
62
|
+
* OUTPUT. Wrapping unconditionally therefore corrupted every value that was
|
|
63
|
+
* already normalized: `'${x}'` became `'${${x}}'`, which no longer matches
|
|
64
|
+
* `ExpressionEvaluator`'s single-template fast path (`/^\$\{([^}]+)\}$/` —
|
|
65
|
+
* `[^}]+` cannot cross the inner `}`), so it fell to the global replace, whose
|
|
66
|
+
* inner expression `'${x'` does not parse. The verdict then stopped depending
|
|
67
|
+
* on the predicate at all, in a direction set by the CALLER's error policy:
|
|
68
|
+
*
|
|
69
|
+
* - fail-soft callers (every `disabled` / `enabled` leg, and the `visible`
|
|
70
|
+
* legs of `action:group` / `action:icon` / `RelatedList`) got the original
|
|
71
|
+
* non-empty string back from the evaluator's `catch { return match }`, so
|
|
72
|
+
* `Boolean(…)` was a constant `true` — `disabled` greyed out forever,
|
|
73
|
+
* `visible` shown forever, `enabled` never disabling.
|
|
74
|
+
* - fail-closed callers (`throwOnError: true` — `action:button` / `action:bar`
|
|
75
|
+
* / `action:menu` / `DeclaredActionsBar` `visible`, and
|
|
76
|
+
* `ActionEngine.getActionsForLocation`) got a THROW, so the constant was
|
|
77
|
+
* the other one: the action was hidden forever, including when its
|
|
78
|
+
* predicate held.
|
|
79
|
+
*
|
|
80
|
+
* Either way the author's expression was never consulted. The guard is
|
|
81
|
+
* `includes('${')` — "does this string already carry template syntax", the
|
|
82
|
+
* same question `evaluateCondition` itself asks before choosing the template
|
|
83
|
+
* path — not an anchored full match: a multi-part string like `'a ${x} b'` is
|
|
84
|
+
* not a predicate in any spelling, and wrapping it produced the same constant
|
|
85
|
+
* verdict, so the narrower guard would buy nothing and would still corrupt a
|
|
86
|
+
* value the evaluator can read.
|
|
87
|
+
*
|
|
88
|
+
* The same guard covers the envelope branch below, where the unwrapped
|
|
89
|
+
* `source` reaches the identical wrap one line later. It is one defect with two
|
|
90
|
+
* spellings, and `{ dialect: 'template', source: '${x}' }` is the shape a
|
|
91
|
+
* producer that compiles an authored template emits. A bare `source` (what
|
|
92
|
+
* `@objectstack/spec`'s `ExpressionInputSchema` produces today, pinned in
|
|
93
|
+
* `packages/react/src/hooks/__tests__/useExpression.test.ts`) is unaffected.
|
|
94
|
+
*
|
|
95
|
+
* What this does NOT do is decide whether `${…}` SHOULD be authorable on an
|
|
96
|
+
* action predicate: `@objectstack/spec`'s `PredicateInput` models a bare string
|
|
97
|
+
* and a dialect envelope, not the template spelling. Rejecting it loudly at
|
|
98
|
+
* publish time is a spec-side ruling (deferred to the objectstack seat by the
|
|
99
|
+
* objectui#3871 dispatch); until then this normalizer must not silently invent
|
|
100
|
+
* a verdict for a spelling both the docs and this file's own output type bless.
|
|
101
|
+
*
|
|
102
|
+
* What pins the single implementation is the identity assertion in
|
|
46
103
|
* `packages/react/src/hooks/__tests__/actionPredicate.parity.test.tsx` — the
|
|
47
104
|
* react export must BE this function object — alongside the engine-path vs
|
|
48
105
|
* renderer-path verdict parity suite in the same file, which is a separate
|
|
@@ -55,7 +112,7 @@ export function toPredicateInput(value) {
|
|
|
55
112
|
if (typeof value === 'boolean')
|
|
56
113
|
return value;
|
|
57
114
|
if (typeof value === 'string')
|
|
58
|
-
return
|
|
115
|
+
return wrapIfBare(value);
|
|
59
116
|
if (typeof value === 'object' && typeof value.source === 'string') {
|
|
60
117
|
const src = value.source;
|
|
61
118
|
if (!src)
|
|
@@ -66,7 +123,7 @@ export function toPredicateInput(value) {
|
|
|
66
123
|
if (value.dialect === 'cel')
|
|
67
124
|
return { dialect: 'cel', source: src };
|
|
68
125
|
// Every other dialect (template / unset) keeps the legacy `${…}` behavior.
|
|
69
|
-
return
|
|
126
|
+
return wrapIfBare(src);
|
|
70
127
|
}
|
|
71
128
|
return undefined;
|
|
72
129
|
}
|
|
@@ -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;
|