@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
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ObjectUI — retired field-type spellings
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
+ * TOMBSTONE table — field-type spellings this renderer has RETIRED, mapped to
10
+ * the prescription an author must follow instead (ADR-0049 enforce-or-remove).
11
+ *
12
+ * A retired spelling is not merely absent: absence here means
13
+ * `mapFieldTypeToFormType`'s `|| 'field:text'` tail would hand back a
14
+ * working plain text input, which is the failure mode this table exists to
15
+ * prevent. An author who writes a retired name — or an AI author who copies one
16
+ * out of a stale doc — must be TOLD, not quietly given a text box that looks
17
+ * like it worked. So each entry resolves to a registered tombstone widget that
18
+ * renders a visible refusal naming the migration
19
+ * (`packages/fields/src/index.tsx`), while {@link reportRetiredFieldType}
20
+ * writes the same prescription to the console.
21
+ *
22
+ * `owner` (objectui#4814, ruling A′): a synonym for `user` with zero behavioral
23
+ * delta — both resolved to the SAME `UserField` widget — and absent from the
24
+ * spec's closed 48-member `FieldType`, so no object schema could ever declare
25
+ * it; it was reachable only through hand-written SDUI. Three code faces had
26
+ * already drifted apart on this one word (the form's data-source rule excluded
27
+ * it while plugin-grid's bulk dialog included it), which is the standing
28
+ * evidence that a second spelling for one concept is a drift channel, not a
29
+ * convenience. The idiom survives verbatim as `{ type: 'user', name: 'owner' }`.
30
+ *
31
+ * ## Why this table lives in `@object-ui/core` and not in `@object-ui/fields`
32
+ *
33
+ * It was born in `packages/fields/src/field-type-alias.ts`, next to the widget
34
+ * map that is its first consumer, and `@object-ui/fields` still exports every
35
+ * name in this module so no consumer of that surface had to move. It was HOISTED
36
+ * here by objectui#4914, and for a measured reason rather than a tidiness one:
37
+ * the retirement gate has to run ahead of six predicate faces, and one of them
38
+ * — `operatorsForFieldType` / the filter row's control choice in
39
+ * `packages/components/src/custom/filter-builder.tsx` — lives in
40
+ * `@object-ui/components`, which `@object-ui/fields` DEPENDS ON (54 source
41
+ * files import from it). A `components → fields` import is a package cycle, so
42
+ * the six faces could not all read one table while that table lived in
43
+ * `fields`.
44
+ *
45
+ * The alternative was a second copy of the table inside `components`, and that
46
+ * is precisely what this file exists to make impossible: two tables means two
47
+ * `reportedRetiredTypes` sets, so one retired spelling logs its prescription
48
+ * TWICE — and the "fires once" half of the ruling would be false the day it
49
+ * landed. `@object-ui/core` is below all six faces' packages (it imports only
50
+ * `@object-ui/types`), and already owns the sibling cross-package field-type
51
+ * vocabulary — {@link EXPANDABLE_FIELD_TYPES} in `./expand-fields.js`, whose
52
+ * own docblock records what happened the last time this kind of table was
53
+ * hand-copied per consumer. One table, one home, seven consumers.
54
+ */
55
+ export declare const RETIRED_FIELD_TYPES: Readonly<Record<string, string>>;
56
+ /**
57
+ * THE retirement gate — true when a spelling has been retired by this renderer.
58
+ *
59
+ * Ruled into existence by the maintainer on 2026-08-18 (objectui#4914, option
60
+ * B) as the ONE question every field-type predicate asks before it answers:
61
+ * "a retired spelling fires `reportRetiredFieldType` once and takes the
62
+ * established disposition, instead of riding the lookup-family paths or
63
+ * silently degrading."
64
+ *
65
+ * Quantified over the whole table rather than written per spelling, which is
66
+ * the entire point of having a gate at all: the next retirement closes every
67
+ * face the day it lands in {@link RETIRED_FIELD_TYPES}, instead of leaving each
68
+ * consumer open until someone notices it. `owner` survived objectui#4814 in
69
+ * thirteen more faces exactly because each one had hand-written its own
70
+ * membership list.
71
+ *
72
+ * `hasOwnProperty`, not `RETIRED_FIELD_TYPES[type]`: a `type` of
73
+ * `'constructor'` or `'toString'` reaches `Object.prototype` and would answer
74
+ * truthy through a plain index read.
75
+ *
76
+ * @param type - The spelling an author wrote (or a backend column declared).
77
+ * @returns `true` when the spelling is retired. Pure — it does NOT log; pair it
78
+ * with {@link reportRetiredFieldType} at the face that wants to be loud.
79
+ */
80
+ export declare function isRetiredFieldType(type: string | undefined | null): boolean;
81
+ /**
82
+ * Report a retired field-type spelling loudly, once per spelling.
83
+ *
84
+ * @param fieldType - The spelling the author wrote.
85
+ * @returns `true` when `fieldType` is retired (whether or not this call was the
86
+ * one that logged), so callers can branch on it without a second table lookup.
87
+ */
88
+ export declare function reportRetiredFieldType(fieldType: string): boolean;
89
+ /** Test seam — forget which spellings have been reported. */
90
+ export declare function resetRetiredFieldTypeReports(): void;
@@ -0,0 +1,124 @@
1
+ /**
2
+ * ObjectUI — retired field-type spellings
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
+ * TOMBSTONE table — field-type spellings this renderer has RETIRED, mapped to
10
+ * the prescription an author must follow instead (ADR-0049 enforce-or-remove).
11
+ *
12
+ * A retired spelling is not merely absent: absence here means
13
+ * `mapFieldTypeToFormType`'s `|| 'field:text'` tail would hand back a
14
+ * working plain text input, which is the failure mode this table exists to
15
+ * prevent. An author who writes a retired name — or an AI author who copies one
16
+ * out of a stale doc — must be TOLD, not quietly given a text box that looks
17
+ * like it worked. So each entry resolves to a registered tombstone widget that
18
+ * renders a visible refusal naming the migration
19
+ * (`packages/fields/src/index.tsx`), while {@link reportRetiredFieldType}
20
+ * writes the same prescription to the console.
21
+ *
22
+ * `owner` (objectui#4814, ruling A′): a synonym for `user` with zero behavioral
23
+ * delta — both resolved to the SAME `UserField` widget — and absent from the
24
+ * spec's closed 48-member `FieldType`, so no object schema could ever declare
25
+ * it; it was reachable only through hand-written SDUI. Three code faces had
26
+ * already drifted apart on this one word (the form's data-source rule excluded
27
+ * it while plugin-grid's bulk dialog included it), which is the standing
28
+ * evidence that a second spelling for one concept is a drift channel, not a
29
+ * convenience. The idiom survives verbatim as `{ type: 'user', name: 'owner' }`.
30
+ *
31
+ * ## Why this table lives in `@object-ui/core` and not in `@object-ui/fields`
32
+ *
33
+ * It was born in `packages/fields/src/field-type-alias.ts`, next to the widget
34
+ * map that is its first consumer, and `@object-ui/fields` still exports every
35
+ * name in this module so no consumer of that surface had to move. It was HOISTED
36
+ * here by objectui#4914, and for a measured reason rather than a tidiness one:
37
+ * the retirement gate has to run ahead of six predicate faces, and one of them
38
+ * — `operatorsForFieldType` / the filter row's control choice in
39
+ * `packages/components/src/custom/filter-builder.tsx` — lives in
40
+ * `@object-ui/components`, which `@object-ui/fields` DEPENDS ON (54 source
41
+ * files import from it). A `components → fields` import is a package cycle, so
42
+ * the six faces could not all read one table while that table lived in
43
+ * `fields`.
44
+ *
45
+ * The alternative was a second copy of the table inside `components`, and that
46
+ * is precisely what this file exists to make impossible: two tables means two
47
+ * `reportedRetiredTypes` sets, so one retired spelling logs its prescription
48
+ * TWICE — and the "fires once" half of the ruling would be false the day it
49
+ * landed. `@object-ui/core` is below all six faces' packages (it imports only
50
+ * `@object-ui/types`), and already owns the sibling cross-package field-type
51
+ * vocabulary — {@link EXPANDABLE_FIELD_TYPES} in `./expand-fields.js`, whose
52
+ * own docblock records what happened the last time this kind of table was
53
+ * hand-copied per consumer. One table, one home, seven consumers.
54
+ */
55
+ export const RETIRED_FIELD_TYPES = Object.freeze({
56
+ owner: "[object-ui] Field type `owner` was RETIRED (objectui#4814). It was a synonym " +
57
+ "for `user` with no behavioral difference, and it is not a member of " +
58
+ "`@objectstack/spec`'s FieldType. Write the record-owner field as " +
59
+ "`{ type: 'user', name: 'owner' }` — the field NAME carries the ownership " +
60
+ "meaning, the type carries the widget. The `widget: 'field:owner'` spelling " +
61
+ "is retired with it.",
62
+ });
63
+ /**
64
+ * THE retirement gate — true when a spelling has been retired by this renderer.
65
+ *
66
+ * Ruled into existence by the maintainer on 2026-08-18 (objectui#4914, option
67
+ * B) as the ONE question every field-type predicate asks before it answers:
68
+ * "a retired spelling fires `reportRetiredFieldType` once and takes the
69
+ * established disposition, instead of riding the lookup-family paths or
70
+ * silently degrading."
71
+ *
72
+ * Quantified over the whole table rather than written per spelling, which is
73
+ * the entire point of having a gate at all: the next retirement closes every
74
+ * face the day it lands in {@link RETIRED_FIELD_TYPES}, instead of leaving each
75
+ * consumer open until someone notices it. `owner` survived objectui#4814 in
76
+ * thirteen more faces exactly because each one had hand-written its own
77
+ * membership list.
78
+ *
79
+ * `hasOwnProperty`, not `RETIRED_FIELD_TYPES[type]`: a `type` of
80
+ * `'constructor'` or `'toString'` reaches `Object.prototype` and would answer
81
+ * truthy through a plain index read.
82
+ *
83
+ * @param type - The spelling an author wrote (or a backend column declared).
84
+ * @returns `true` when the spelling is retired. Pure — it does NOT log; pair it
85
+ * with {@link reportRetiredFieldType} at the face that wants to be loud.
86
+ */
87
+ export function isRetiredFieldType(type) {
88
+ if (typeof type !== 'string' || !type)
89
+ return false;
90
+ return Object.prototype.hasOwnProperty.call(RETIRED_FIELD_TYPES, type);
91
+ }
92
+ /**
93
+ * Spellings already reported this session, so a retired type inside a rendered
94
+ * list logs its prescription ONCE instead of once per row. The message is a
95
+ * fix instruction for an author, not a per-render event — a 1000-row grid
96
+ * repeating it 1000 times buries the very thing it is trying to surface.
97
+ *
98
+ * Module state, and that is load bearing now that SEVEN faces call the reporter
99
+ * (objectui#4914): the dedupe is per SPELLING, not per face, so a filter row
100
+ * whose column is refused by `operatorsForFieldType`, by the control chooser
101
+ * and by `normalizeFieldType` on the same render still prints one line.
102
+ */
103
+ const reportedRetiredTypes = new Set();
104
+ /**
105
+ * Report a retired field-type spelling loudly, once per spelling.
106
+ *
107
+ * @param fieldType - The spelling the author wrote.
108
+ * @returns `true` when `fieldType` is retired (whether or not this call was the
109
+ * one that logged), so callers can branch on it without a second table lookup.
110
+ */
111
+ export function reportRetiredFieldType(fieldType) {
112
+ const prescription = isRetiredFieldType(fieldType) ? RETIRED_FIELD_TYPES[fieldType] : undefined;
113
+ if (!prescription)
114
+ return false;
115
+ if (!reportedRetiredTypes.has(fieldType)) {
116
+ reportedRetiredTypes.add(fieldType);
117
+ console.error(prescription);
118
+ }
119
+ return true;
120
+ }
121
+ /** Test seam — forget which spellings have been reported. */
122
+ export function resetRetiredFieldTypeReports() {
123
+ reportedRetiredTypes.clear();
124
+ }
@@ -9,47 +9,149 @@
9
9
  * sort-query — lower a block's authored `sort` to the `$orderby` value a
10
10
  * `DataSource.find` query carries.
11
11
  *
12
- * Two authored spellings reach every object-bound block, because both are
13
- * declared: the legacy OData-ish string (`ObjectGridSchema.sort: "name desc"`)
14
- * and the spec's `SortConfig[]` (`[{ field, order }]`). A read site that hands
15
- * either straight to `$orderby` works only because `QueryParams.$orderby`
16
- * accepts four shapes; normalizing here means one shape reaches the wire and a
17
- * block does not have to know which spelling its author used.
18
- *
19
- * This is the same conversion three sibling blocks already inline privately
20
- * (`ObjectGantt` / `ObjectMap` / `ObjectCalendar`, byte-identical copies). It is
21
- * hoisted here because objectstack#7137 added two more read sites
22
- * (`object-timeline`, `record:line_items`), and a fifth and sixth private copy is
23
- * how the conversions start disagreeing. Collapsing the three existing copies
24
- * onto this one is deliberately NOT part of #7137 — it is tracked as
25
- * objectstack#7148.
26
- *
27
- * Two deliberate differences from those private copies, both of which make this
28
- * function more faithful to the declared contract rather than adding tolerance:
29
- *
30
- * - **`order` is optional in `SortConfig`**, so an entry that omits it means
31
- * ascending (that is what `$orderby`'s own
32
- * `Array<{ field: string; order?: 'asc' | 'desc' }>` shape says). The private
33
- * copies require BOTH keys and silently drop such an entry, which loses an
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.
27
+ *
28
+ * This is now the ONLY definition in the repo. It was hoisted here because
29
+ * objectstack#7137 added two more read sites (`object-timeline`,
30
+ * `record:line_items`) next to three sibling blocks that each inlined a
31
+ * byte-identical private copy (`ObjectGantt` / `ObjectMap` / `ObjectCalendar`),
32
+ * and a fifth and sixth copy is how the conversions start disagreeing. Those
33
+ * three copies were deliberately left alone by objectstack-ai/objectstack#7137 and collapsed onto this
34
+ * function by objectui#4022; every block now imports it from here.
35
+ *
36
+ * Two deliberate differences from those retired private copies, both of which
37
+ * make this function more faithful to the declared contract rather than adding
38
+ * tolerance — and both are BEHAVIOUR CHANGES the migration delivered, not pure
39
+ * refactor:
40
+ *
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
34
52
  * authored sort key instead of ordering by it.
35
53
  * - **Nothing usable yields `undefined`, never `{}`.** An empty object is a
36
54
  * truthy value that means "no ordering" only by accident of the adapter's
37
55
  * serializer; `undefined` says it, so the query simply carries no `$orderby`.
38
56
  *
39
57
  * Not a lenient alias: an unusable input (a number, an object, an array of
40
- * strings) yields `undefined` rather than a guess. Only the two spellings the
41
- * schema types declare are honoured.
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.
42
78
  */
43
79
  /** A field name paired with a direction — the spec's `SortConfig`. */
44
80
  export interface QuerySortEntry {
45
81
  field?: string;
46
82
  order?: 'asc' | 'desc';
47
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;
48
141
  /**
49
142
  * Normalize an authored `sort` into the field→direction map used for
50
143
  * `QueryParams.$orderby`.
51
144
  *
52
- * @param sort `"name desc"` (legacy string) or `SortConfig[]`.
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`.
53
155
  * @returns The ordering map, or `undefined` when nothing orderable was authored.
54
156
  */
55
- export declare function convertSortToQueryParams(sort: string | QuerySortEntry[] | undefined | null): Record<string, 'asc' | 'desc'> | undefined;
157
+ export declare function convertSortToQueryParams(sort: QuerySortEntry[] | undefined | null): Record<string, 'asc' | 'desc'> | undefined;