@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +7260 -0
  2. package/README.md +228 -14
  3. package/dist/actions/ActionEngine.d.ts +4 -2
  4. package/dist/actions/ActionEngine.js +13 -8
  5. package/dist/actions/ActionRunner.d.ts +473 -69
  6. package/dist/actions/ActionRunner.js +792 -125
  7. package/dist/actions/TransactionManager.d.ts +7 -2
  8. package/dist/actions/UndoManager.d.ts +8 -13
  9. package/dist/actions/actionKeys.d.ts +9 -7
  10. package/dist/actions/actionKeys.js +67 -14
  11. package/dist/actions/actionResponse.js +2 -2
  12. package/dist/actions/index.d.ts +2 -0
  13. package/dist/actions/index.js +2 -0
  14. package/dist/actions/navigationTarget.d.ts +41 -0
  15. package/dist/actions/navigationTarget.js +20 -0
  16. package/dist/actions/recordDelete.d.ts +154 -0
  17. package/dist/actions/recordDelete.js +110 -0
  18. package/dist/adapters/ApiDataSource.d.ts +8 -4
  19. package/dist/adapters/ApiDataSource.js +19 -7
  20. package/dist/adapters/ValueDataSource.d.ts +6 -1
  21. package/dist/adapters/ValueDataSource.js +1090 -98
  22. package/dist/adapters/batchTransaction.d.ts +1 -1
  23. package/dist/adapters/batchTransaction.js +1 -1
  24. package/dist/adapters/resolveDataSource.d.ts +9 -6
  25. package/dist/adapters/resolveDataSource.js +9 -12
  26. package/dist/builder/schema-builder.d.ts +38 -81
  27. package/dist/builder/schema-builder.js +33 -152
  28. package/dist/data-scope/ViewDataProvider.d.ts +8 -25
  29. package/dist/data-scope/ViewDataProvider.js +27 -54
  30. package/dist/data-scope/element-data-source.d.ts +261 -7
  31. package/dist/data-scope/element-data-source.js +342 -52
  32. package/dist/data-scope/index.d.ts +9 -4
  33. package/dist/data-scope/index.js +9 -4
  34. package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
  35. package/dist/evaluator/ExpressionEvaluator.js +184 -12
  36. package/dist/evaluator/FormulaFunctions.d.ts +0 -10
  37. package/dist/evaluator/FormulaFunctions.js +166 -27
  38. package/dist/evaluator/deadlineSettled.d.ts +40 -0
  39. package/dist/evaluator/deadlineSettled.js +45 -0
  40. package/dist/evaluator/declaredPredicate.d.ts +93 -9
  41. package/dist/evaluator/declaredPredicate.js +117 -16
  42. package/dist/evaluator/fieldRules.d.ts +88 -4
  43. package/dist/evaluator/fieldRules.js +208 -31
  44. package/dist/evaluator/index.d.ts +2 -0
  45. package/dist/evaluator/index.js +2 -0
  46. package/dist/evaluator/listConditional.d.ts +103 -34
  47. package/dist/evaluator/listConditional.js +127 -50
  48. package/dist/evaluator/optionRules.d.ts +77 -6
  49. package/dist/evaluator/optionRules.js +87 -10
  50. package/dist/evaluator/predicateInput.d.ts +15 -4
  51. package/dist/evaluator/predicateInput.js +35 -2
  52. package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
  53. package/dist/evaluator/rowPredicateCanon.js +65 -115
  54. package/dist/evaluator/subjectPermissions.d.ts +105 -0
  55. package/dist/evaluator/subjectPermissions.js +86 -0
  56. package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
  57. package/dist/evaluator/unevaluablePredicate.js +70 -0
  58. package/dist/index.d.ts +15 -0
  59. package/dist/index.js +83 -2
  60. package/dist/protocols/index.d.ts +0 -1
  61. package/dist/protocols/index.js +9 -1
  62. package/dist/registry/Registry.d.ts +382 -34
  63. package/dist/registry/Registry.js +247 -12
  64. package/dist/registry/WidgetRegistry.js +7 -3
  65. package/dist/registry/html-tier-intrinsics.d.ts +90 -0
  66. package/dist/registry/html-tier-intrinsics.js +110 -0
  67. package/dist/registry/public-blocks.d.ts +8 -0
  68. package/dist/registry/public-blocks.js +57 -0
  69. package/dist/theme/ThemeEngine.js +1 -1
  70. package/dist/utils/affordanceGrants.d.ts +371 -0
  71. package/dist/utils/affordanceGrants.js +252 -0
  72. package/dist/utils/chart-category-key.d.ts +82 -0
  73. package/dist/utils/chart-category-key.js +84 -0
  74. package/dist/utils/chart-measure-key.d.ts +64 -0
  75. package/dist/utils/chart-measure-key.js +65 -0
  76. package/dist/utils/chart-presentation.d.ts +95 -112
  77. package/dist/utils/chart-presentation.js +169 -104
  78. package/dist/utils/chart-series.d.ts +165 -5
  79. package/dist/utils/chart-series.js +123 -18
  80. package/dist/utils/column-sortability.d.ts +157 -0
  81. package/dist/utils/column-sortability.js +217 -0
  82. package/dist/utils/dashboard-filters.d.ts +38 -6
  83. package/dist/utils/dashboard-filters.js +107 -61
  84. package/dist/utils/dataset-format.d.ts +211 -30
  85. package/dist/utils/dataset-format.js +389 -46
  86. package/dist/utils/date-display.d.ts +346 -0
  87. package/dist/utils/date-display.js +644 -0
  88. package/dist/utils/date-macros.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 +81 -9
  94. package/dist/utils/expand-fields.js +81 -9
  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 +158 -6
  98. package/dist/utils/filter-converter.js +1098 -24
  99. package/dist/utils/filter-tokens.d.ts +49 -5
  100. package/dist/utils/filter-tokens.js +105 -33
  101. package/dist/utils/grouping-fields.d.ts +61 -0
  102. package/dist/utils/grouping-fields.js +84 -0
  103. package/dist/utils/managedBy.d.ts +3 -3
  104. package/dist/utils/managedBy.js +5 -5
  105. package/dist/utils/native-date-value.d.ts +48 -0
  106. package/dist/utils/native-date-value.js +167 -0
  107. package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
  108. package/dist/utils/non-grid-row-ceiling.js +141 -0
  109. package/dist/utils/normalize-list-view.d.ts +135 -11
  110. package/dist/utils/normalize-list-view.js +243 -64
  111. package/dist/utils/number-display.d.ts +63 -14
  112. package/dist/utils/number-display.js +34 -12
  113. package/dist/utils/object-aggregate-query.d.ts +113 -0
  114. package/dist/utils/object-aggregate-query.js +50 -0
  115. package/dist/utils/parent-scope.d.ts +123 -0
  116. package/dist/utils/parent-scope.js +140 -0
  117. package/dist/utils/predicate-fields.d.ts +45 -12
  118. package/dist/utils/predicate-fields.js +141 -12
  119. package/dist/utils/record-color.d.ts +75 -0
  120. package/dist/utils/record-color.js +65 -0
  121. package/dist/utils/record-source.d.ts +298 -0
  122. package/dist/utils/record-source.js +331 -0
  123. package/dist/utils/record-title.d.ts +148 -8
  124. package/dist/utils/record-title.js +220 -26
  125. package/dist/utils/reference-keys.d.ts +21 -22
  126. package/dist/utils/reference-keys.js +532 -29
  127. package/dist/utils/sort-query.d.ts +115 -15
  128. package/dist/utils/sort-query.js +240 -13
  129. package/dist/utils/text-comparand.d.ts +92 -0
  130. package/dist/utils/text-comparand.js +99 -0
  131. package/dist/utils/without-denied-fields.d.ts +49 -0
  132. package/dist/utils/without-denied-fields.js +52 -0
  133. package/dist/validation/index.d.ts +8 -4
  134. package/dist/validation/index.js +8 -4
  135. package/dist/validation/schema-validator.d.ts +1 -1
  136. package/dist/validation/schema-validator.js +225 -83
  137. package/dist/validation/server-owned-value.d.ts +5 -1
  138. package/dist/validation/server-owned-value.js +5 -1
  139. package/dist/validation/validators/object-validation-engine.d.ts +1 -1
  140. package/dist/validation/validators/object-validation-engine.js +14 -3
  141. package/package.json +8 -9
  142. package/dist/data-scope/DataScopeManager.d.ts +0 -127
  143. package/dist/data-scope/DataScopeManager.js +0 -229
  144. package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
  145. package/dist/protocols/ResponsiveProtocol.js +0 -158
  146. package/dist/validation/validation-engine.d.ts +0 -88
  147. package/dist/validation/validation-engine.js +0 -434
@@ -9,7 +9,51 @@ export interface ChartResultField {
9
9
  name: string;
10
10
  label?: string;
11
11
  format?: string;
12
+ /**
13
+ * The aggregate the measure column carries — the spec's
14
+ * `AnalyticsResult.fields[].aggregate`, which the dataset answer states on
15
+ * every measure column whose measure declares one, labelled or not
16
+ * (objectstack#21995). Absent on dimension columns and derived measures.
17
+ *
18
+ * Typed `string`, not the closed vocabulary, for the reason
19
+ * {@link BuiltinAggregateCarrier} gives: a JSON response is handed here
20
+ * as-is, so the value is judged at the read ({@link isIntegerAggregate}).
21
+ * {@link buildChartSeries} copies it onto each series it derives from the
22
+ * column, so a value axis can tell a count from a sum (objectui#11681).
23
+ */
24
+ aggregate?: string;
25
+ }
26
+ /**
27
+ * The OPTIONAL wire discriminator naming which server-side BUILT-IN default
28
+ * measure a result column is (objectui#7258; producer half objectstack#14492).
29
+ * The analytics service sets it ONLY on the measures it minted itself with a
30
+ * hard-coded English default label (`count` → `'Count'`, …); an author-declared
31
+ * measure never carries it, so its `label` stays the author's verbatim text
32
+ * (objectui#4106). {@link resolveMeasureLabel} is the one reader.
33
+ *
34
+ * Declared BESIDE {@link ChartResultField} rather than on it, deliberately:
35
+ * `dataset-result-field-spec-parity` pins every key of `ChartResultField` as a
36
+ * key of the spec's `AnalyticsResult.fields[]`, and the `@objectstack/spec`
37
+ * this package is built against does not carry `builtinAggregate` yet. The
38
+ * intersection {@link ChartMeasureField} is what the helpers accept, so a
39
+ * spec-typed field flows in unchanged today and carries the key the moment
40
+ * the spec does — structurally, with no cast at the call sites.
41
+ *
42
+ * Typed as `string` rather than the closed {@link BuiltinAggregate} union on
43
+ * purpose: a JSON response is handed here as-is, so the value is validated at
44
+ * the lookup ({@link isBuiltinAggregate}) instead of trusted at the type.
45
+ *
46
+ * TODO(objectstack#14492): once the spec dependency is bumped past the release
47
+ * that adds `AnalyticsResult.fields[].builtinAggregate`, fold this key into
48
+ * `ChartResultField` as `DatasetResultField['builtinAggregate']` (pinning the
49
+ * two closed vocabularies to each other in the parity suite) and retire the
50
+ * carrier.
51
+ */
52
+ export interface BuiltinAggregateCarrier {
53
+ builtinAggregate?: string;
12
54
  }
55
+ /** A result field as the series helpers read it: the renderer shape plus the optional discriminator. */
56
+ export type ChartMeasureField = ChartResultField & BuiltinAggregateCarrier;
13
57
  /**
14
58
  * One renderer-internal series binding produced by {@link buildChartSeries}:
15
59
  * WHICH result-set column to draw and what to call it.
@@ -25,12 +69,74 @@ export interface ChartResultField {
25
69
  export interface ChartSeriesBinding {
26
70
  dataKey: string;
27
71
  label: string;
72
+ /**
73
+ * The aggregate of the measure this series plots, copied from its result
74
+ * column's `aggregate` (objectui#11681). Renderer-internal: the chart's value
75
+ * axis draws whole-number ticks when every series on it plots an
76
+ * {@link isIntegerAggregate} measure. Absent when the column states none.
77
+ */
78
+ aggregate?: string;
28
79
  }
29
80
  export interface ChartSeriesResult {
30
81
  data: Array<Record<string, unknown>>;
31
82
  xAxisKey: string | undefined;
32
83
  series: ChartSeriesBinding[];
33
84
  }
85
+ /**
86
+ * The closed vocabulary of server-side built-in default measures a result field
87
+ * can declare itself as (objectui#7258 / objectstack#14492) — the spec's own
88
+ * measure-aggregate enum spelling (`count_distinct`, not `countDistinct`), so
89
+ * there is exactly ONE spelling on the wire and this list never grows on its
90
+ * own: a value outside it is not "a new aggregate", it is an unrecognised one,
91
+ * and {@link resolveMeasureLabel} treats it as absent.
92
+ */
93
+ export declare const BUILTIN_AGGREGATES: readonly ["count", "sum", "avg", "min", "max", "count_distinct"];
94
+ /** One member of {@link BUILTIN_AGGREGATES}. */
95
+ export type BuiltinAggregate = (typeof BUILTIN_AGGREGATES)[number];
96
+ /**
97
+ * The aggregates whose every value is a whole number: a count of rows, or of
98
+ * distinct values (objectui#11681, triage ruling Q3 A on that card). `sum` /
99
+ * `min` / `max` over an integer field would qualify too, but the answer does
100
+ * not state the source field's scale, so they are not judged integer here.
101
+ */
102
+ export declare const INTEGER_AGGREGATES: readonly ["count", "count_distinct"];
103
+ /**
104
+ * Whether a measure column's `aggregate` makes every value a whole number — a
105
+ * value axis that plots only such measures takes integer ticks
106
+ * (objectui#11681). Judged over an untrusted wire value; anything else,
107
+ * absence included, answers `false`, so the axis keeps its default.
108
+ */
109
+ export declare function isIntegerAggregate(value: unknown): boolean;
110
+ /** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
111
+ export declare function isBuiltinAggregate(value: unknown): value is BuiltinAggregate;
112
+ /**
113
+ * The LOCALIZED display label per built-in aggregate, as the layer that holds
114
+ * the i18n provider resolved them (`@object-ui/i18n`'s
115
+ * `builtinAggregateLabels(tt)`). Partial on purpose: an aggregate the caller
116
+ * did not resolve falls back to the wire `label`, exactly as if the field had
117
+ * carried no discriminator.
118
+ */
119
+ export type BuiltinAggregateLabels = Partial<Record<BuiltinAggregate, string>>;
120
+ /**
121
+ * The display label of ONE result field, for a legend / axis / tooltip.
122
+ *
123
+ * Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
124
+ * 1. the field declares itself a built-in default measure
125
+ * (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
126
+ * a locale label for it → that label. This is the only path that can
127
+ * replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
128
+ * never by matching the label's text or the field's name, both of which
129
+ * the ruling rejected (a real author measure literally named `count`, or
130
+ * labelled `Count`, must keep its own label);
131
+ * 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
132
+ * author-declared measure (`'Tasks'` reaches the legend untouched);
133
+ * 3. otherwise the field's `name`.
134
+ *
135
+ * So a discriminator the caller has no label for (a provider-less host, or a
136
+ * value outside the closed vocabulary) costs nothing: the field renders exactly
137
+ * what it rendered before the discriminator existed.
138
+ */
139
+ export declare function resolveMeasureLabel(field: ChartMeasureField, labels?: BuiltinAggregateLabels): string;
34
140
  /**
35
141
  * The bucket a NULL/undefined category VALUE renders under, when the caller
36
142
  * supplies no localized label of its own (objectui#4466).
@@ -110,6 +216,16 @@ export interface ChartSeriesOptions {
110
216
  * it, so a mismatch costs the null bucket its drill-through.
111
217
  */
112
218
  nullCategoryLabel?: string;
219
+ /**
220
+ * Locale labels for the server's BUILT-IN default measures, keyed by the
221
+ * wire discriminator a result field carries as `builtinAggregate`
222
+ * (objectui#7258). Same division of labour as `nullCategoryLabel`: this
223
+ * package is React-free and i18n-free, so the renderer that holds the
224
+ * provider resolves the strings (`builtinAggregateLabels(tt)` in
225
+ * `@object-ui/i18n`) and passes them in. Absent ⇒ every measure keeps its
226
+ * wire `label`, which for a built-in default is the server's English.
227
+ */
228
+ builtinAggregateLabels?: BuiltinAggregateLabels;
113
229
  }
114
230
  /** Per-CLICK knobs for {@link findChartSeriesRow}, on top of the shared ones. */
115
231
  export interface ChartDrillLookupOptions extends ChartSeriesOptions {
@@ -174,7 +290,7 @@ export interface ChartSegmentClickEvent {
174
290
  /** The measure value at the click point. */
175
291
  value?: number;
176
292
  }
177
- export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartResultField[] | null, options?: ChartSeriesOptions): ChartSeriesResult;
293
+ export declare function buildChartSeries(rows: Array<Record<string, unknown>> | null | undefined, dimensions: string[] | null | undefined, values: string[] | null | undefined, fields?: ChartMeasureField[] | null, options?: ChartSeriesOptions): ChartSeriesResult;
178
294
  /**
179
295
  * Resolve select/enum dimension VALUES to display LABELS in chart rows.
180
296
  *
@@ -362,10 +478,54 @@ export declare function buildDimensionLabelMap(options: unknown, translate?: Opt
362
478
  * The object a relationship field points at, or `undefined` when the field is
363
479
  * not a relationship (or names no target).
364
480
  *
365
- * The target lives under `reference` on framework-served field defs; older /
366
- * spec shapes spell it `reference_to` / `referenceTo` / `reference_to_object`,
367
- * and any of them may carry a bare name, a one-element array, or `{ object }`.
368
- * Same canonicalization as the dataset designer's `resolveReferenceTo`.
481
+ * The target lives under `reference` — the ONLY spelling read here, and the same
482
+ * canonicalization as the dataset designer's `resolveReferenceTo`, which is
483
+ * narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
484
+ * CARRIER matches too: a bare string and nothing else. The two helpers are one
485
+ * doctrine in two files and must never drift — a divergence recreates the
486
+ * defect one file over.
487
+ *
488
+ * The three legacy spellings this dropped (`reference_to` / `referenceTo` /
489
+ * `reference_to_object`) were censused against every producer, with `reference`
490
+ * itself as the positive control (445 of 565 lookup / master_detail defs in the
491
+ * framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
492
+ * REFUSES all three others BY NAME). No producer emits any of them onto an
493
+ * object metadata document: `reference_to` was then live only on ObjectUI's own
494
+ * view/field schema (a different contract, translated INTO from `reference`;
495
+ * objectui#11070 round 4 moved that contract to `reference` as well),
496
+ * `referenceTo`'s two producers were retired by objectui#6041, and
497
+ * `reference_to_object` occurs nowhere in either tree outside this chain and the
498
+ * test that called it.
499
+ *
500
+ * ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
501
+ * document DIRECTLY — no `readFields` door strips retired keys on this path. So
502
+ * the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
503
+ * row spelling the target `referenceTo` now resolves to `undefined` here, the
504
+ * walk yields no entry, and the caller keeps the raw value (best-effort by
505
+ * construction — see {@link resolveDimensionFieldMeta}). That is the intended
506
+ * outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
507
+ * lenient consumer here is exactly where it would have stayed hidden. Note
508
+ * `reference` was already HEAD of the old chain, so any doc carrying both is
509
+ * unaffected.
510
+ *
511
+ * The array and `{ object }` CARRIERS this used to accept were censused on the
512
+ * same standard and removed with the same reasoning (objectui#6648).
513
+ * `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
514
+ * (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
515
+ * `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
516
+ * structure-walking, key-position-aware census of both trees found 587
517
+ * bare-string carriers at the field-def key position and ZERO array or
518
+ * `{ object }` carriers from any producer — the only ones in either tree were
519
+ * the green pins asserting this very tolerance. See `resolveReferenceTo` for
520
+ * the full census, including why a multi-target lookup is NOT what the array
521
+ * branch was serving: it returned `raw[0]` and discarded the rest, and
522
+ * polymorphic lookup remains an undeclared, open spec gap.
523
+ *
524
+ * This path matters MORE than the designer's for the same reason the spelling
525
+ * narrowing did: it reads the metadata document directly, with no `readFields`
526
+ * door in between, so a non-string carrier now degrades visibly (the walk
527
+ * yields no entry and the caller keeps the raw value) instead of being absorbed
528
+ * here.
369
529
  *
370
530
  * The **type gate is deliberate**: only a declared relationship is walked, so a
371
531
  * path segment naming a plain field can never be turned into an object name and
@@ -44,6 +44,63 @@
44
44
  * know" refuses — one doctrine, and now BOTH branches state it.
45
45
  */
46
46
  import { pivotBucketId, pivotDimensionValue } from './dataset-pivot.js';
47
+ /**
48
+ * The closed vocabulary of server-side built-in default measures a result field
49
+ * can declare itself as (objectui#7258 / objectstack#14492) — the spec's own
50
+ * measure-aggregate enum spelling (`count_distinct`, not `countDistinct`), so
51
+ * there is exactly ONE spelling on the wire and this list never grows on its
52
+ * own: a value outside it is not "a new aggregate", it is an unrecognised one,
53
+ * and {@link resolveMeasureLabel} treats it as absent.
54
+ */
55
+ export const BUILTIN_AGGREGATES = ['count', 'sum', 'avg', 'min', 'max', 'count_distinct'];
56
+ /**
57
+ * The aggregates whose every value is a whole number: a count of rows, or of
58
+ * distinct values (objectui#11681, triage ruling Q3 A on that card). `sum` /
59
+ * `min` / `max` over an integer field would qualify too, but the answer does
60
+ * not state the source field's scale, so they are not judged integer here.
61
+ */
62
+ export const INTEGER_AGGREGATES = ['count', 'count_distinct'];
63
+ /**
64
+ * Whether a measure column's `aggregate` makes every value a whole number — a
65
+ * value axis that plots only such measures takes integer ticks
66
+ * (objectui#11681). Judged over an untrusted wire value; anything else,
67
+ * absence included, answers `false`, so the axis keeps its default.
68
+ */
69
+ export function isIntegerAggregate(value) {
70
+ return typeof value === 'string' && INTEGER_AGGREGATES.includes(value);
71
+ }
72
+ /** Runtime membership test for {@link BUILTIN_AGGREGATES}, over an untrusted wire value. */
73
+ export function isBuiltinAggregate(value) {
74
+ return typeof value === 'string' && BUILTIN_AGGREGATES.includes(value);
75
+ }
76
+ /**
77
+ * The display label of ONE result field, for a legend / axis / tooltip.
78
+ *
79
+ * Resolution order (objectui#7258, maintainer ruling B of 2026-09-02):
80
+ * 1. the field declares itself a built-in default measure
81
+ * (`builtinAggregate` ∈ {@link BUILTIN_AGGREGATES}) AND the caller resolved
82
+ * a locale label for it → that label. This is the only path that can
83
+ * replace a wire `label`, and it is keyed by a STRUCTURAL discriminator —
84
+ * never by matching the label's text or the field's name, both of which
85
+ * the ruling rejected (a real author measure literally named `count`, or
86
+ * labelled `Count`, must keep its own label);
87
+ * 2. otherwise the wire `label` verbatim — the objectui#4106 contract for an
88
+ * author-declared measure (`'Tasks'` reaches the legend untouched);
89
+ * 3. otherwise the field's `name`.
90
+ *
91
+ * So a discriminator the caller has no label for (a provider-less host, or a
92
+ * value outside the closed vocabulary) costs nothing: the field renders exactly
93
+ * what it rendered before the discriminator existed.
94
+ */
95
+ export function resolveMeasureLabel(field, labels) {
96
+ const aggregate = field.builtinAggregate;
97
+ if (isBuiltinAggregate(aggregate)) {
98
+ const localized = labels?.[aggregate];
99
+ if (typeof localized === 'string' && localized !== '')
100
+ return localized;
101
+ }
102
+ return field.label ?? field.name;
103
+ }
47
104
  /**
48
105
  * The bucket a NULL/undefined category VALUE renders under, when the caller
49
106
  * supplies no localized label of its own (objectui#4466).
@@ -296,7 +353,19 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
296
353
  const dims = (dimensions ?? []).filter(Boolean);
297
354
  const vals = (values ?? []).filter(Boolean);
298
355
  const safeRows = Array.isArray(rows) ? rows : [];
299
- const labelOf = (name) => (fields ?? []).find((f) => f.name === name)?.label ?? name;
356
+ // A measure's series label: the locale label when the field declares itself
357
+ // a built-in default measure and the caller resolved one, else the wire
358
+ // `label` verbatim (objectui#4106), else the name — see `resolveMeasureLabel`.
359
+ const labelOf = (name) => {
360
+ const field = (fields ?? []).find((f) => f.name === name);
361
+ return field ? resolveMeasureLabel(field, options?.builtinAggregateLabels) : name;
362
+ };
363
+ // The measure column's own `aggregate`, spread onto each series that plots
364
+ // it (objectui#11681) — present only when the column states a string one.
365
+ const aggregateOf = (name) => {
366
+ const aggregate = (fields ?? []).find((f) => f.name === name)?.aggregate;
367
+ return typeof aggregate === 'string' ? { aggregate } : {};
368
+ };
300
369
  // Multi-dimension, single-measure → pivot the second dimension into series.
301
370
  if (dims.length >= 2 && vals.length === 1) {
302
371
  const xKey = dims[0];
@@ -402,7 +471,8 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
402
471
  // server-resolved to display labels by queryDataset), except the null
403
472
  // group, which says so with the bucket label the renderer supplied. The
404
473
  // key is a separate question from the text — see `pivotSeriesBuckets`.
405
- series: groups.map(({ dataKey, label }) => ({ dataKey, label })),
474
+ // Every group plots the one measure, so each carries its aggregate.
475
+ series: groups.map(({ dataKey, label }) => ({ dataKey, label, ...aggregateOf(measure) })),
406
476
  };
407
477
  }
408
478
  // Default: first dimension on the x-axis, one series per measure.
@@ -425,7 +495,7 @@ export function buildChartSeries(rows, dimensions, values, fields, options) {
425
495
  ? bucketNullCategories(safeRows, xKey, options?.nullCategoryLabel ?? NULL_CATEGORY_LABEL)
426
496
  : safeRows,
427
497
  xAxisKey: xKey,
428
- series: vals.map((v) => ({ dataKey: v, label: labelOf(v) })),
498
+ series: vals.map((v) => ({ dataKey: v, label: labelOf(v), ...aggregateOf(v) })),
429
499
  };
430
500
  }
431
501
  /**
@@ -765,10 +835,54 @@ function fieldDefsOf(schema) {
765
835
  * The object a relationship field points at, or `undefined` when the field is
766
836
  * not a relationship (or names no target).
767
837
  *
768
- * The target lives under `reference` on framework-served field defs; older /
769
- * spec shapes spell it `reference_to` / `referenceTo` / `reference_to_object`,
770
- * and any of them may carry a bare name, a one-element array, or `{ object }`.
771
- * Same canonicalization as the dataset designer's `resolveReferenceTo`.
838
+ * The target lives under `reference` — the ONLY spelling read here, and the same
839
+ * canonicalization as the dataset designer's `resolveReferenceTo`, which is
840
+ * narrowed to match in the same pass (objectui#6528). As of objectui#6648 the
841
+ * CARRIER matches too: a bare string and nothing else. The two helpers are one
842
+ * doctrine in two files and must never drift — a divergence recreates the
843
+ * defect one file over.
844
+ *
845
+ * The three legacy spellings this dropped (`reference_to` / `referenceTo` /
846
+ * `reference_to_object`) were censused against every producer, with `reference`
847
+ * itself as the positive control (445 of 565 lookup / master_detail defs in the
848
+ * framework tree; ACCEPTED by `ObjectSchema.safeParse` on spec 17.2.0, which
849
+ * REFUSES all three others BY NAME). No producer emits any of them onto an
850
+ * object metadata document: `reference_to` was then live only on ObjectUI's own
851
+ * view/field schema (a different contract, translated INTO from `reference`;
852
+ * objectui#11070 round 4 moved that contract to `reference` as well),
853
+ * `referenceTo`'s two producers were retired by objectui#6041, and
854
+ * `reference_to_object` occurs nowhere in either tree outside this chain and the
855
+ * test that called it.
856
+ *
857
+ * ⚠ Unlike `resolveReferenceTo`, this helper reads the `GET /meta/object/:name`
858
+ * document DIRECTLY — no `readFields` door strips retired keys on this path. So
859
+ * the narrowing is load-bearing rather than cosmetic: a stored pre-objectui#6041
860
+ * row spelling the target `referenceTo` now resolves to `undefined` here, the
861
+ * walk yields no entry, and the caller keeps the raw value (best-effort by
862
+ * construction — see {@link resolveDimensionFieldMeta}). That is the intended
863
+ * outcome: per AGENTS.md #0.1 such a row is a PRODUCER-side defect, and a
864
+ * lenient consumer here is exactly where it would have stayed hidden. Note
865
+ * `reference` was already HEAD of the old chain, so any doc carrying both is
866
+ * unaffected.
867
+ *
868
+ * The array and `{ object }` CARRIERS this used to accept were censused on the
869
+ * same standard and removed with the same reasoning (objectui#6648).
870
+ * `FieldSchema.reference` is a plain `z.string()`; `ObjectSchema.safeParse`
871
+ * (spec 17.2.0) ACCEPTS `reference: 'crm_account'` and REFUSES both
872
+ * `['crm_account']` and `{ object: 'crm_account' }` as `invalid_type`. A
873
+ * structure-walking, key-position-aware census of both trees found 587
874
+ * bare-string carriers at the field-def key position and ZERO array or
875
+ * `{ object }` carriers from any producer — the only ones in either tree were
876
+ * the green pins asserting this very tolerance. See `resolveReferenceTo` for
877
+ * the full census, including why a multi-target lookup is NOT what the array
878
+ * branch was serving: it returned `raw[0]` and discarded the rest, and
879
+ * polymorphic lookup remains an undeclared, open spec gap.
880
+ *
881
+ * This path matters MORE than the designer's for the same reason the spelling
882
+ * narrowing did: it reads the metadata document directly, with no `readFields`
883
+ * door in between, so a non-string carrier now degrades visibly (the walk
884
+ * yields no entry and the caller keeps the raw value) instead of being absorbed
885
+ * here.
772
886
  *
773
887
  * The **type gate is deliberate**: only a declared relationship is walked, so a
774
888
  * path segment naming a plain field can never be turned into an object name and
@@ -781,17 +895,8 @@ export function resolveRelationshipTarget(fieldDef) {
781
895
  const type = typeof def.type === 'string' ? def.type.toLowerCase() : '';
782
896
  if (!RELATIONSHIP_FIELD_TYPES.has(type))
783
897
  return undefined;
784
- const raw = def.reference ?? def.reference_to ?? def.referenceTo ?? def.reference_to_object;
785
- if (typeof raw === 'string' && raw)
786
- return raw;
787
- if (Array.isArray(raw) && typeof raw[0] === 'string' && raw[0])
788
- return raw[0];
789
- if (raw && typeof raw === 'object') {
790
- const obj = raw.object;
791
- if (typeof obj === 'string' && obj)
792
- return obj;
793
- }
794
- return undefined;
898
+ const raw = def.reference;
899
+ return typeof raw === 'string' && raw ? raw : undefined;
795
900
  }
796
901
  /**
797
902
  * Resolve each dataset dimension's underlying `field` to that field's select
@@ -0,0 +1,157 @@
1
+ /**
2
+ * ObjectUI — served per-column sortability projection (consumer side)
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 consumer half of objectstack#10235 (maintainer ruling A, 2026-08-23):
10
+ * the platform serves an explicit per-column sortability signal, and a grid
11
+ * reads THAT to decide whether a header offers a sort click — it never
12
+ * re-derives "virtual ⇒ unsortable" from the field's `type`.
13
+ *
14
+ * ## What the platform serves
15
+ *
16
+ * `GET /api/v1/meta/object/:name` answers `GetMetaItemResponseSchema`, whose
17
+ * ENVELOPE (never the document under `item` — `FieldSchema` is a
18
+ * `strictObject`, so the key must stay un-authorable) carries an optional
19
+ * `sortability` key when the served type is `object`, on every serving branch
20
+ * including the cached one:
21
+ *
22
+ * ```jsonc
23
+ * { "type": "object", "name": "crm_opportunity", "item": { … },
24
+ * "sortability": { "fields": {
25
+ * "amount": { "sortable": true },
26
+ * "expected_revenue": { "sortable": false, "reason": "virtual-type" },
27
+ * "remote_anchor": { "sortable": true, "caveat": "unprovisioned-anchor" }
28
+ * } } }
29
+ * ```
30
+ *
31
+ * Shape verified against the MERGED upstream change, not against a description
32
+ * of it: `@objectstack/spec`'s `ObjectSortabilitySchema` /
33
+ * `FieldSortabilitySchema` and `GetMetaItemResponseSchema.sortability`.
34
+ *
35
+ * ## The contract, and the asymmetry that makes it easy to get backwards
36
+ *
37
+ * Offer a sort affordance on a column **iff the projection has an entry for
38
+ * the column's field name AND that entry says `sortable: true`**.
39
+ *
40
+ * ABSENCE OF AN ENTRY MEANS "no platform sort behind this name" — never
41
+ * "assume sortable". The projection's domain is exactly the served field map
42
+ * plus the always-provisioned `id`, so an unknown name, a dotted path
43
+ * (`account.name`) and an unprovisioned audit column all arrive as absence,
44
+ * and all three are refused by the runtime doors. A `!== false` test — the
45
+ * spelling every other optional flag in this repo uses — answers `true` for
46
+ * absence and so gets exactly that family backwards. {@link isPlatformSortableField}
47
+ * is the only spelling of this test; do not inline a comparison at a call site.
48
+ *
49
+ * ## Present-vs-absent PROJECTION is a different question
50
+ *
51
+ * `readObjectSortability` returns `undefined` when the served metadata carried
52
+ * no `sortability` key at all — a backend older than the upstream change, an
53
+ * inline/mock data source, a fixture. That is NOT "every column is unsortable":
54
+ * it is "no signal was served", and the caller decides what it did before the
55
+ * signal existed. The two are deliberately different types (`undefined` vs an
56
+ * empty `fields` map) so a caller cannot conflate them by accident.
57
+ *
58
+ * ## `caveat: 'unprovisioned-anchor'` is NOT a refusal
59
+ *
60
+ * Platform-injected anchors on ADR-0015 `external` objects come back
61
+ * `sortable: true, caveat: 'unprovisioned-anchor'`: the runtime ACCEPTS the
62
+ * sort, but silently drops it (`asc` === `desc` under a `200`) when the remote
63
+ * table carries no such column — a degradation the platform cannot prove
64
+ * either way at serve time. This module treats the caveat as advisory and the
65
+ * `sortable` verdict as the enforcement fact, so a caveated column keeps its
66
+ * click. Refusing what the platform does not refuse would recreate
67
+ * declared-≠-enforced drift in mirror image; whether to badge or withhold on
68
+ * the caveat is an open affordance question, not something to settle here.
69
+ */
70
+ /**
71
+ * The served shape is the SPEC's, re-exported rather than restated.
72
+ *
73
+ * `FIELD_UNSORTABLE_VIRTUAL_TYPE` / `FIELD_SORTABLE_UNPROVISIONED_ANCHOR` are
74
+ * the two string literals the projection can carry, and `FieldSortability` /
75
+ * `ObjectSortability` are the wire types themselves (`z.input` of the
76
+ * schemas the REST layer answers with). Re-exporting is the point of a
77
+ * contract-first consumer: a local declaration under a spec export's name is
78
+ * read by the next agent as the spec's own definition, and then drifts from
79
+ * it — which is what `check:spec-symbols` guards, and what it caught here.
80
+ */
81
+ export { FIELD_UNSORTABLE_VIRTUAL_TYPE, FIELD_SORTABLE_UNPROVISIONED_ANCHOR, } from '@objectstack/spec/api';
82
+ export type { FieldSortability, ObjectSortability } from '@objectstack/spec/api';
83
+ import type { ObjectSortability } from '@objectstack/spec/api';
84
+ /**
85
+ * Where the projection rides on the client-side object schema.
86
+ *
87
+ * A GLOBAL-REGISTRY SYMBOL, deliberately — the same carrier shape
88
+ * `INFLIGHT_GET_REGISTRY_KEY` uses in `@object-ui/types`. Three properties
89
+ * this signal needs and a string key would not give it:
90
+ *
91
+ * - `JSON.stringify` drops symbol-keyed properties, so a schema object that
92
+ * is ever round-tripped back at a metadata WRITE endpoint cannot carry the
93
+ * projection into a document the server parses `strict` and rejects by
94
+ * name. The upstream change kept the key off `item` for exactly that
95
+ * reason; stamping a string key here would undo it one repo away.
96
+ * - `Object.keys` / spread / `for…in` do not see it, so every existing reader
97
+ * that enumerates the schema (column generators, diffing, form builders)
98
+ * is unchanged by its presence.
99
+ * - `Symbol.for` is the cross-realm registry, so two copies of this module
100
+ * (a workspace build and a published one in the same graph) agree on the
101
+ * key rather than silently reading past each other.
102
+ */
103
+ export declare const OBJECT_SORTABILITY_KEY: unique symbol;
104
+ /**
105
+ * Normalize a served `sortability` envelope value into the projection, or
106
+ * `undefined` when the envelope carried nothing usable.
107
+ *
108
+ * Tolerant on purpose (the same contract the upstream resolver carries): the
109
+ * adapter hands this whatever came back over the wire, unparsed.
110
+ */
111
+ export declare function normalizeObjectSortability(served: unknown): ObjectSortability | undefined;
112
+ /**
113
+ * Stamp the served projection onto the client-side object schema, under
114
+ * {@link OBJECT_SORTABILITY_KEY}.
115
+ *
116
+ * Non-enumerable and non-writable-by-accident: the property is defined rather
117
+ * than assigned so it never shows up in a spread of the schema and cannot be
118
+ * clobbered by one. Returns the same object it was handed (the adapter caches
119
+ * that instance), and is a no-op when there is nothing usable to attach — so
120
+ * an older backend leaves the schema exactly as it found it rather than
121
+ * stamping an empty projection that would read as "nothing is sortable".
122
+ */
123
+ export declare function attachObjectSortability<T>(schema: T, served: unknown): T;
124
+ /**
125
+ * Read the served projection off a client-side object schema.
126
+ *
127
+ * `undefined` means NO SIGNAL WAS SERVED — not "nothing is sortable". See the
128
+ * module docblock: callers must branch on this before applying the contract.
129
+ */
130
+ export declare function readObjectSortability(schema: unknown): ObjectSortability | undefined;
131
+ /**
132
+ * THE contract test: does the platform offer a sort over this field name?
133
+ *
134
+ * `true` iff the projection has an entry for `fieldName` and that entry says
135
+ * `sortable: true`. Every other case — no entry, `sortable: false`, a
136
+ * malformed entry — answers `false`, because absence means "no platform sort
137
+ * behind this name".
138
+ *
139
+ * `sortability` is REQUIRED and non-optional on purpose: a caller holding
140
+ * `undefined` (no signal served) has a different question to answer and must
141
+ * not reach this function with it. See {@link readObjectSortability}.
142
+ */
143
+ export declare function isPlatformSortableField(sortability: ObjectSortability, fieldName: string | undefined): boolean;
144
+ /**
145
+ * Drop from a sort list every entry the platform will not order by.
146
+ *
147
+ * The RESTORE leg of the same contract: withholding the header click stops a
148
+ * fresh unsortable sort from being created, but a sort persisted BEFORE the
149
+ * signal existed is already in stored view state, and replaying it would keep
150
+ * a refused `$orderby` alive and re-persist it on the next unrelated edit.
151
+ * Filtering the list at the seam the grid both renders from and emits through
152
+ * means an already-stored entry is inert and is dropped the first time
153
+ * anything writes the sort back.
154
+ */
155
+ export declare function filterPlatformSortableSort<T extends {
156
+ field?: string;
157
+ }>(sort: readonly T[] | undefined, sortability: ObjectSortability): T[];