@object-ui/core 17.6.0 → 18.0.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 +7260 -0
- package/README.md +228 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +473 -69
- package/dist/actions/ActionRunner.js +792 -125
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/UndoManager.d.ts +8 -13
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +67 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/navigationTarget.d.ts +41 -0
- package/dist/actions/navigationTarget.js +20 -0
- package/dist/actions/recordDelete.d.ts +154 -0
- package/dist/actions/recordDelete.js +110 -0
- package/dist/adapters/ApiDataSource.d.ts +8 -4
- package/dist/adapters/ApiDataSource.js +19 -7
- 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/adapters/resolveDataSource.d.ts +9 -6
- package/dist/adapters/resolveDataSource.js +9 -12
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +8 -25
- package/dist/data-scope/ViewDataProvider.js +27 -54
- 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 +86 -0
- package/dist/evaluator/ExpressionEvaluator.js +184 -12
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/deadlineSettled.d.ts +40 -0
- package/dist/evaluator/deadlineSettled.js +45 -0
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +117 -16
- package/dist/evaluator/fieldRules.d.ts +88 -4
- package/dist/evaluator/fieldRules.js +208 -31
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +103 -34
- package/dist/evaluator/listConditional.js +127 -50
- package/dist/evaluator/optionRules.d.ts +77 -6
- package/dist/evaluator/optionRules.js +87 -10
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
- package/dist/evaluator/rowPredicateCanon.js +65 -115
- package/dist/evaluator/subjectPermissions.d.ts +105 -0
- package/dist/evaluator/subjectPermissions.js +86 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +83 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +382 -34
- package/dist/registry/Registry.js +247 -12
- 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/utils/affordanceGrants.d.ts +371 -0
- package/dist/utils/affordanceGrants.js +252 -0
- 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 +95 -112
- package/dist/utils/chart-presentation.js +169 -104
- package/dist/utils/chart-series.d.ts +165 -5
- package/dist/utils/chart-series.js +123 -18
- 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 +389 -46
- package/dist/utils/date-display.d.ts +346 -0
- package/dist/utils/date-display.js +644 -0
- package/dist/utils/date-macros.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 +81 -9
- package/dist/utils/expand-fields.js +81 -9
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +158 -6
- package/dist/utils/filter-converter.js +1098 -24
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -33
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/managedBy.d.ts +3 -3
- 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 +135 -11
- package/dist/utils/normalize-list-view.js +243 -64
- package/dist/utils/number-display.d.ts +63 -14
- 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 +45 -12
- package/dist/utils/predicate-fields.js +141 -12
- 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/sort-query.d.ts +115 -15
- 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/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +8 -4
- package/dist/validation/index.js +8 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +225 -83
- package/dist/validation/server-owned-value.d.ts +5 -1
- package/dist/validation/server-owned-value.js +5 -1
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -3
- package/package.json +8 -9
- 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
|
@@ -9,19 +9,28 @@
|
|
|
9
9
|
* sort-query — lower a block's authored `sort` to the `$orderby` value a
|
|
10
10
|
* `DataSource.find` query carries.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* ONE authored spelling reaches every object-bound block: the spec's
|
|
13
|
+
* `SortConfig[]` (`[{ field, order }]`). The legacy OData-ish string clause
|
|
14
|
+
* (`"name desc"`) that this function also honoured until objectui#8221 is
|
|
15
|
+
* RETIRED — director ruling, decision batch #77, 2026-09-07 (objectui#8221,
|
|
16
|
+
* maintainer approved), option B: "The platform has one `sort` spelling, the
|
|
17
|
+
* array, everywhere."
|
|
18
|
+
*
|
|
19
|
+
* Why the string had to go rather than be declared alongside the array: every
|
|
20
|
+
* `sort` input in the registry publishes `type: 'array'` alone, so the html
|
|
21
|
+
* tier already answered `type-mismatch` for the string, and
|
|
22
|
+
* `@objectstack/spec` refuses it outright on `element-record-picker`. A
|
|
23
|
+
* spelling that core implements, the docs teach and the validator rejects is
|
|
24
|
+
* three faces disagreeing, and the ruling settled which one wins. Declaring
|
|
25
|
+
* per-block string arms instead (option A) was rejected by name: it would make
|
|
26
|
+
* one key mean different things on different blocks.
|
|
18
27
|
*
|
|
19
28
|
* This is now the ONLY definition in the repo. It was hoisted here because
|
|
20
29
|
* objectstack#7137 added two more read sites (`object-timeline`,
|
|
21
30
|
* `record:line_items`) next to three sibling blocks that each inlined a
|
|
22
31
|
* byte-identical private copy (`ObjectGantt` / `ObjectMap` / `ObjectCalendar`),
|
|
23
32
|
* and a fifth and sixth copy is how the conversions start disagreeing. Those
|
|
24
|
-
* three copies were deliberately left alone by #7137 and collapsed onto this
|
|
33
|
+
* three copies were deliberately left alone by objectstack-ai/objectstack#7137 and collapsed onto this
|
|
25
34
|
* function by objectui#4022; every block now imports it from here.
|
|
26
35
|
*
|
|
27
36
|
* Two deliberate differences from those retired private copies, both of which
|
|
@@ -29,29 +38,120 @@
|
|
|
29
38
|
* tolerance — and both are BEHAVIOUR CHANGES the migration delivered, not pure
|
|
30
39
|
* refactor:
|
|
31
40
|
*
|
|
32
|
-
* -
|
|
33
|
-
*
|
|
34
|
-
* `
|
|
35
|
-
*
|
|
41
|
+
* - **A missing `order` is READ as ascending rather than dropped.** ⚠️ This
|
|
42
|
+
* bullet used to say `order` is "optional in `SortConfig`". It is not:
|
|
43
|
+
* `SortConfig.order` is required on the interface, on its zod mirror, and on
|
|
44
|
+
* `@objectstack/spec`'s `SortItemSchema`, which refuses an entry without it
|
|
45
|
+
* (`invalid_value` at `0.order`, measured on `@objectstack/spec@17.4.0`).
|
|
46
|
+
* Corrected under objectui#8973, which is where objectui#8767's contract
|
|
47
|
+
* review routed this sentence and its twin in
|
|
48
|
+
* `@object-ui/types`' `ObjectGridSchema.sort` docblock. The tolerance is
|
|
49
|
+
* real but it is a RUNTIME one — types are erased, so an entry missing
|
|
50
|
+
* `order` still arrives and still has to mean something. The private copies
|
|
51
|
+
* required BOTH keys and silently dropped such an entry, which lost an
|
|
36
52
|
* authored sort key instead of ordering by it.
|
|
37
53
|
* - **Nothing usable yields `undefined`, never `{}`.** An empty object is a
|
|
38
54
|
* truthy value that means "no ordering" only by accident of the adapter's
|
|
39
55
|
* serializer; `undefined` says it, so the query simply carries no `$orderby`.
|
|
40
56
|
*
|
|
41
57
|
* Not a lenient alias: an unusable input (a number, an object, an array of
|
|
42
|
-
* strings) yields `undefined` rather than a guess. Only the
|
|
43
|
-
* schema types declare
|
|
58
|
+
* strings) yields `undefined` rather than a guess. Only the ONE spelling the
|
|
59
|
+
* schema types declare is honoured.
|
|
60
|
+
*
|
|
61
|
+
* ⚠️ A retired string is REFUSED OUT LOUD, not dropped in silence. Types are
|
|
62
|
+
* erased, so the narrowed signature below cannot stop a string: authored JSON,
|
|
63
|
+
* a stored `sys_metadata` row and an `as any` bag all reach this function
|
|
64
|
+
* unparsed. Returning `undefined` and saying nothing would turn every one of
|
|
65
|
+
* those into a row order that quietly stopped applying — the exact failure this
|
|
66
|
+
* repository has measured over and over. See {@link convertSortToQueryParams}.
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ OUT LOUD is about the SPELLING, not about the container it arrives in
|
|
69
|
+
* (objectui#9955). `sort: ['name asc']` is the same retired clause one level
|
|
70
|
+
* down, and it used to reach the array arm's `continue` and leave through the
|
|
71
|
+
* `undefined` return with nothing printed — the loud scalar refusal sat one arm
|
|
72
|
+
* above it, so the two spellings of one mistake got opposite treatment. Both are
|
|
73
|
+
* loud now. ⛔ Being loud is NOT accepting: the sentence four paragraphs up
|
|
74
|
+
* ("an unusable input … yields `undefined` rather than a guess") still holds
|
|
75
|
+
* byte for byte, and `['name asc']` still contributes no ordering. The ruling
|
|
76
|
+
* quoted at the top of this docblock — one `sort` spelling, the array,
|
|
77
|
+
* everywhere — is the reason making it WORK was never on the table.
|
|
44
78
|
*/
|
|
45
79
|
/** A field name paired with a direction — the spec's `SortConfig`. */
|
|
46
80
|
export interface QuerySortEntry {
|
|
47
81
|
field?: string;
|
|
48
82
|
order?: 'asc' | 'desc';
|
|
49
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Test seam — forget which retired spellings have been reported.
|
|
86
|
+
*
|
|
87
|
+
* Needed because the dedupe above is module state: without it the second test
|
|
88
|
+
* to assert the refusal diagnostic would observe silence and pass for the
|
|
89
|
+
* wrong reason.
|
|
90
|
+
*/
|
|
91
|
+
export declare function resetRetiredSortSpellingReports(): void;
|
|
92
|
+
/**
|
|
93
|
+
* A sort entry after normalization — BOTH keys present, always. That is the
|
|
94
|
+
* difference from {@link QuerySortEntry}, whose two members are optional
|
|
95
|
+
* because it describes what an author may WRITE, not what survives this door.
|
|
96
|
+
*/
|
|
97
|
+
export interface NormalizedSortEntry {
|
|
98
|
+
field: string;
|
|
99
|
+
order: 'asc' | 'desc';
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Decide WHICH authored entries survive and WHAT a missing direction means —
|
|
103
|
+
* the whole of this module's normalization, with no opinion about the wire.
|
|
104
|
+
*
|
|
105
|
+
* Split out of {@link convertSortToQueryParams} by objectui#8973, which measured
|
|
106
|
+
* `object-grid` re-implementing this decision privately and getting it wrong:
|
|
107
|
+
* its array arm interpolated every key unconditionally, so an entry missing
|
|
108
|
+
* `field` or `order` reached the wire as the literal text `undefined`
|
|
109
|
+
* (`$orderby: 'name undefined'`), which the server answers `400 INVALID_QUERY`.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ It is SHAPE-AGNOSTIC on purpose, and that is what makes it reusable where
|
|
112
|
+
* the map below is not. `object-grid` sends a `"field order"` join string, not
|
|
113
|
+
* this module's `{field: direction}` map; swapping its wire shape is route B on
|
|
114
|
+
* objectui#8767, which the maintainer DECLINED by name (2026-09-10) pending a
|
|
115
|
+
* card that measures the server contract and both readers. Exporting the
|
|
116
|
+
* decision without the map lets that block share the ONE implementation of the
|
|
117
|
+
* rule while keeping the wire shape the declination protects — so "one
|
|
118
|
+
* operation, one implementation" is satisfied for the operation that actually
|
|
119
|
+
* has two copies, rather than being traded away for a shape change nobody
|
|
120
|
+
* ruled on.
|
|
121
|
+
*
|
|
122
|
+
* The two rules, unchanged from the map builder they were lifted out of:
|
|
123
|
+
*
|
|
124
|
+
* - an entry with no usable `field` is SKIPPED (it names nothing to order by);
|
|
125
|
+
* - `order` normalizes to `'asc'` unless it is exactly `'desc'`, so a missing
|
|
126
|
+
* direction means ascending and a garbage one does not reach the wire.
|
|
127
|
+
*
|
|
128
|
+
* A third rule was added by objectui#9955 and it changes no RETURN value: a
|
|
129
|
+
* STRING entry is the retired clause one container deeper, so it is now REFUSED
|
|
130
|
+
* OUT LOUD before the skip above reaches it — and then skipped, contributing
|
|
131
|
+
* nothing, exactly as before. ⛔ The two rules above are untouched by it; in
|
|
132
|
+
* particular the entry-shaped object with no `field` keeps its DELIBERATE
|
|
133
|
+
* silence, because it names nothing to order by rather than naming it in a
|
|
134
|
+
* retired spelling. The seam is `typeof entry === 'string'` and only that.
|
|
135
|
+
*
|
|
136
|
+
* @returns The surviving entries in authored order, or `undefined` when nothing
|
|
137
|
+
* orderable was authored — never an empty array, so callers can omit the
|
|
138
|
+
* query key entirely rather than asking for an ordering with no content.
|
|
139
|
+
*/
|
|
140
|
+
export declare function normalizeSortEntries(sort: QuerySortEntry[] | undefined | null): NormalizedSortEntry[] | undefined;
|
|
50
141
|
/**
|
|
51
142
|
* Normalize an authored `sort` into the field→direction map used for
|
|
52
143
|
* `QueryParams.$orderby`.
|
|
53
144
|
*
|
|
54
|
-
*
|
|
145
|
+
* The decision about which entries survive lives in
|
|
146
|
+
* {@link normalizeSortEntries}; this function is only the MAP projection of it.
|
|
147
|
+
* Duplicate fields therefore collapse last-wins, keeping the position of the
|
|
148
|
+
* first mention — the same thing the single-pass loop this replaced did, since
|
|
149
|
+
* an object key keeps its insertion position when it is re-assigned.
|
|
150
|
+
*
|
|
151
|
+
* @param sort `SortConfig[]` — the one declared spelling. The legacy string
|
|
152
|
+
* clause (`"name desc"`) is retired (objectui#8221): a string that reaches here
|
|
153
|
+
* at runtime is refused with a diagnostic naming the array form, and this
|
|
154
|
+
* function returns `undefined` so the query carries no `$orderby`.
|
|
55
155
|
* @returns The ordering map, or `undefined` when nothing orderable was authored.
|
|
56
156
|
*/
|
|
57
|
-
export declare function convertSortToQueryParams(sort:
|
|
157
|
+
export declare function convertSortToQueryParams(sort: QuerySortEntry[] | undefined | null): Record<string, 'asc' | 'desc'> | undefined;
|
package/dist/utils/sort-query.js
CHANGED
|
@@ -6,30 +6,257 @@
|
|
|
6
6
|
* LICENSE file in the root directory of this source tree.
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* The canonical spelling, quoted in the refusal diagnostic so the message
|
|
10
|
+
* carries the fix and not just the complaint.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* ⚠️ BOTH keys, deliberately. `order` is REQUIRED on `SortConfig`, on its zod
|
|
13
|
+
* counterpart and on `@objectstack/spec`'s `SortItemSchema`, so an example that
|
|
14
|
+
* omitted it would make this diagnostic prescribe metadata a publish refuses —
|
|
15
|
+
* objectui#9031. The pin feeds this very example back through `SortItemSchema`
|
|
16
|
+
* from the installed artifact rather than eyeballing it, because the way this
|
|
17
|
+
* regresses is somebody "simplifying" the example, not somebody editing prose.
|
|
14
18
|
*/
|
|
15
|
-
|
|
19
|
+
const ARRAY_FORM_EXAMPLE = "[{ field: 'name', order: 'desc' }]";
|
|
20
|
+
/**
|
|
21
|
+
* Retired string clauses already reported, so one bad `sort` logs its
|
|
22
|
+
* prescription ONCE instead of once per render. The message is a fix
|
|
23
|
+
* instruction for an author, not a per-render event, and this sink runs inside
|
|
24
|
+
* the query memo of every object-bound block — a related-list derivation over
|
|
25
|
+
* an object with N lists would otherwise print N lines per pass.
|
|
26
|
+
*
|
|
27
|
+
* Module state, exactly as {@link resetRetiredSortSpellingReports}'s sibling
|
|
28
|
+
* `resetRetiredFieldTypeReports` keeps it for retired field types: the dedupe
|
|
29
|
+
* is per SPELLING, so two blocks that inherit the same bad view sort still
|
|
30
|
+
* print one line between them.
|
|
31
|
+
*
|
|
32
|
+
* ⚠️ It holds keys, not raw spellings — the two arms that report are ARM-TAGGED
|
|
33
|
+
* (objectui#9955). A retired string arrives either as the whole `sort` or as one
|
|
34
|
+
* entry of the array, and the two get DIFFERENT messages (what a reader must
|
|
35
|
+
* change, and what the query still carries, are not the same in the two cases).
|
|
36
|
+
* On one shared raw key the first arm to fire would silence the other, and the
|
|
37
|
+
* author would read a sentence about the wrong container. {@link retiredKey}
|
|
38
|
+
* builds the keys so the two spaces cannot collide, and the bound each arm keeps
|
|
39
|
+
* is unchanged: ONE line per distinct spelling per arm, however many blocks
|
|
40
|
+
* inherit the same bad view.
|
|
41
|
+
*/
|
|
42
|
+
const reportedRetiredSpellings = new Set();
|
|
43
|
+
/**
|
|
44
|
+
* Key a report by the arm that produced it AND the spelling that produced it.
|
|
45
|
+
*
|
|
46
|
+
* The two spaces are disjoint by construction rather than by luck: `arm` is one
|
|
47
|
+
* of two fixed tokens, neither of which contains a newline, so the text before
|
|
48
|
+
* the first newline identifies the arm no matter what an author wrote. A raw
|
|
49
|
+
* spelling can be anything at all — it comes out of authored JSON — so a bare
|
|
50
|
+
* string prefix would only be disjoint until somebody authored the prefix.
|
|
51
|
+
*/
|
|
52
|
+
function retiredKey(arm, spelling) {
|
|
53
|
+
return `${arm}\n${spelling}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Test seam — forget which retired spellings have been reported.
|
|
57
|
+
*
|
|
58
|
+
* Needed because the dedupe above is module state: without it the second test
|
|
59
|
+
* to assert the refusal diagnostic would observe silence and pass for the
|
|
60
|
+
* wrong reason.
|
|
61
|
+
*/
|
|
62
|
+
export function resetRetiredSortSpellingReports() {
|
|
63
|
+
reportedRetiredSpellings.clear();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Name a retired string `sort` clause, once per spelling.
|
|
67
|
+
*
|
|
68
|
+
* `console.error`, not `warn`, and not dev-gated: this call REFUSES an authored
|
|
69
|
+
* row order, so the page renders in a different order than the author asked
|
|
70
|
+
* for. That is the same severity class as `reportRetiredFieldType`, which is
|
|
71
|
+
* also unconditional, and the opposite of `warnOnUnknownActionKeys`, which only
|
|
72
|
+
* reports keys nothing was ever going to read.
|
|
73
|
+
*
|
|
74
|
+
* ⚠️ This text is read at the moment the author is ALREADY being corrected, so
|
|
75
|
+
* it must not prescribe metadata the spec refuses (objectui#9031). It used to
|
|
76
|
+
* end "`order` is optional and means `'asc'`" — a RUNTIME tolerance stated as
|
|
77
|
+
* an AUTHORING permission. Both halves of that are load-bearing and both have to
|
|
78
|
+
* survive any rewording:
|
|
79
|
+
*
|
|
80
|
+
* - the tolerance is REAL — a missing `order` is read as ascending here rather
|
|
81
|
+
* than dropped (see {@link normalizeSortEntries}), because types are erased
|
|
82
|
+
* and an entry that arrives without it still has to mean something;
|
|
83
|
+
* - and it is NOT permission — `order` is required on `SortConfig`, on its zod
|
|
84
|
+
* counterpart and on `@objectstack/spec`'s `SortItemSchema`, which refuses an
|
|
85
|
+
* entry without it.
|
|
86
|
+
*
|
|
87
|
+
* Stating only the first is what made an author who followed this correction
|
|
88
|
+
* verbatim get refused a second time, at publish, by a different door. Deleting
|
|
89
|
+
* the tolerance instead would be the opposite error: it is what this renderer
|
|
90
|
+
* actually does, and a diagnostic that denies it sends the author hunting for a
|
|
91
|
+
* dropped sort key that was never dropped.
|
|
92
|
+
*/
|
|
93
|
+
function reportRetiredSortSpelling(sort) {
|
|
94
|
+
const key = retiredKey('scalar', sort);
|
|
95
|
+
if (reportedRetiredSpellings.has(key))
|
|
96
|
+
return;
|
|
97
|
+
reportedRetiredSpellings.add(key);
|
|
98
|
+
console.error(`[object-ui] convertSortToQueryParams: the legacy string \`sort\` clause is retired ` +
|
|
99
|
+
`(objectui#8221) and was REFUSED — received ${JSON.stringify(sort)}, so this query ` +
|
|
100
|
+
`carries no \`$orderby\`. Write the array form instead: ` +
|
|
101
|
+
`sort: ${ARRAY_FORM_EXAMPLE} — both keys, on every entry. \`order\` is required: on ` +
|
|
102
|
+
`\`SortConfig\`, on its zod counterpart, and on \`@objectstack/spec\`'s \`SortItemSchema\`, ` +
|
|
103
|
+
`which refuses an entry without it. (A missing \`order\` is still read as \`'asc'\` ` +
|
|
104
|
+
`here — that is a runtime tolerance, not permission to omit the key: metadata ` +
|
|
105
|
+
`written that way is refused when it is published.) ` +
|
|
106
|
+
`The array is the only spelling every \`sort\` input declares, and the only one ` +
|
|
107
|
+
`\`@objectstack/spec\` accepts.`);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Name a retired string clause that arrived as an ENTRY of the `sort` array,
|
|
111
|
+
* once per spelling (objectui#9955).
|
|
112
|
+
*
|
|
113
|
+
* A sibling of {@link reportRetiredSortSpelling} rather than a reuse of it, and
|
|
114
|
+
* the difference is the one sentence an author acts on: the scalar refusal can
|
|
115
|
+
* say "this query carries no `$orderby`" because refusing the whole value is
|
|
116
|
+
* refusing the whole ordering, while one bad entry among good ones leaves the
|
|
117
|
+
* other entries lowering normally — `[{ field: 'a', order: 'asc' }, 'b desc']`
|
|
118
|
+
* still orders by `a`. Reusing the scalar text here would tell an author their
|
|
119
|
+
* rows are unordered when they are ordered by everything except the key they
|
|
120
|
+
* are hunting for, which is worse than silence.
|
|
121
|
+
*
|
|
122
|
+
* Everything the scalar text carries, this one carries too, because objectui#9031
|
|
123
|
+
* ruled BOTH halves load-bearing and a reader reaching this message is being
|
|
124
|
+
* corrected in exactly the same moment: the worked array-of-objects example, the
|
|
125
|
+
* fact that `order` is REQUIRED at publish, and the fact that this renderer's
|
|
126
|
+
* ascending default is a RUNTIME tolerance and ⛔ not permission to omit the key.
|
|
127
|
+
*
|
|
128
|
+
* The caller withholds exactly one string from this reporter — `''` — because
|
|
129
|
+
* `sort: ''` does not reach the scalar reporter either (the `!sort` guard reads
|
|
130
|
+
* an empty spelling as "the author wrote nothing", and its pin says reporting it
|
|
131
|
+
* would train readers to ignore the message). Mirroring that is the point of the
|
|
132
|
+
* whole card; inverting it one container deeper would recreate the same
|
|
133
|
+
* inconsistency in the other direction.
|
|
134
|
+
*
|
|
135
|
+
* `console.error`, unconditional and not dev-gated — the same choice, for the
|
|
136
|
+
* same reason, as its scalar sibling: this refuses an authored row order, so the
|
|
137
|
+
* page renders in an order the author did not ask for. ⛔ Not the
|
|
138
|
+
* `warnOnUnknownActionKeys` class, which reports keys nothing was ever going to
|
|
139
|
+
* read; here something WAS going to be read and now is not.
|
|
140
|
+
*/
|
|
141
|
+
function reportRetiredSortEntrySpelling(entry) {
|
|
142
|
+
const key = retiredKey('entry', entry);
|
|
143
|
+
if (reportedRetiredSpellings.has(key))
|
|
144
|
+
return;
|
|
145
|
+
reportedRetiredSpellings.add(key);
|
|
146
|
+
console.error(`[object-ui] convertSortToQueryParams: a \`sort\` ARRAY ENTRY is the retired string ` +
|
|
147
|
+
`clause (objectui#8221) and was REFUSED — received ${JSON.stringify(entry)} as an entry ` +
|
|
148
|
+
`of the \`sort\` array, so that entry orders nothing (and if it was the only usable ` +
|
|
149
|
+
`entry, this query carries no \`$orderby\` at all). The retirement is about the ` +
|
|
150
|
+
`SPELLING, not the container: wrapping the clause in an array does not revive it. ` +
|
|
151
|
+
`Write entry OBJECTS instead: sort: ${ARRAY_FORM_EXAMPLE} — both keys, on every entry. ` +
|
|
152
|
+
`\`order\` is required: on \`SortConfig\`, on its zod counterpart, and on ` +
|
|
153
|
+
`\`@objectstack/spec\`'s \`SortItemSchema\`, which refuses an entry without it. ` +
|
|
154
|
+
`(A missing \`order\` is still read as \`'asc'\` here — that is a runtime tolerance, ` +
|
|
155
|
+
`not permission to omit the key: metadata written that way is refused when it is ` +
|
|
156
|
+
`published.) The array OF OBJECTS is the only spelling every \`sort\` input declares, ` +
|
|
157
|
+
`and the only one \`@objectstack/spec\` accepts.`);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Decide WHICH authored entries survive and WHAT a missing direction means —
|
|
161
|
+
* the whole of this module's normalization, with no opinion about the wire.
|
|
162
|
+
*
|
|
163
|
+
* Split out of {@link convertSortToQueryParams} by objectui#8973, which measured
|
|
164
|
+
* `object-grid` re-implementing this decision privately and getting it wrong:
|
|
165
|
+
* its array arm interpolated every key unconditionally, so an entry missing
|
|
166
|
+
* `field` or `order` reached the wire as the literal text `undefined`
|
|
167
|
+
* (`$orderby: 'name undefined'`), which the server answers `400 INVALID_QUERY`.
|
|
168
|
+
*
|
|
169
|
+
* ⚠️ It is SHAPE-AGNOSTIC on purpose, and that is what makes it reusable where
|
|
170
|
+
* the map below is not. `object-grid` sends a `"field order"` join string, not
|
|
171
|
+
* this module's `{field: direction}` map; swapping its wire shape is route B on
|
|
172
|
+
* objectui#8767, which the maintainer DECLINED by name (2026-09-10) pending a
|
|
173
|
+
* card that measures the server contract and both readers. Exporting the
|
|
174
|
+
* decision without the map lets that block share the ONE implementation of the
|
|
175
|
+
* rule while keeping the wire shape the declination protects — so "one
|
|
176
|
+
* operation, one implementation" is satisfied for the operation that actually
|
|
177
|
+
* has two copies, rather than being traded away for a shape change nobody
|
|
178
|
+
* ruled on.
|
|
179
|
+
*
|
|
180
|
+
* The two rules, unchanged from the map builder they were lifted out of:
|
|
181
|
+
*
|
|
182
|
+
* - an entry with no usable `field` is SKIPPED (it names nothing to order by);
|
|
183
|
+
* - `order` normalizes to `'asc'` unless it is exactly `'desc'`, so a missing
|
|
184
|
+
* direction means ascending and a garbage one does not reach the wire.
|
|
185
|
+
*
|
|
186
|
+
* A third rule was added by objectui#9955 and it changes no RETURN value: a
|
|
187
|
+
* STRING entry is the retired clause one container deeper, so it is now REFUSED
|
|
188
|
+
* OUT LOUD before the skip above reaches it — and then skipped, contributing
|
|
189
|
+
* nothing, exactly as before. ⛔ The two rules above are untouched by it; in
|
|
190
|
+
* particular the entry-shaped object with no `field` keeps its DELIBERATE
|
|
191
|
+
* silence, because it names nothing to order by rather than naming it in a
|
|
192
|
+
* retired spelling. The seam is `typeof entry === 'string'` and only that.
|
|
193
|
+
*
|
|
194
|
+
* @returns The surviving entries in authored order, or `undefined` when nothing
|
|
195
|
+
* orderable was authored — never an empty array, so callers can omit the
|
|
196
|
+
* query key entirely rather than asking for an ordering with no content.
|
|
197
|
+
*/
|
|
198
|
+
export function normalizeSortEntries(sort) {
|
|
16
199
|
if (!sort)
|
|
17
200
|
return undefined;
|
|
18
|
-
//
|
|
201
|
+
// Retired spelling — reachable only at runtime, since the signature above no
|
|
202
|
+
// longer admits it. Read through `unknown` on purpose: the check is about the
|
|
203
|
+
// VALUE that actually arrived, not about the type the caller promised.
|
|
19
204
|
if (typeof sort === 'string') {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return undefined;
|
|
23
|
-
return { [field]: direction?.toLowerCase() === 'desc' ? 'desc' : 'asc' };
|
|
205
|
+
reportRetiredSortSpelling(sort);
|
|
206
|
+
return undefined;
|
|
24
207
|
}
|
|
25
208
|
if (Array.isArray(sort)) {
|
|
26
|
-
const out =
|
|
209
|
+
const out = [];
|
|
27
210
|
for (const entry of sort) {
|
|
211
|
+
// The retired spelling, one container deeper (objectui#9955). Read through
|
|
212
|
+
// `unknown` for the same reason the scalar arm above does: the check is
|
|
213
|
+
// about the VALUE that arrived, not the type the caller promised. ⛔ This
|
|
214
|
+
// branch does not ACCEPT the string — the entry still contributes nothing,
|
|
215
|
+
// exactly as it did when it fell through the skip below. All it adds is the
|
|
216
|
+
// sentence that says so.
|
|
217
|
+
//
|
|
218
|
+
// The one string it does not name is `''`, and that is SYMMETRY with the
|
|
219
|
+
// scalar arm rather than an exception to the seam: `sort: ''` never reaches
|
|
220
|
+
// the loud arm above either, because the `!sort` guard reads it as "the
|
|
221
|
+
// author wrote nothing" rather than as the retired clause. Whitespace is
|
|
222
|
+
// NOT in that carve-out — `' '` is truthy, is loud on the scalar arm
|
|
223
|
+
// today, and stays loud here.
|
|
224
|
+
if (typeof entry === 'string') {
|
|
225
|
+
const spelling = entry;
|
|
226
|
+
if (spelling !== '')
|
|
227
|
+
reportRetiredSortEntrySpelling(spelling);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
28
230
|
if (!entry || typeof entry.field !== 'string' || entry.field === '')
|
|
29
231
|
continue;
|
|
30
|
-
out
|
|
232
|
+
out.push({ field: entry.field, order: entry.order === 'desc' ? 'desc' : 'asc' });
|
|
31
233
|
}
|
|
32
|
-
return
|
|
234
|
+
return out.length > 0 ? out : undefined;
|
|
33
235
|
}
|
|
34
236
|
return undefined;
|
|
35
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Normalize an authored `sort` into the field→direction map used for
|
|
240
|
+
* `QueryParams.$orderby`.
|
|
241
|
+
*
|
|
242
|
+
* The decision about which entries survive lives in
|
|
243
|
+
* {@link normalizeSortEntries}; this function is only the MAP projection of it.
|
|
244
|
+
* Duplicate fields therefore collapse last-wins, keeping the position of the
|
|
245
|
+
* first mention — the same thing the single-pass loop this replaced did, since
|
|
246
|
+
* an object key keeps its insertion position when it is re-assigned.
|
|
247
|
+
*
|
|
248
|
+
* @param sort `SortConfig[]` — the one declared spelling. The legacy string
|
|
249
|
+
* clause (`"name desc"`) is retired (objectui#8221): a string that reaches here
|
|
250
|
+
* at runtime is refused with a diagnostic naming the array form, and this
|
|
251
|
+
* function returns `undefined` so the query carries no `$orderby`.
|
|
252
|
+
* @returns The ordering map, or `undefined` when nothing orderable was authored.
|
|
253
|
+
*/
|
|
254
|
+
export function convertSortToQueryParams(sort) {
|
|
255
|
+
const entries = normalizeSortEntries(sort);
|
|
256
|
+
if (!entries)
|
|
257
|
+
return undefined;
|
|
258
|
+
const out = {};
|
|
259
|
+
for (const entry of entries)
|
|
260
|
+
out[entry.field] = entry.order;
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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 ONE `icontains` comparand ruling this package owns, in one place
|
|
10
|
+
* (objectui#9048).
|
|
11
|
+
*
|
|
12
|
+
* ## Why this module exists rather than a third copy
|
|
13
|
+
*
|
|
14
|
+
* `@objectstack/spec`'s `FILTER_TEXT_CASES` carries two REJECTION rows for the
|
|
15
|
+
* case-insensitive contains operator — an empty comparand and a non-string one
|
|
16
|
+
* — each with `code: 'INVALID_FILTER'` and `mustMention: ['$icontains']`.
|
|
17
|
+
* Before this module the SAME discrimination and the SAME message were written
|
|
18
|
+
* out twice inside this package: `ValueDataSource`'s `refuseTextComparand`
|
|
19
|
+
* (objectui#8748, the reference implementation) and `filter-converter`'s
|
|
20
|
+
* (objectui#9001, whose own docblock calls itself "a PORT of a shipped,
|
|
21
|
+
* reviewed implementation"). objectui#9048's triage ruled on that directly:
|
|
22
|
+
*
|
|
23
|
+
* > three implementations of one refusal, of which one is correct ⇒ the useful
|
|
24
|
+
* > dispatch is not "add a third guard" but "make the dialects share the one
|
|
25
|
+
* > that works"
|
|
26
|
+
*
|
|
27
|
+
* So the contract's half — WHICH comparands are refused, and WHAT the author is
|
|
28
|
+
* told — lives here once, and each face keeps only its own ENVELOPE. That split
|
|
29
|
+
* is not a tidy-up: it is the half that does and the half that does not
|
|
30
|
+
* transfer, and both docblocks already said so before this module existed.
|
|
31
|
+
* `ValueDataSource` excludes-and-logs because it is deciding about one ROW and
|
|
32
|
+
* HAS a row to exclude; `filter-converter` is a PRODUCER deciding whether to
|
|
33
|
+
* send a query at all and has none, so it throws `FilterOperatorError`
|
|
34
|
+
* (`INVALID_FILTER` / 400).
|
|
35
|
+
*
|
|
36
|
+
* ## Not exported from `@object-ui/core`'s entry, deliberately
|
|
37
|
+
*
|
|
38
|
+
* `src/index.ts` names its re-exports one file at a time and does NOT name this
|
|
39
|
+
* one, so nothing here reaches the published surface. The package's sibling
|
|
40
|
+
* `@object-ui/data-objectstack` carries the same defect on its own ARRAY-form
|
|
41
|
+
* producer and CANNOT reach this module — `@object-ui/core`'s `exports` map has
|
|
42
|
+
* exactly one entry (`"."`), so a second package can only import what
|
|
43
|
+
* `src/index.ts` re-exports. Closing that half is a published-surface addition
|
|
44
|
+
* and a maintainer's call, not this module's; see objectui#9048 for the exact
|
|
45
|
+
* export it would need.
|
|
46
|
+
*
|
|
47
|
+
* ## Scope
|
|
48
|
+
*
|
|
49
|
+
* Only the case-insensitive contains operator, because only that operator is
|
|
50
|
+
* what the published table declares. The sibling positive operators
|
|
51
|
+
* (`$contains` / `$startsWith` / `$endsWith`) have no such row and keep the
|
|
52
|
+
* answer they have always given on every face — widening by analogy is the
|
|
53
|
+
* table's decision, not this module's.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* A comparand as it appears INSIDE a refusal message.
|
|
57
|
+
*
|
|
58
|
+
* `JSON.stringify` alone is not safe here even though it is what the message
|
|
59
|
+
* wants: it THROWS on a BigInt and on a cyclic object. Both faces are hurt by
|
|
60
|
+
* that and in mirror-image ways, which is why the guard travels with the text
|
|
61
|
+
* rather than being re-derived per face. On the THROWING face the call sits
|
|
62
|
+
* inside a `throw new FilterOperatorError(...)` expression, so a `TypeError`
|
|
63
|
+
* raised while the message is being built escapes in the refusal's place, and
|
|
64
|
+
* `classifyLoadError` reads a bare `TypeError` as a network fault: the author
|
|
65
|
+
* would be told to check their connection about a filter this layer had already
|
|
66
|
+
* judged. On the EXCLUDE-AND-LOG face a refusal that throws while explaining
|
|
67
|
+
* itself turns the one path that stays quiet about a bad filter into the one
|
|
68
|
+
* path that takes the caller down.
|
|
69
|
+
*
|
|
70
|
+
* No JSON-sourced filter can carry either shape, so this is about the in-memory
|
|
71
|
+
* callers who hand a literal to a data source or to the converter.
|
|
72
|
+
* `?? String(target)` keeps `undefined` and a symbol readable — `JSON.stringify`
|
|
73
|
+
* returns `undefined` for both.
|
|
74
|
+
*/
|
|
75
|
+
export declare function describeComparand(target: unknown): string;
|
|
76
|
+
/**
|
|
77
|
+
* THE CONTRACT HALF, now the spec's own (objectui#11073).
|
|
78
|
+
*
|
|
79
|
+
* `isRefusedTextComparand` (the discrimination: a non-string or empty
|
|
80
|
+
* comparand) and `textComparandRefusalReason` (the half of the message the
|
|
81
|
+
* contract owns, no face's envelope on it) were written here once, for both
|
|
82
|
+
* faces. `@objectstack/spec` 17.5.0 publishes the SAME two functions from
|
|
83
|
+
* `@objectstack/spec/data`, byte for byte the implementation this module carried
|
|
84
|
+
* — measured against the published `dist` when the bump landed, message text
|
|
85
|
+
* included. So they are re-exported rather than kept as a copy under the spec's
|
|
86
|
+
* own names, which `pnpm check:spec-symbols` refuses (a hand-written copy under
|
|
87
|
+
* a spec name is one release away from drifting). Both faces still import them
|
|
88
|
+
* from here, so the ONE-place property this module exists for is unchanged; the
|
|
89
|
+
* one place is now the spec. `describeComparand` above stays local: the spec
|
|
90
|
+
* does not export it.
|
|
91
|
+
*/
|
|
92
|
+
export { isRefusedTextComparand, textComparandRefusalReason } from '@objectstack/spec/data';
|
|
@@ -0,0 +1,99 @@
|
|
|
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 ONE `icontains` comparand ruling this package owns, in one place
|
|
10
|
+
* (objectui#9048).
|
|
11
|
+
*
|
|
12
|
+
* ## Why this module exists rather than a third copy
|
|
13
|
+
*
|
|
14
|
+
* `@objectstack/spec`'s `FILTER_TEXT_CASES` carries two REJECTION rows for the
|
|
15
|
+
* case-insensitive contains operator — an empty comparand and a non-string one
|
|
16
|
+
* — each with `code: 'INVALID_FILTER'` and `mustMention: ['$icontains']`.
|
|
17
|
+
* Before this module the SAME discrimination and the SAME message were written
|
|
18
|
+
* out twice inside this package: `ValueDataSource`'s `refuseTextComparand`
|
|
19
|
+
* (objectui#8748, the reference implementation) and `filter-converter`'s
|
|
20
|
+
* (objectui#9001, whose own docblock calls itself "a PORT of a shipped,
|
|
21
|
+
* reviewed implementation"). objectui#9048's triage ruled on that directly:
|
|
22
|
+
*
|
|
23
|
+
* > three implementations of one refusal, of which one is correct ⇒ the useful
|
|
24
|
+
* > dispatch is not "add a third guard" but "make the dialects share the one
|
|
25
|
+
* > that works"
|
|
26
|
+
*
|
|
27
|
+
* So the contract's half — WHICH comparands are refused, and WHAT the author is
|
|
28
|
+
* told — lives here once, and each face keeps only its own ENVELOPE. That split
|
|
29
|
+
* is not a tidy-up: it is the half that does and the half that does not
|
|
30
|
+
* transfer, and both docblocks already said so before this module existed.
|
|
31
|
+
* `ValueDataSource` excludes-and-logs because it is deciding about one ROW and
|
|
32
|
+
* HAS a row to exclude; `filter-converter` is a PRODUCER deciding whether to
|
|
33
|
+
* send a query at all and has none, so it throws `FilterOperatorError`
|
|
34
|
+
* (`INVALID_FILTER` / 400).
|
|
35
|
+
*
|
|
36
|
+
* ## Not exported from `@object-ui/core`'s entry, deliberately
|
|
37
|
+
*
|
|
38
|
+
* `src/index.ts` names its re-exports one file at a time and does NOT name this
|
|
39
|
+
* one, so nothing here reaches the published surface. The package's sibling
|
|
40
|
+
* `@object-ui/data-objectstack` carries the same defect on its own ARRAY-form
|
|
41
|
+
* producer and CANNOT reach this module — `@object-ui/core`'s `exports` map has
|
|
42
|
+
* exactly one entry (`"."`), so a second package can only import what
|
|
43
|
+
* `src/index.ts` re-exports. Closing that half is a published-surface addition
|
|
44
|
+
* and a maintainer's call, not this module's; see objectui#9048 for the exact
|
|
45
|
+
* export it would need.
|
|
46
|
+
*
|
|
47
|
+
* ## Scope
|
|
48
|
+
*
|
|
49
|
+
* Only the case-insensitive contains operator, because only that operator is
|
|
50
|
+
* what the published table declares. The sibling positive operators
|
|
51
|
+
* (`$contains` / `$startsWith` / `$endsWith`) have no such row and keep the
|
|
52
|
+
* answer they have always given on every face — widening by analogy is the
|
|
53
|
+
* table's decision, not this module's.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* A comparand as it appears INSIDE a refusal message.
|
|
57
|
+
*
|
|
58
|
+
* `JSON.stringify` alone is not safe here even though it is what the message
|
|
59
|
+
* wants: it THROWS on a BigInt and on a cyclic object. Both faces are hurt by
|
|
60
|
+
* that and in mirror-image ways, which is why the guard travels with the text
|
|
61
|
+
* rather than being re-derived per face. On the THROWING face the call sits
|
|
62
|
+
* inside a `throw new FilterOperatorError(...)` expression, so a `TypeError`
|
|
63
|
+
* raised while the message is being built escapes in the refusal's place, and
|
|
64
|
+
* `classifyLoadError` reads a bare `TypeError` as a network fault: the author
|
|
65
|
+
* would be told to check their connection about a filter this layer had already
|
|
66
|
+
* judged. On the EXCLUDE-AND-LOG face a refusal that throws while explaining
|
|
67
|
+
* itself turns the one path that stays quiet about a bad filter into the one
|
|
68
|
+
* path that takes the caller down.
|
|
69
|
+
*
|
|
70
|
+
* No JSON-sourced filter can carry either shape, so this is about the in-memory
|
|
71
|
+
* callers who hand a literal to a data source or to the converter.
|
|
72
|
+
* `?? String(target)` keeps `undefined` and a symbol readable — `JSON.stringify`
|
|
73
|
+
* returns `undefined` for both.
|
|
74
|
+
*/
|
|
75
|
+
export function describeComparand(target) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(target) ?? String(target);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return String(target);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* THE CONTRACT HALF, now the spec's own (objectui#11073).
|
|
85
|
+
*
|
|
86
|
+
* `isRefusedTextComparand` (the discrimination: a non-string or empty
|
|
87
|
+
* comparand) and `textComparandRefusalReason` (the half of the message the
|
|
88
|
+
* contract owns, no face's envelope on it) were written here once, for both
|
|
89
|
+
* faces. `@objectstack/spec` 17.5.0 publishes the SAME two functions from
|
|
90
|
+
* `@objectstack/spec/data`, byte for byte the implementation this module carried
|
|
91
|
+
* — measured against the published `dist` when the bump landed, message text
|
|
92
|
+
* included. So they are re-exported rather than kept as a copy under the spec's
|
|
93
|
+
* own names, which `pnpm check:spec-symbols` refuses (a hand-written copy under
|
|
94
|
+
* a spec name is one release away from drifting). Both faces still import them
|
|
95
|
+
* from here, so the ONE-place property this module exists for is unchanged; the
|
|
96
|
+
* one place is now the spec. `describeComparand` above stays local: the spec
|
|
97
|
+
* does not export it.
|
|
98
|
+
*/
|
|
99
|
+
export { isRefusedTextComparand, textComparandRefusalReason } from '@objectstack/spec/data';
|