@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +7404 -0
  2. package/README.md +127 -26
  3. package/dist/actions/ActionEngine.d.ts +5 -3
  4. package/dist/actions/ActionEngine.js +14 -10
  5. package/dist/actions/ActionRunner.d.ts +363 -30
  6. package/dist/actions/ActionRunner.js +544 -58
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/actionKeys.d.ts +75 -13
  9. package/dist/actions/actionKeys.js +131 -16
  10. package/dist/actions/actionResponse.js +3 -3
  11. package/dist/actions/index.d.ts +2 -0
  12. package/dist/actions/index.js +2 -0
  13. package/dist/actions/recordDelete.d.ts +118 -0
  14. package/dist/actions/recordDelete.js +112 -0
  15. package/dist/actions/recordIdParam.d.ts +77 -0
  16. package/dist/actions/recordIdParam.js +37 -0
  17. package/dist/actions/serverActionHandler.d.ts +1 -1
  18. package/dist/actions/serverActionHandler.js +1 -1
  19. package/dist/adapters/ValueDataSource.d.ts +6 -1
  20. package/dist/adapters/ValueDataSource.js +1090 -98
  21. package/dist/adapters/batchTransaction.d.ts +1 -1
  22. package/dist/adapters/batchTransaction.js +1 -1
  23. package/dist/builder/schema-builder.d.ts +38 -81
  24. package/dist/builder/schema-builder.js +33 -152
  25. package/dist/data-scope/ViewDataProvider.d.ts +1 -1
  26. package/dist/data-scope/ViewDataProvider.js +21 -1
  27. package/dist/data-scope/element-data-source.d.ts +261 -7
  28. package/dist/data-scope/element-data-source.js +342 -52
  29. package/dist/data-scope/index.d.ts +9 -4
  30. package/dist/data-scope/index.js +9 -4
  31. package/dist/evaluator/ExpressionEvaluator.d.ts +128 -1
  32. package/dist/evaluator/ExpressionEvaluator.js +226 -13
  33. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  34. package/dist/evaluator/FormulaFunctions.js +166 -27
  35. package/dist/evaluator/SafeExpressionParser.js +0 -4
  36. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  37. package/dist/evaluator/declaredPredicate.js +118 -17
  38. package/dist/evaluator/fieldRules.d.ts +107 -4
  39. package/dist/evaluator/fieldRules.js +231 -35
  40. package/dist/evaluator/index.d.ts +2 -0
  41. package/dist/evaluator/index.js +2 -0
  42. package/dist/evaluator/listConditional.d.ts +125 -9
  43. package/dist/evaluator/listConditional.js +167 -26
  44. package/dist/evaluator/optionRules.d.ts +126 -0
  45. package/dist/evaluator/optionRules.js +141 -4
  46. package/dist/evaluator/predicateInput.d.ts +15 -4
  47. package/dist/evaluator/predicateInput.js +35 -2
  48. package/dist/evaluator/rowPredicateCanon.d.ts +56 -0
  49. package/dist/evaluator/rowPredicateCanon.js +158 -0
  50. package/dist/evaluator/subjectPermissions.d.ts +76 -0
  51. package/dist/evaluator/subjectPermissions.js +44 -0
  52. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  53. package/dist/evaluator/unevaluablePredicate.js +70 -0
  54. package/dist/index.d.ts +18 -0
  55. package/dist/index.js +97 -2
  56. package/dist/protocols/index.d.ts +0 -1
  57. package/dist/protocols/index.js +9 -1
  58. package/dist/registry/Registry.d.ts +397 -48
  59. package/dist/registry/Registry.js +263 -11
  60. package/dist/registry/WidgetRegistry.js +7 -3
  61. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  62. package/dist/registry/html-tier-intrinsics.js +110 -0
  63. package/dist/registry/public-blocks.d.ts +8 -0
  64. package/dist/registry/public-blocks.js +57 -0
  65. package/dist/theme/ThemeEngine.js +1 -1
  66. package/dist/theme/index.d.ts +1 -1
  67. package/dist/theme/index.js +1 -1
  68. package/dist/types/index.d.ts +92 -13
  69. package/dist/utils/chart-category-key.d.ts +82 -0
  70. package/dist/utils/chart-category-key.js +84 -0
  71. package/dist/utils/chart-measure-key.d.ts +64 -0
  72. package/dist/utils/chart-measure-key.js +65 -0
  73. package/dist/utils/chart-presentation.d.ts +188 -0
  74. package/dist/utils/chart-presentation.js +345 -0
  75. package/dist/utils/chart-series.d.ts +271 -31
  76. package/dist/utils/chart-series.js +478 -55
  77. package/dist/utils/column-identity.d.ts +43 -0
  78. package/dist/utils/column-identity.js +47 -1
  79. package/dist/utils/column-sortability.d.ts +157 -0
  80. package/dist/utils/column-sortability.js +217 -0
  81. package/dist/utils/dashboard-filters.d.ts +38 -6
  82. package/dist/utils/dashboard-filters.js +107 -61
  83. package/dist/utils/dataset-format.d.ts +211 -30
  84. package/dist/utils/dataset-format.js +407 -52
  85. package/dist/utils/date-display.d.ts +354 -0
  86. package/dist/utils/date-display.js +463 -0
  87. package/dist/utils/date-macros.js +7 -0
  88. package/dist/utils/debug.js +7 -0
  89. package/dist/utils/drill-down.d.ts +78 -0
  90. package/dist/utils/drill-down.js +93 -0
  91. package/dist/utils/emptiness.d.ts +96 -0
  92. package/dist/utils/emptiness.js +101 -0
  93. package/dist/utils/expand-fields.d.ts +171 -11
  94. package/dist/utils/expand-fields.js +171 -11
  95. package/dist/utils/extract-records.d.ts +65 -5
  96. package/dist/utils/extract-records.js +65 -8
  97. package/dist/utils/filter-converter.d.ts +152 -4
  98. package/dist/utils/filter-converter.js +1092 -22
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -34
  101. package/dist/utils/freeze-schema.d.ts +38 -1
  102. package/dist/utils/freeze-schema.js +10 -0
  103. package/dist/utils/grouping-fields.d.ts +61 -0
  104. package/dist/utils/grouping-fields.js +84 -0
  105. package/dist/utils/humanize-label.d.ts +60 -0
  106. package/dist/utils/humanize-label.js +62 -0
  107. package/dist/utils/managedBy.d.ts +30 -5
  108. package/dist/utils/managedBy.js +5 -5
  109. package/dist/utils/native-date-value.d.ts +48 -0
  110. package/dist/utils/native-date-value.js +167 -0
  111. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  112. package/dist/utils/non-grid-row-ceiling.js +141 -0
  113. package/dist/utils/normalize-list-view.d.ts +131 -1
  114. package/dist/utils/normalize-list-view.js +248 -6
  115. package/dist/utils/number-display.d.ts +70 -17
  116. package/dist/utils/number-display.js +34 -12
  117. package/dist/utils/object-aggregate-query.d.ts +113 -0
  118. package/dist/utils/object-aggregate-query.js +50 -0
  119. package/dist/utils/parent-scope.d.ts +123 -0
  120. package/dist/utils/parent-scope.js +140 -0
  121. package/dist/utils/predicate-fields.d.ts +62 -4
  122. package/dist/utils/predicate-fields.js +169 -4
  123. package/dist/utils/record-color.d.ts +75 -0
  124. package/dist/utils/record-color.js +65 -0
  125. package/dist/utils/record-source.d.ts +298 -0
  126. package/dist/utils/record-source.js +331 -0
  127. package/dist/utils/record-title.d.ts +148 -8
  128. package/dist/utils/record-title.js +220 -26
  129. package/dist/utils/reference-keys.d.ts +21 -22
  130. package/dist/utils/reference-keys.js +532 -29
  131. package/dist/utils/retired-field-types.d.ts +90 -0
  132. package/dist/utils/retired-field-types.js +124 -0
  133. package/dist/utils/sort-query.d.ts +128 -26
  134. package/dist/utils/sort-query.js +240 -13
  135. package/dist/utils/text-comparand.d.ts +92 -0
  136. package/dist/utils/text-comparand.js +99 -0
  137. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  138. package/dist/utils/unmaterialized-fields.js +57 -0
  139. package/dist/utils/without-denied-fields.d.ts +49 -0
  140. package/dist/utils/without-denied-fields.js +52 -0
  141. package/dist/validation/index.d.ts +9 -4
  142. package/dist/validation/index.js +9 -4
  143. package/dist/validation/schema-validator.d.ts +1 -1
  144. package/dist/validation/schema-validator.js +305 -82
  145. package/dist/validation/server-owned-value.d.ts +62 -0
  146. package/dist/validation/server-owned-value.js +99 -0
  147. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  148. package/dist/validation/validators/object-validation-engine.js +14 -5
  149. package/package.json +8 -9
  150. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  151. package/dist/__benchmarks__/core.bench.js +0 -53
  152. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  153. package/dist/data-scope/DataScopeManager.js +0 -229
  154. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  155. package/dist/protocols/ResponsiveProtocol.js +0 -158
  156. package/dist/validation/validation-engine.d.ts +0 -88
  157. package/dist/validation/validation-engine.js +0 -434
@@ -6,30 +6,257 @@
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
8
  /**
9
- * Normalize an authored `sort` into the field→direction map used for
10
- * `QueryParams.$orderby`.
9
+ * The canonical spelling, quoted in the refusal diagnostic so the message
10
+ * carries the fix and not just the complaint.
11
11
  *
12
- * @param sort `"name desc"` (legacy string) or `SortConfig[]`.
13
- * @returns The ordering map, or `undefined` when nothing orderable was authored.
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
- export function convertSortToQueryParams(sort) {
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
- // Legacy string clause: "name desc" / "name" (a bare field means ascending).
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
- const [field, direction] = sort.trim().split(/\s+/);
21
- if (!field)
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[entry.field] = entry.order === 'desc' ? 'desc' : 'asc';
232
+ out.push({ field: entry.field, order: entry.order === 'desc' ? 'desc' : 'asc' });
31
233
  }
32
- return Object.keys(out).length > 0 ? out : undefined;
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';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * ObjectUI — unmaterialized-fields utility
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ /**
9
+ * Field types no driver materialises a stored column for — the value is
10
+ * computed on read (objectui#3950).
11
+ *
12
+ * A STORAGE fact, not a taste judgement, and it decides what the SERVER can be
13
+ * asked to do with such a field. All three query axes have now closed on it:
14
+ *
15
+ * - SORT — refused since objectstack#6994 (`400 INVALID_SORT`). Before that it
16
+ * degraded in silence: the response carried the very values it was asked to
17
+ * order by, out of order, under a `200`, with ascending and descending
18
+ * byte-identical (measured on a real SQL driver).
19
+ * - FILTER — refused since objectstack#8296 (`400 INVALID_FIELD`); previously
20
+ * `200` with zero rows whichever way the predicate pointed.
21
+ * - SEARCH — refused by name since objectstack#6674.
22
+ *
23
+ * The membership is bound to `@objectstack/spec` instead of being written out
24
+ * here, on the reasoning the platform's own filter door applies to itself: a
25
+ * layer that re-decided "which types have a column" drifts from the drivers,
26
+ * and then either withholds an affordance that works or offers one that 400s.
27
+ * The spec pins the set to `formula` alone.
28
+ *
29
+ * Deliberately NOT `COMPUTED_VALUE_TYPES` (`formula` / `summary` /
30
+ * `autonumber`) — that is the WRITE contract ("never client-written"). A
31
+ * `summary` and an `autonumber` each get a real maintained column and order,
32
+ * filter and search correctly, so widening to that set would withhold
33
+ * affordances from two types that work. The platform's own conformance suite
34
+ * pins that trap by name.
35
+ *
36
+ * SCOPE on the sort axis: this rules out a sort the SERVER performs. A
37
+ * client-side sort keys off the value the cell renders — which the server
38
+ * hydrates on read — so it orders by exactly what the user sees and stays
39
+ * honest. Consumers therefore apply this where they already apply the
40
+ * relational carve-out (see {@link EXPANDABLE_FIELD_TYPES}): under server
41
+ * sorting only.
42
+ */
43
+ export declare const UNMATERIALIZED_FIELD_TYPES: ReadonlySet<string>;
44
+ /**
45
+ * Whether a field definition's type materialises no stored column.
46
+ *
47
+ * Shaped like {@link isExpandableFieldType}: only `type` is read, so it answers
48
+ * for any field-metadata shape an object schema resolves to, and an unreadable
49
+ * definition is not a verdict — `null` / `undefined` / a bare type string all
50
+ * answer `false`.
51
+ */
52
+ export declare function isUnmaterializedFieldType(fieldDef: unknown): boolean;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * ObjectUI — unmaterialized-fields utility
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import { SEARCH_VIRTUAL_TYPES } from '@objectstack/spec/data';
9
+ /**
10
+ * Field types no driver materialises a stored column for — the value is
11
+ * computed on read (objectui#3950).
12
+ *
13
+ * A STORAGE fact, not a taste judgement, and it decides what the SERVER can be
14
+ * asked to do with such a field. All three query axes have now closed on it:
15
+ *
16
+ * - SORT — refused since objectstack#6994 (`400 INVALID_SORT`). Before that it
17
+ * degraded in silence: the response carried the very values it was asked to
18
+ * order by, out of order, under a `200`, with ascending and descending
19
+ * byte-identical (measured on a real SQL driver).
20
+ * - FILTER — refused since objectstack#8296 (`400 INVALID_FIELD`); previously
21
+ * `200` with zero rows whichever way the predicate pointed.
22
+ * - SEARCH — refused by name since objectstack#6674.
23
+ *
24
+ * The membership is bound to `@objectstack/spec` instead of being written out
25
+ * here, on the reasoning the platform's own filter door applies to itself: a
26
+ * layer that re-decided "which types have a column" drifts from the drivers,
27
+ * and then either withholds an affordance that works or offers one that 400s.
28
+ * The spec pins the set to `formula` alone.
29
+ *
30
+ * Deliberately NOT `COMPUTED_VALUE_TYPES` (`formula` / `summary` /
31
+ * `autonumber`) — that is the WRITE contract ("never client-written"). A
32
+ * `summary` and an `autonumber` each get a real maintained column and order,
33
+ * filter and search correctly, so widening to that set would withhold
34
+ * affordances from two types that work. The platform's own conformance suite
35
+ * pins that trap by name.
36
+ *
37
+ * SCOPE on the sort axis: this rules out a sort the SERVER performs. A
38
+ * client-side sort keys off the value the cell renders — which the server
39
+ * hydrates on read — so it orders by exactly what the user sees and stays
40
+ * honest. Consumers therefore apply this where they already apply the
41
+ * relational carve-out (see {@link EXPANDABLE_FIELD_TYPES}): under server
42
+ * sorting only.
43
+ */
44
+ export const UNMATERIALIZED_FIELD_TYPES = SEARCH_VIRTUAL_TYPES;
45
+ /**
46
+ * Whether a field definition's type materialises no stored column.
47
+ *
48
+ * Shaped like {@link isExpandableFieldType}: only `type` is read, so it answers
49
+ * for any field-metadata shape an object schema resolves to, and an unreadable
50
+ * definition is not a verdict — `null` / `undefined` / a bare type string all
51
+ * answer `false`.
52
+ */
53
+ export function isUnmaterializedFieldType(fieldDef) {
54
+ return (!!fieldDef &&
55
+ typeof fieldDef === 'object' &&
56
+ UNMATERIALIZED_FIELD_TYPES.has(fieldDef.type));
57
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * ObjectUI — the field-read rule for a single record
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 two members of a loaded permission policy the field-read rule asks.
10
+ *
11
+ * Structural on purpose: `usePermissions()` from `@object-ui/permissions`
12
+ * satisfies it as it is, and so does any caller-supplied policy of the same
13
+ * shape, so this package depends on neither React nor the permissions package.
14
+ */
15
+ interface FieldReadPolicy {
16
+ /** False until a policy has loaded; also false with no provider mounted. */
17
+ readonly isLoaded: boolean;
18
+ /** May the viewer READ `field` on `object`? */
19
+ checkField(object: string, field: string, action: 'read'): boolean;
20
+ }
21
+ /**
22
+ * `record` as the viewer may READ it on `objectName`: every field the loaded
23
+ * `policy` denies is removed, which leaves the row ObjectStack's `FieldMasker`
24
+ * already serves (objectui#10594).
25
+ *
26
+ * The renderer-side field-level-security rulings (objectui#7215 /
27
+ * objectui#7230) say FLS gates the OUTPUT: a value the loaded policy denies is
28
+ * not drawn, even when a backend serves it. Every surface that builds a display
29
+ * value from a whole row (a record title, a lookup label, a search-hit label, a
30
+ * person's name) calls this one function, so a denied field reads as an absent
31
+ * field on every surface at once, and the display ladder falls through to its
32
+ * next source exactly as it does for the stripped row. Field-LIST filters (which
33
+ * columns to head, which relations to `$expand`) are a different shape and are
34
+ * not this function.
35
+ *
36
+ * - `id` and `_id` are never judged, nor is any key in `extraKeep` (a declared
37
+ * id field): an id addresses the record and is not a field value the policy
38
+ * withholds, and the `Record #<id>` floor reads it.
39
+ * - Before a policy loads, with no policy, with no object to judge the fields
40
+ * against, or for a value that is not an object, `record` comes back as it
41
+ * is.
42
+ * - When nothing is withheld, the SAME object comes back, so a caller can tell
43
+ * "nothing withheld" from "something withheld" by identity and a cache keyed
44
+ * on the served row keeps hitting.
45
+ *
46
+ * Pure: it reads `policy.checkField` and never writes to `record`.
47
+ */
48
+ export declare function withoutDeniedFields<T>(record: T, policy: FieldReadPolicy | null | undefined, objectName: string | undefined, extraKeep?: readonly string[]): T;
49
+ export {};