@object-ui/core 17.5.0 → 17.7.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 +7404 -0
- package/README.md +127 -26
- package/dist/actions/ActionEngine.d.ts +5 -3
- package/dist/actions/ActionEngine.js +14 -10
- package/dist/actions/ActionRunner.d.ts +363 -30
- package/dist/actions/ActionRunner.js +544 -58
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +75 -13
- package/dist/actions/actionKeys.js +131 -16
- package/dist/actions/actionResponse.js +3 -3
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -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/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
- package/dist/evaluator/ExpressionEvaluator.js +226 -13
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +118 -17
- package/dist/evaluator/fieldRules.d.ts +107 -4
- package/dist/evaluator/fieldRules.js +231 -35
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +125 -9
- package/dist/evaluator/listConditional.js +167 -26
- package/dist/evaluator/optionRules.d.ts +126 -0
- package/dist/evaluator/optionRules.js +141 -4
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
- package/dist/evaluator/rowPredicateCanon.js +158 -0
- package/dist/evaluator/subjectPermissions.d.ts +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +97 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +397 -48
- package/dist/registry/Registry.js +263 -11
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- 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-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +188 -0
- package/dist/utils/chart-presentation.js +345 -0
- package/dist/utils/chart-series.d.ts +271 -31
- package/dist/utils/chart-series.js +478 -55
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +407 -52
- package/dist/utils/date-display.d.ts +354 -0
- package/dist/utils/date-display.js +463 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/debug.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +171 -11
- package/dist/utils/expand-fields.js +171 -11
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -34
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +30 -5
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +131 -1
- package/dist/utils/normalize-list-view.js +248 -6
- package/dist/utils/number-display.d.ts +70 -17
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +62 -4
- package/dist/utils/predicate-fields.js +169 -4
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- 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 +128 -26
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +9 -4
- package/dist/validation/index.js +9 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +305 -82
- 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.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -5
- package/package.json +8 -9
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- package/dist/validation/validation-engine.js +0 -434
|
@@ -6,30 +6,44 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
import { formatDisplayNumber } from './number-display.js';
|
|
9
|
+
import { formatDate, formatDateTime, formatRelativeDate } from './date-display.js';
|
|
10
|
+
import { resolveMeasureLabel } from './chart-series.js';
|
|
11
|
+
import { composeDrillFilter } from './drill-down.js';
|
|
9
12
|
/**
|
|
10
|
-
* Scale a stored
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* `1
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
13
|
+
* Scale a stored percentage to its DISPLAY magnitude (percentage points), at
|
|
14
|
+
* the storage the caller STATES.
|
|
15
|
+
*
|
|
16
|
+
* `percentScale` is the spec's `PercentScale`: `fraction` is a 0–1 ratio
|
|
17
|
+
* (`0.75` ⇒ 75, `1` ⇒ 100), `whole` is already in percentage points (`75` ⇒
|
|
18
|
+
* 75, `1` ⇒ 1). A caller holding a field definition reads it with the spec's
|
|
19
|
+
* `percentScaleOf` (`@objectstack/spec/data`): a `percent` field stores a
|
|
20
|
+
* fraction unless it declares a `max` above 1. A caller with no field (a
|
|
21
|
+
* computed ratio, a server measure column) states the storage it knows: a
|
|
22
|
+
* `derived: { op: 'ratio' }` measure is a fraction by definition, and the
|
|
23
|
+
* server annotates a measure column with its `percentScale`.
|
|
24
|
+
*
|
|
25
|
+
* ⛔ No storage is inferred from the VALUE any more (objectui#11475). This
|
|
26
|
+
* function used to be `value > -1 && value < 1 ? value * 100 : value`, a guess
|
|
27
|
+
* from the magnitude that read neither the field nor its `max`. A fraction-
|
|
28
|
+
* stored `1` (100%) therefore read `1%` in the list cell while the read-only
|
|
29
|
+
* form, which reads the declaration, read `100%`; a whole-stored `0.5` read
|
|
30
|
+
* `50%`. objectui#3136 was the same guess on a dashboard measure. The
|
|
31
|
+
* parameter is REQUIRED, so every caller says which storage it holds, and a
|
|
32
|
+
* value outside the union throws rather than falling to a branch silently.
|
|
33
|
+
*
|
|
34
|
+
* ⭐ The contract a third percent surface still owes is BOTH halves: this
|
|
35
|
+
* scaling, at the stored scale, AND the locale's percent convention
|
|
36
|
+
* (`style: 'percentPoints'`, see {@link formatMeasure} and `formatPercent` in
|
|
37
|
+
* `@object-ui/fields`). Between objectui#4553 and objectui#4576 the two ends
|
|
38
|
+
* shared the scaling and not the convention, so a German session read
|
|
39
|
+
* `1.234,5 %` in a list cell and `1.234,5%` in a dashboard measure.
|
|
30
40
|
*/
|
|
31
|
-
export function percentDisplayValue(value) {
|
|
32
|
-
|
|
41
|
+
export function percentDisplayValue(value, percentScale) {
|
|
42
|
+
if (percentScale === 'fraction')
|
|
43
|
+
return value * 100;
|
|
44
|
+
if (percentScale === 'whole')
|
|
45
|
+
return value;
|
|
46
|
+
throw new TypeError(`percentDisplayValue: percentScale must be 'fraction' or 'whole' (the spec's PercentScale), got ${String(percentScale)}`);
|
|
33
47
|
}
|
|
34
48
|
/**
|
|
35
49
|
* Format one number in the DISPLAY locale, surviving a malformed locale tag.
|
|
@@ -65,6 +79,137 @@ export function percentDisplayValue(value) {
|
|
|
65
79
|
function formatNumberInLocale(value, locale, options) {
|
|
66
80
|
return formatDisplayNumber(value, { ...options, locale });
|
|
67
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* ISO calendar date with NO time part — `2026-07-04`.
|
|
84
|
+
*
|
|
85
|
+
* Deliberately anchored at both ends and deliberately narrower than
|
|
86
|
+
* `Date.parse`, which also accepts locale prose (`March 5, 2026`), bare years
|
|
87
|
+
* (`2026`) and — the one that matters here — plain NUMERALS. A measure value
|
|
88
|
+
* is untyped by the time it reaches {@link formatMeasure}, so the shape test
|
|
89
|
+
* IS the type test, and a loose one would capture the numeric strings and
|
|
90
|
+
* counts this formatter must leave exactly as they are.
|
|
91
|
+
*/
|
|
92
|
+
const ISO_DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
93
|
+
/**
|
|
94
|
+
* ISO date carrying a time part — `2026-07-04T07:00:00.000Z`, or the same with
|
|
95
|
+
* a space separator, with or without seconds/offset. Matched only as far as
|
|
96
|
+
* `HH:mm`; `Date.parse` decides the rest. What it rejects is an out-of-range
|
|
97
|
+
* MONTH or CLOCK reading (`2026-13-01T09:30`, `2024-07-04T99:99`), which this
|
|
98
|
+
* pattern admits — such a value falls through untouched.
|
|
99
|
+
*
|
|
100
|
+
* ⛔ A DAY that overflows its month does NOT fall through. `2026-02-30T09:30`
|
|
101
|
+
* parses and reaches the shared date path, which refuses it with the dash
|
|
102
|
+
* `formatDateTime` renders for any unparsable value (objectui#10301, the
|
|
103
|
+
* date-time half of objectui#10026). The refusal is that path's, not this
|
|
104
|
+
* pattern's. See the note inside {@link formatMeasureDate}.
|
|
105
|
+
*/
|
|
106
|
+
const ISO_DATETIME_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/;
|
|
107
|
+
/**
|
|
108
|
+
* Render a date-shaped measure value through the display path a list cell
|
|
109
|
+
* already uses for that field, or return `undefined` to say "not a date" and
|
|
110
|
+
* leave the caller's own fallthrough in charge.
|
|
111
|
+
*
|
|
112
|
+
* ── Why this is a ROUTE and not a formatter (objectui#7178) ──
|
|
113
|
+
* Every line of date rendering below happens in `./date-display.ts`, which is
|
|
114
|
+
* where `@object-ui/fields`' `formatDate` / `formatDateTime` now live and what
|
|
115
|
+
* `DateCellRenderer`, `ObjectGrid`'s date cells and `ObjectGantt`'s tooltips
|
|
116
|
+
* call. Writing the date convention HERE instead is the mistake this file's
|
|
117
|
+
* own header is about: objectui#4576, where a percent convention had one copy
|
|
118
|
+
* in a list cell and another in this function, both correct, and a German
|
|
119
|
+
* session read `1.234,5 %` in the cell beside `1.234,5%` in the measure. One
|
|
120
|
+
* date convention, one home, nothing to drift.
|
|
121
|
+
*
|
|
122
|
+
* The date/datetime split mirrors `ObjectGantt.tsx`'s sniffed-ISO dispatch
|
|
123
|
+
* verbatim, because that site is already the repo's answer to this exact
|
|
124
|
+
* situation: an ISO string arriving with NO field type attached. A measure is
|
|
125
|
+
* that situation by construction — the analytics result carries a column, not
|
|
126
|
+
* a field definition — so the absolute locale form is taken rather than
|
|
127
|
+
* `DateCellRenderer`'s `|| 'relative'` default, which is keyed on knowing the
|
|
128
|
+
* value is a `date` FIELD in a row.
|
|
129
|
+
*
|
|
130
|
+
* ── `format` is honoured on BOTH arms, by different means (objectui#8352) ──
|
|
131
|
+
* The accepted vocabulary is `'short'` and `'relative'` on EITHER arm, and
|
|
132
|
+
* every other string — including `'compact'`, including a date PATTERN such as
|
|
133
|
+
* `'YYYY-MM-DD'` — falls to that arm's default face. The two arms reach that
|
|
134
|
+
* one vocabulary differently, and the difference is the whole of #8352:
|
|
135
|
+
*
|
|
136
|
+
* - The DATE arm threads `format` onward, into `formatDate`'s positional
|
|
137
|
+
* STYLE parameter. That is the same mapping `DateCellRenderer` makes from
|
|
138
|
+
* `field.format` and the same one `plugin-dashboard`'s `recordFields`
|
|
139
|
+
* makes for a date-shaped format.
|
|
140
|
+
* - The DATETIME arm SELECTS a formatter above, because it has nothing to
|
|
141
|
+
* thread into: `formatDateTime(value, options?)` has no style parameter,
|
|
142
|
+
* so before #8352 this arm could not honour `format` even in principle —
|
|
143
|
+
* it dropped it, and `format: 'relative'` on a `Field.datetime` measure
|
|
144
|
+
* rendered the absolute face with no diagnostic.
|
|
145
|
+
*
|
|
146
|
+
* ⚠️ Threading `format` into `formatDateTime`'s `options.style` is NOT the fix
|
|
147
|
+
* and was measured before this one was written: that key's vocabulary is
|
|
148
|
+
* `'compact'` alone, so the pass-through would honour the one word the date
|
|
149
|
+
* arm does NOT honour while still ignoring both words it does — the defect
|
|
150
|
+
* inverted, not closed. Widening the signature was refused separately: it is a
|
|
151
|
+
* published one (see {@link formatDateTime}'s note on why the positional shape
|
|
152
|
+
* was refused there), and the parity #8352 asks for is reachable from the call
|
|
153
|
+
* site without touching it.
|
|
154
|
+
*
|
|
155
|
+
* Which FACE each honoured word selects is per-arm, because the two arms
|
|
156
|
+
* render different types and a datetime's own resolution is part of its datum:
|
|
157
|
+
*
|
|
158
|
+
* - `'relative'` → `formatRelativeDate` on both arms, the same function, so
|
|
159
|
+
* an in-window value reads `In 2 days` in either column. Its ±7-day
|
|
160
|
+
* fallback is inherited rather than re-decided here — an out-of-window
|
|
161
|
+
* value renders the absolute DATE face on both arms. Re-deciding that
|
|
162
|
+
* window at this call site would put a second copy of the convention in
|
|
163
|
+
* this file, which is objectui#4576 exactly.
|
|
164
|
+
* - `'short'` → the dense narrow-card face of the arm's own type:
|
|
165
|
+
* `formatDate`'s `'short'` (`Sep 11, '26`) for a date, `formatDateTime`'s
|
|
166
|
+
* `'compact'` (`9/11/2026 9:30 am`) for a datetime. The datetime face
|
|
167
|
+
* keeps the time, and it is byte-identical to what every `datetime` CELL
|
|
168
|
+
* paints, so a measure tile and a grid cell showing the same instant
|
|
169
|
+
* agree — objectui#4576's rule again, in the other direction.
|
|
170
|
+
*
|
|
171
|
+
* See the `format` note on {@link formatMeasure}.
|
|
172
|
+
*/
|
|
173
|
+
function formatMeasureDate(v, format, locale) {
|
|
174
|
+
if (typeof v !== 'string')
|
|
175
|
+
return undefined;
|
|
176
|
+
// `Date.parse` guards both arms, and what it rejects is an out-of-range
|
|
177
|
+
// MONTH (`2026-13-01`) — which both patterns above admit — ⛔ NOT an
|
|
178
|
+
// out-of-range DAY. ECMAScript's Date Time String Format accepts `DD` in
|
|
179
|
+
// `01`-`31` syntactically and `MakeDay` rolls the surplus into the next
|
|
180
|
+
// month, so a well-shaped impossible date (`2026-02-30`) is PARSEABLE on
|
|
181
|
+
// both arms: it does not fall through to `String(v)`, it reaches the shared
|
|
182
|
+
// date path. This paragraph once asserted the opposite — that such a value
|
|
183
|
+
// keeps falling through — until objectui#8263 measured it.
|
|
184
|
+
//
|
|
185
|
+
// ⭐ What that path answers is ITS decision, never this guard's. Since
|
|
186
|
+
// objectui#10026 it refuses a date-only nonexistent day with the dash it
|
|
187
|
+
// renders for any unparsable value, so the measure shows that same dash —
|
|
188
|
+
// agreeing with the list cell ON THE REFUSAL. This guard must not refuse
|
|
189
|
+
// on its own (by letting the value fall through to `String(v)`, say): the
|
|
190
|
+
// measure would then disagree with the list cell, the objectui#4576 split.
|
|
191
|
+
// `agrees with the list cell on an impossible calendar day — on the refusal`
|
|
192
|
+
// in this file's co-located date suite pins it; read that test for the
|
|
193
|
+
// full argument.
|
|
194
|
+
if (ISO_DATE_ONLY_RE.test(v)) {
|
|
195
|
+
return Number.isNaN(Date.parse(v)) ? undefined : formatDate(v, format, { locale });
|
|
196
|
+
}
|
|
197
|
+
if (ISO_DATETIME_RE.test(v)) {
|
|
198
|
+
if (Number.isNaN(Date.parse(v)))
|
|
199
|
+
return undefined;
|
|
200
|
+
// The datetime arm honours the SAME two words the date arm does, by
|
|
201
|
+
// choosing a formatter here (objectui#8352). It cannot thread `format`
|
|
202
|
+
// onward the way the date arm does, because `formatDateTime(value,
|
|
203
|
+
// options?)` takes no style parameter — see this function's docblock for
|
|
204
|
+
// why that is answered HERE and not by widening a published signature.
|
|
205
|
+
if (format === 'relative')
|
|
206
|
+
return formatRelativeDate(v, { locale });
|
|
207
|
+
if (format === 'short')
|
|
208
|
+
return formatDateTime(v, { locale, style: 'compact' });
|
|
209
|
+
return formatDateTime(v, { locale });
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
68
213
|
/**
|
|
69
214
|
* Format a MEASURE value. Currency comes from the field's declared `currency`
|
|
70
215
|
* (locale-correct symbol via `Intl`), NOT from a "$" baked into the format
|
|
@@ -102,14 +247,70 @@ function formatNumberInLocale(value, locale, options) {
|
|
|
102
247
|
* is pure, so it moved DOWN into this package (`./number-display.ts`) and
|
|
103
248
|
* `@object-ui/i18n` re-exports it. The duplicate implementation this file used
|
|
104
249
|
* to carry is gone, and with it the drift that duplicate produced — see the
|
|
105
|
-
* percent note in the body, and
|
|
106
|
-
*
|
|
250
|
+
* percent note in the body, and the both-halves contract
|
|
251
|
+
* {@link percentDisplayValue}'s doc comment states.
|
|
252
|
+
*
|
|
253
|
+
* ── Date-valued measures (objectui#7178) ──
|
|
254
|
+
* `min` / `max` over a date or datetime field is a legitimate measure, and it
|
|
255
|
+
* used to render its stored value verbatim: the guard above returned
|
|
256
|
+
* `String(v)` for anything non-numeric BEFORE `format` was read, so a KPI tile
|
|
257
|
+
* showed `2026-07-04T07:00:00.000Z` in `text-2xl` and wrapped it over two
|
|
258
|
+
* lines. A date-shaped value now routes through {@link formatMeasureDate} to
|
|
259
|
+
* the display path list cells use; nothing about the numeric path moved.
|
|
260
|
+
*
|
|
261
|
+
* ⚠️ What `format` can and cannot say for those values, measured rather than
|
|
262
|
+
* assumed, and stated PER ARM — a date-shaped measure takes one of two arms in
|
|
263
|
+
* {@link formatMeasureDate}, and this paragraph used to describe only one of
|
|
264
|
+
* them while reading as though it described both. That cost a card: an
|
|
265
|
+
* objectui triage pass read the undifferentiated claim, concluded the defect
|
|
266
|
+
* was already fixed, and downgraded objectui#8352, which a driven browser run
|
|
267
|
+
* then refuted. The two arms agree TODAY, but only because #8352 made them; a
|
|
268
|
+
* universal sentence here is what hid the disagreement before, so it is not
|
|
269
|
+
* coming back even now that it would be true.
|
|
270
|
+
*
|
|
271
|
+
* The shared date path takes a named STYLE, not a date pattern. On BOTH arms
|
|
272
|
+
* the honoured vocabulary is `'short'` and `'relative'`:
|
|
273
|
+
*
|
|
274
|
+
* - `Field.date` (`2026-09-08`) — honoured by threading `format` into
|
|
275
|
+
* `formatDate`'s style parameter. The same words `DateCellRenderer`
|
|
276
|
+
* honours from `field.format` for the same field.
|
|
277
|
+
* - `Field.datetime` (`2026-09-01T00:00:00.000Z`) — honoured by SELECTING a
|
|
278
|
+
* formatter, because `formatDateTime` has no style parameter to thread
|
|
279
|
+
* into (objectui#8352). Before that card this arm honoured NOTHING: it
|
|
280
|
+
* dropped `format` entirely, so `'relative'` rendered the absolute face.
|
|
281
|
+
*
|
|
282
|
+
* Every other string falls to that arm's default face — a PATTERN like
|
|
283
|
+
* `format: 'YYYY-MM-DD'` is accepted by the schema, reaches this function, and
|
|
284
|
+
* renders the locale default, and so does `'compact'`, which is
|
|
285
|
+
* `formatDateTime`'s own vocabulary but not this path's. The pattern part is
|
|
286
|
+
* not new behaviour introduced here: `plugin-dashboard`'s `recordFields`
|
|
287
|
+
* already routes a date-shaped `format` into the same style slot and gets the
|
|
288
|
+
* same locale default. Closing it would mean teaching the shared path a
|
|
289
|
+
* pattern grammar, which is a change to the path itself and to every list cell
|
|
290
|
+
* that reads it — not a measure concern, and not either card.
|
|
291
|
+
*
|
|
292
|
+
* ⚠️ `'relative'` renders the ABSOLUTE face for a value more than ±7 days from
|
|
293
|
+
* today, on both arms, by `formatRelativeDate`'s own design. That is correct
|
|
294
|
+
* output, not a regression — but it also means an out-of-window value is
|
|
295
|
+
* useless for testing this path, because it renders identically whether
|
|
296
|
+
* `format` is honoured or dropped. Both readings that missed #8352 were taken
|
|
297
|
+
* on 40–60-day-old data.
|
|
107
298
|
*/
|
|
108
299
|
export function formatMeasure(v, format, currency, percentScale, locale) {
|
|
109
300
|
if (v == null)
|
|
110
301
|
return '—';
|
|
111
|
-
if (typeof v !== 'number')
|
|
112
|
-
|
|
302
|
+
if (typeof v !== 'number') {
|
|
303
|
+
// Ahead of the `String(v)` short-circuit, which is what made `format` dead
|
|
304
|
+
// on this path and printed a `min`/`max` over a datetime as a raw
|
|
305
|
+
// 24-character ISO string (objectui#7178). Deliberately NOT ahead of the
|
|
306
|
+
// `typeof` test itself: above that line the argument can be a bare number,
|
|
307
|
+
// and any date test generous enough to consider one would have to decide
|
|
308
|
+
// whether `1751612400000` is epoch milliseconds — which is how a measure
|
|
309
|
+
// that counts things starts rendering as a date. Numbers reach the numeral
|
|
310
|
+
// formatter below byte for byte as before.
|
|
311
|
+
const asDate = formatMeasureDate(v, format, locale);
|
|
312
|
+
return asDate ?? String(v);
|
|
313
|
+
}
|
|
113
314
|
const decimals = format ? (format.split('.')[1]?.match(/0/g)?.length ?? 0) : undefined;
|
|
114
315
|
if (currency) {
|
|
115
316
|
try {
|
|
@@ -145,18 +346,24 @@ export function formatMeasure(v, format, currency, percentScale, locale) {
|
|
|
145
346
|
// A legacy "$" literal in the format string is still honored (explicit author
|
|
146
347
|
// choice) — but it is NOT how a real currency field gets its symbol.
|
|
147
348
|
const legacyDollar = format.includes('$') ? '$' : '';
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
349
|
+
// The column's storage is STATED, never read off the value (objectui#11475).
|
|
350
|
+
//
|
|
351
|
+
// The server annotates a percentage column with its `percentScale`: a
|
|
352
|
+
// `derived: { op: 'ratio' }` measure is a fraction by definition, and a
|
|
353
|
+
// measure over a `percent` field inherits that field's scale
|
|
354
|
+
// (`AnalyticsResult.fields[].percentScale`). That annotation wins, which is
|
|
355
|
+
// what fixed an SLA rate of full compliance reading "1.0%" instead of
|
|
356
|
+
// "100.0%" (#3136).
|
|
152
357
|
//
|
|
153
|
-
// A
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
358
|
+
// A column the server does NOT annotate is, in the contract's words, "not a
|
|
359
|
+
// percentage", so the only statement left is the author's numeral PATTERN,
|
|
360
|
+
// and numeral's `%` multiplies by 100: a `'0.0%'` pattern states a fraction.
|
|
361
|
+
// That is the spec's own reading for a measure over a plain number ("keeps
|
|
362
|
+
// whatever the author's format string implies", `percentScaleOf`'s doc). It
|
|
363
|
+
// used to fall to a magnitude guess instead, which read an unannotated `57`
|
|
364
|
+
// as 57% and an unannotated `0.57` as 57% alike.
|
|
158
365
|
const display = isPercent
|
|
159
|
-
? (
|
|
366
|
+
? percentDisplayValue(v, percentScale ?? 'fraction')
|
|
160
367
|
: v;
|
|
161
368
|
// The percent sign is the LOCALE's, not a literal '%' (objectui#4576).
|
|
162
369
|
//
|
|
@@ -164,20 +371,32 @@ export function formatMeasure(v, format, currency, percentScale, locale) {
|
|
|
164
371
|
// a German session read `1.234,5%` from a dashboard measure beside
|
|
165
372
|
// `1.234,5 %` (no-break space — the German percent convention) from a list
|
|
166
373
|
// cell showing the SAME number, because `formatPercent` had gone through
|
|
167
|
-
// `Intl` since #4553. That contradicted {@link percentDisplayValue}
|
|
168
|
-
//
|
|
374
|
+
// `Intl` since #4553. That contradicted the promise {@link percentDisplayValue}
|
|
375
|
+
// made then, that the two surfaces "can never drift": the SCALING had stopped
|
|
169
376
|
// drifting, the CONVENTION had started. Measured to differ in de, fr, es, ru,
|
|
170
377
|
// sv, cs, fi (no-break space), tr (the sign moves to the FRONT: `%1.234,5`)
|
|
171
378
|
// and ar (its own sign plus U+061C); en, ja, zh were already identical.
|
|
172
379
|
//
|
|
173
380
|
// `style: 'percentPoints'` is what closes it, and the choice of route is
|
|
174
381
|
// measured, not incidental. `display` is already in percentage POINTS, while
|
|
175
|
-
// `Intl`'s `style: 'percent'` wants a fraction — so routing through that
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
382
|
+
// `Intl`'s `style: 'percent'` wants a fraction — so routing through that
|
|
383
|
+
// would mean dividing by 100 for `Intl` to multiply straight back, and that
|
|
384
|
+
// round trip is lossy at rounding TIES: 27,581 of 1,200,013
|
|
385
|
+
// ordinary-magnitude en-US forms move (`0.175` at 2 decimals goes from
|
|
386
|
+
// `0.18%` to `0.17%`), plus `MAX_SAFE_INTEGER` and everything from 1e23 up.
|
|
387
|
+
// That count is #4576's tie-dense grid — 0.005 steps to 2,000, precisions
|
|
388
|
+
// 0/1/2 — measured on THIS formatter's call shape. #4590 re-measured the same
|
|
389
|
+
// route through `formatPercent` and reports 27,577 of 1,200,003: a different
|
|
390
|
+
// form set, not a correction of this one.
|
|
391
|
+
//
|
|
392
|
+
// No caller in this repo takes the divide-by-100 route today. `formatPercent`
|
|
393
|
+
// was the last one and moved to `style: 'percentPoints'` in #4590; the two
|
|
394
|
+
// `style: 'percent'` sites left (`element:number`'s format options and the
|
|
395
|
+
// report exporter's Excel options) hand `Intl` a FRACTION, which is that
|
|
396
|
+
// style's own contract and not this trap. The route now survives only where a
|
|
397
|
+
// test constructs it in order to show it disagreeing.
|
|
398
|
+
//
|
|
399
|
+
// Formatting the points directly with the percent UNIT was measured to
|
|
181
400
|
// produce a byte-identical percent affix to `style: 'percent'` across all 171
|
|
182
401
|
// locale tags tested, while moving ZERO of those 1,200,013 en forms — the
|
|
183
402
|
// same convention by a route that does not touch the value.
|
|
@@ -213,12 +432,31 @@ export function formatDimensionValue(v, locale) {
|
|
|
213
432
|
*
|
|
214
433
|
* `fieldLabel` is injected (rather than imported) so this stays React/i18n-free;
|
|
215
434
|
* callers pass `useSafeFieldLabel().fieldLabel`.
|
|
435
|
+
*
|
|
436
|
+
* ## `builtinAggregateLabels` (objectui#7534, sibling of #7258)
|
|
437
|
+
*
|
|
438
|
+
* A result field the analytics service minted as a BUILT-IN default measure
|
|
439
|
+
* carries `builtinAggregate` (objectstack#14492) and a hard-coded English
|
|
440
|
+
* `label` ('Count'). {@link resolveMeasureLabel} is the ONE resolver for that —
|
|
441
|
+
* #7258 wired it into `buildChartSeries()`, so a chart legend already read
|
|
442
|
+
* `计数` on a zh console while the table underneath it, the KPI caption, the
|
|
443
|
+
* pivot header and the dataset preview all still said `Count`, because they
|
|
444
|
+
* resolve their titles through here instead.
|
|
445
|
+
*
|
|
446
|
+
* Passing the caller's resolved labels closes that gap without a second
|
|
447
|
+
* resolution order: the discriminator is consulted FIRST (exactly as on the
|
|
448
|
+
* chart seam), the existing `fieldLabel` convention still applies on top, and
|
|
449
|
+
* the raw name is still the floor. Omitting the argument reproduces the
|
|
450
|
+
* previous output byte for byte — an author-declared measure carries no
|
|
451
|
+
* discriminator and keeps its wire `label` verbatim (objectui#4106), and a
|
|
452
|
+
* provider-less host resolves no labels and so changes nothing.
|
|
216
453
|
*/
|
|
217
|
-
export function buildDatasetFieldHelpers(fields, object, fieldLabel) {
|
|
454
|
+
export function buildDatasetFieldHelpers(fields, object, fieldLabel, builtinAggregateLabels) {
|
|
218
455
|
const fieldByName = new Map((fields ?? []).map((f) => [f.name, f]));
|
|
219
456
|
const measureField = (name) => fieldByName.get(name);
|
|
220
457
|
const headerLabel = (name) => {
|
|
221
|
-
const
|
|
458
|
+
const field = measureField(name);
|
|
459
|
+
const fallback = field ? resolveMeasureLabel(field, builtinAggregateLabels) : name;
|
|
222
460
|
return object && fieldLabel ? fieldLabel(object, name, fallback) : fallback;
|
|
223
461
|
};
|
|
224
462
|
return { measureField, headerLabel };
|
|
@@ -229,17 +467,126 @@ export function buildDatasetFieldHelpers(fields, object, fieldLabel) {
|
|
|
229
467
|
* Each drillable dimension maps to its underlying object field, filtered by the
|
|
230
468
|
* dimension's RAW grouped value (from the server's parallel `drillRawRows`, NOT
|
|
231
469
|
* the visible row which carries the display LABEL — a select/lookup label would
|
|
232
|
-
* mis-filter).
|
|
233
|
-
*
|
|
234
|
-
*
|
|
470
|
+
* mis-filter). The render-time `runtimeFilter` is ANDed in so the drilled list
|
|
471
|
+
* stays within the same slice the aggregate was computed over.
|
|
472
|
+
*
|
|
473
|
+
* ## What the EMPTY bucket means, and why it is spelled `{ $null: true }`
|
|
474
|
+
*
|
|
475
|
+
* A drill must return exactly the rows the clicked bucket COUNTED. The empty
|
|
476
|
+
* bucket is the one the aggregate grouped rows with NO VALUE for that dimension
|
|
477
|
+
* into, so the drill means "this dimension has no value" — NOT "this dimension
|
|
478
|
+
* holds the literal value null". Those are different row sets, and the
|
|
479
|
+
* difference is measurable rather than theoretical: against `ValueDataSource`'s
|
|
480
|
+
* matcher over rows `{owner:'alice'}` / `{owner:null}` / `{}` (no `owner` key),
|
|
481
|
+
* `['owner','is_null',true]` selects the explicit-null row AND the missing-key
|
|
482
|
+
* row, while an equality test against `null` selects only the explicit-null one
|
|
483
|
+
* and drops the row that has no `owner` at all. The bucket counted both, so
|
|
484
|
+
* is-null is the one that agrees with the number the user clicked.
|
|
235
485
|
*
|
|
236
|
-
*
|
|
486
|
+
* ⇒ every empty authoring — `''`, `null` and `undefined` alike — becomes ONE
|
|
487
|
+
* spelling, `{ [field]: { $null: true } }`, which `convertFiltersToAST` lowers
|
|
488
|
+
* to `[field, 'is_null', true]`. The three are not told apart because the
|
|
489
|
+
* server already merged them into the single bucket the user clicked; there is
|
|
490
|
+
* no second bucket here for a second spelling to address.
|
|
491
|
+
*
|
|
492
|
+
* ⚠️ This value used to be a bare `null`, and that is the defect objectui#9085
|
|
493
|
+
* records: `convertFiltersToAST`'s OLDEST pinned behaviour is to SKIP a key
|
|
494
|
+
* whose value is `null` / `undefined` (pinned as "should skip null and
|
|
495
|
+
* undefined values"), so the constraint was dropped on the way to the wire and
|
|
496
|
+
* the empty-bucket drill answered with a SUPERSET — every row, silently. The
|
|
497
|
+
* repair is here at the PRODUCER and ⛔ not at that converter: the skip is
|
|
498
|
+
* relied on by other producers, and changing it would move every caller's
|
|
499
|
+
* meaning at once (objectui#9020 ruled it stays). `$null` is not a new
|
|
500
|
+
* vocabulary either — it is a declared spec operator this dialect already
|
|
501
|
+
* lowers, and the one spelling that says the same thing on BOTH of
|
|
502
|
+
* `ValueDataSource`'s routes, where a bare `null` said "only an explicit null"
|
|
503
|
+
* on the plain-object route and "no constraint at all" on the AST route.
|
|
504
|
+
*
|
|
505
|
+
* ⚠️ Known BOUNDARY, unchanged by this and deliberately not widened: the drill
|
|
506
|
+
* "escape hatch" (the host's `openRecordList`, which serializes a drill filter
|
|
507
|
+
* into `filter[...]` URL params) has NO spelling for is-null — its operator
|
|
508
|
+
* vocabulary is equality plus four range bounds — so it drops this condition
|
|
509
|
+
* exactly as it already dropped the bare `null`, byte-for-byte the same query
|
|
510
|
+
* string. That surface is a superset today and stays one; closing it needs a
|
|
511
|
+
* URL-dialect operator on both the write and the read side, which is its own
|
|
512
|
+
* card.
|
|
513
|
+
*
|
|
514
|
+
* A time-bucketed date dimension (objectstack-ai/objectstack#1752) drills by RANGE, not equality — a
|
|
237
515
|
* humanized bucket ("2026-Q2") can't be exact-matched, so the server sends a
|
|
238
516
|
* half-open `[gte, lt)` per date dim in `rawRanges` instead of a raw value.
|
|
239
517
|
* Each becomes an ObjectQL range operator object (`{ $gte, $lt }`) so the drill
|
|
240
518
|
* scopes the list to the clicked time bucket instead of every bucket (which the
|
|
241
519
|
* old date-dim skip degraded to — a superset).
|
|
242
520
|
*
|
|
521
|
+
* ## The widget filter is CONJOINED, not spread (objectui#9137)
|
|
522
|
+
*
|
|
523
|
+
* ⛔ `runtimeFilter` is composed through {@link composeDrillFilter}, never by
|
|
524
|
+
* spreading it into the object literal below. Spreading is correct only for the
|
|
525
|
+
* OBJECT arm; this parameter's declared producer sends the ARRAY arm. The
|
|
526
|
+
* dashboard's `DashboardWidgetSchema.filter` docblock states it outright —
|
|
527
|
+
* "objectui passes an ObjectQL FilterNode array here, not the spec's
|
|
528
|
+
* `FilterCondition` envelope" — and `DatasetWidget`'s guard admits it
|
|
529
|
+
* (`typeof [] === 'object'` and `Object.keys(['x']).length === 1` are both
|
|
530
|
+
* true), so the array arm reached this line unfiltered. Spreading an ARRAY
|
|
531
|
+
* yields INDEX keys: `[['region','=','emea']]` became
|
|
532
|
+
* `{ '0': ['region','=','emea'] }`, which is not a condition anyone can honour.
|
|
533
|
+
*
|
|
534
|
+
* ⚠️ MEASURED, and it is NOT the silent superset the sibling sites had. An
|
|
535
|
+
* index key is not dropped on the way to the wire — every sink in circulation
|
|
536
|
+
* refuses it LOUDLY, because a bare array is not a legal equality comparand
|
|
537
|
+
* (objectui#8530 / objectui#8514): `convertFiltersToAST` THROWS
|
|
538
|
+
* `FilterOperatorError`, `driver-sql` answers `400 INVALID_FILTER`, and
|
|
539
|
+
* `ValueDataSource`'s matcher selects NOTHING. Against rows scoped
|
|
540
|
+
* `region = 'emea'`, drilling the `stage = 'won'` bucket returned `(none)`
|
|
541
|
+
* where the widget alone returned three rows and the conjunction returns the
|
|
542
|
+
* right two. So the widget's conditions were not merely lost — the drill was
|
|
543
|
+
* dead in the array arm. The failure is fail-CLOSED here, and this is the third
|
|
544
|
+
* site of the class objectui#8944 (chart) and objectui#9024 (pivot) removed.
|
|
545
|
+
*
|
|
546
|
+
* Three properties of THIS site, each confirmed rather than assumed — it is not
|
|
547
|
+
* a copy-paste of the other two:
|
|
548
|
+
*
|
|
549
|
+
* 1. The return stays a NON-optional `Record<string, unknown>`, because callers
|
|
550
|
+
* do not expect `undefined`. `composeDrillFilter` answers `undefined` when
|
|
551
|
+
* both sources are empty, so that one case is spelled `?? {}` — the same
|
|
552
|
+
* empty object this function already returned for a bucket with no dims.
|
|
553
|
+
* 2. Range operators SURVIVE. A time-bucketed dim contributes
|
|
554
|
+
* `{ $gte, $lt }`, and the composition re-spells it as two `$and` children
|
|
555
|
+
* on the same field (`[{d:{$gte}},{d:{$lt}}]`) rather than losing a bound;
|
|
556
|
+
* the selected rows are identical, and `serializeDrillFilterParams` walks
|
|
557
|
+
* `$and` (objectui#8944) so both bounds still reach `filter[d][gte|lt]`.
|
|
558
|
+
* 3. `runtimeFilter` is declared `unknown`, which is what the two consumers
|
|
559
|
+
* actually hand it — `any` from the dashboard, `Record<string, unknown>`
|
|
560
|
+
* from `DatasetReportRenderer`. Widening the PARAMETER is what admits both
|
|
561
|
+
* without a cast; ⛔ neither consumer's own type is changed by this.
|
|
562
|
+
*
|
|
563
|
+
* ⚠️ Conjunction changes one behaviour deliberately: a field named by BOTH the
|
|
564
|
+
* widget filter and the clicked bucket used to be overwritten by the bucket,
|
|
565
|
+
* and now both conditions apply. That is the point — a drill may narrow the
|
|
566
|
+
* widget's scope and may never widen it — and it is the posture the chart and
|
|
567
|
+
* pivot sites already ship.
|
|
568
|
+
*
|
|
569
|
+
* ## ⭐ Only the COMPOSING leg moves; the identity leg is left byte-identical
|
|
570
|
+
*
|
|
571
|
+
* The sink is asked only when there IS a second source. That is this site's
|
|
572
|
+
* one departure from `ObjectChart` / `ObjectPivotTable`, which call it
|
|
573
|
+
* unconditionally, and it is deliberate rather than a shortcut: the spread
|
|
574
|
+
* this replaces only ever ran when `runtimeFilter` was present, so the
|
|
575
|
+
* no-widget-filter path carries no defect to repair.
|
|
576
|
+
*
|
|
577
|
+
* ⚠️ Routing it anyway was MEASURED and rejected. `composeDrillFilter`'s own
|
|
578
|
+
* note — "a lone surviving source lowers back to exactly the flat object the
|
|
579
|
+
* spread produced" — holds only for a source of ONE condition; with two it
|
|
580
|
+
* lowers to `$and`, because `convertFiltersToAST` emits one node per condition
|
|
581
|
+
* and `parseFilterAST` cannot fold two nodes back onto one key. A `{ $gte,
|
|
582
|
+
* $lt }` range is two conditions on its own, so even a single date bucket
|
|
583
|
+
* re-spells. Sending the identity leg through the sink therefore re-shaped
|
|
584
|
+
* every multi-condition dataset drill — including the controls objectui#9085,
|
|
585
|
+
* objectui#4056, objectstack#5473 and objectstack-ai/objectstack#1752 pin on paths this card does not
|
|
586
|
+
* touch — for ZERO change in the rows selected or the `filter[...]` params
|
|
587
|
+
* emitted. ⛔ Do not "simplify" this to an unconditional call without redoing
|
|
588
|
+
* that measurement.
|
|
589
|
+
*
|
|
243
590
|
* Shared by the dashboard `DatasetWidget` and the report renderer so a drill
|
|
244
591
|
* filters identically (and correctly, including lookups) on both surfaces.
|
|
245
592
|
*/
|
|
@@ -247,7 +594,12 @@ export function buildDatasetDrillFilter(rawRow, drillDims, dimensionFields, runt
|
|
|
247
594
|
const drillFilter = {};
|
|
248
595
|
for (const d of drillDims) {
|
|
249
596
|
const raw = rawRow?.[d];
|
|
250
|
-
|
|
597
|
+
// `null` is in this test, not only `''` / `undefined`: JSON cannot carry
|
|
598
|
+
// `undefined`, so a SQL NULL grouped value arrives over the wire AS `null`
|
|
599
|
+
// — it is the empty bucket's most common shape, and leaving it out would
|
|
600
|
+
// fix the defect for the spellings the wire rarely uses.
|
|
601
|
+
drillFilter[dimensionFields[d]] =
|
|
602
|
+
raw === '' || raw === null || raw === undefined ? { $null: true } : raw;
|
|
251
603
|
}
|
|
252
604
|
if (rawRanges) {
|
|
253
605
|
for (const r of Object.values(rawRanges)) {
|
|
@@ -255,5 +607,8 @@ export function buildDatasetDrillFilter(rawRow, drillDims, dimensionFields, runt
|
|
|
255
607
|
drillFilter[r.field] = { $gte: r.gte, $lt: r.lt };
|
|
256
608
|
}
|
|
257
609
|
}
|
|
258
|
-
|
|
610
|
+
// ⛔ The composition is `composeDrillFilter`'s, never a spread. The identity
|
|
611
|
+
// leg is deliberately left alone — see "only the composing leg moves" above.
|
|
612
|
+
// `?? {}` is property 1: the empty/empty answer stays an empty object.
|
|
613
|
+
return runtimeFilter ? composeDrillFilter(runtimeFilter, drillFilter) ?? {} : drillFilter;
|
|
259
614
|
}
|