@object-ui/core 17.3.0 → 17.6.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 (99) hide show
  1. package/CHANGELOG.md +2018 -0
  2. package/README.md +38 -34
  3. package/dist/actions/ActionEngine.d.ts +43 -26
  4. package/dist/actions/ActionEngine.js +67 -54
  5. package/dist/actions/ActionRunner.d.ts +281 -31
  6. package/dist/actions/ActionRunner.js +458 -36
  7. package/dist/actions/actionKeys.d.ts +163 -27
  8. package/dist/actions/actionKeys.js +238 -27
  9. package/dist/actions/actionResponse.js +1 -1
  10. package/dist/actions/index.d.ts +1 -0
  11. package/dist/actions/index.js +1 -0
  12. package/dist/actions/recordIdParam.d.ts +77 -0
  13. package/dist/actions/recordIdParam.js +37 -0
  14. package/dist/actions/serverActionHandler.d.ts +1 -1
  15. package/dist/actions/serverActionHandler.js +1 -1
  16. package/dist/data-scope/ViewDataProvider.d.ts +37 -12
  17. package/dist/data-scope/ViewDataProvider.js +83 -5
  18. package/dist/data-scope/element-data-source.d.ts +113 -0
  19. package/dist/data-scope/element-data-source.js +216 -0
  20. package/dist/data-scope/index.d.ts +2 -1
  21. package/dist/data-scope/index.js +1 -0
  22. package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
  23. package/dist/evaluator/ExpressionEvaluator.js +42 -1
  24. package/dist/evaluator/SafeExpressionParser.js +0 -4
  25. package/dist/evaluator/declaredPredicate.d.ts +119 -0
  26. package/dist/evaluator/declaredPredicate.js +152 -0
  27. package/dist/evaluator/fieldRules.d.ts +35 -0
  28. package/dist/evaluator/fieldRules.js +79 -20
  29. package/dist/evaluator/index.d.ts +2 -0
  30. package/dist/evaluator/index.js +2 -0
  31. package/dist/evaluator/listConditional.d.ts +54 -4
  32. package/dist/evaluator/listConditional.js +125 -19
  33. package/dist/evaluator/optionRules.d.ts +55 -0
  34. package/dist/evaluator/optionRules.js +60 -0
  35. package/dist/evaluator/predicateInput.d.ts +51 -3
  36. package/dist/evaluator/predicateInput.js +62 -5
  37. package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
  38. package/dist/evaluator/rowPredicateCanon.js +208 -0
  39. package/dist/index.d.ts +8 -1
  40. package/dist/index.js +31 -1
  41. package/dist/protocols/DndProtocol.d.ts +42 -1
  42. package/dist/protocols/KeyboardProtocol.d.ts +31 -1
  43. package/dist/protocols/SharingProtocol.d.ts +11 -1
  44. package/dist/registry/Registry.d.ts +62 -16
  45. package/dist/theme/ThemeEngine.d.ts +0 -8
  46. package/dist/theme/ThemeEngine.js +22 -103
  47. package/dist/theme/index.d.ts +1 -1
  48. package/dist/theme/index.js +7 -1
  49. package/dist/types/index.d.ts +125 -12
  50. package/dist/utils/chart-presentation.d.ts +205 -0
  51. package/dist/utils/chart-presentation.js +280 -0
  52. package/dist/utils/chart-series.d.ts +390 -23
  53. package/dist/utils/chart-series.js +776 -32
  54. package/dist/utils/column-identity.d.ts +43 -0
  55. package/dist/utils/column-identity.js +47 -1
  56. package/dist/utils/compare-to.d.ts +40 -13
  57. package/dist/utils/compare-to.js +4 -32
  58. package/dist/utils/dashboard-filters.d.ts +54 -8
  59. package/dist/utils/dashboard-filters.js +261 -12
  60. package/dist/utils/dataset-format.d.ts +73 -14
  61. package/dist/utils/dataset-format.js +142 -9
  62. package/dist/utils/dataset-pivot.d.ts +28 -7
  63. package/dist/utils/dataset-pivot.js +27 -6
  64. package/dist/utils/debug.js +7 -0
  65. package/dist/utils/dom-props.d.ts +137 -0
  66. package/dist/utils/dom-props.js +153 -0
  67. package/dist/utils/expand-fields.d.ts +97 -7
  68. package/dist/utils/expand-fields.js +97 -7
  69. package/dist/utils/filter-tokens.js +0 -1
  70. package/dist/utils/freeze-schema.d.ts +38 -1
  71. package/dist/utils/freeze-schema.js +10 -0
  72. package/dist/utils/humanize-label.d.ts +60 -0
  73. package/dist/utils/humanize-label.js +62 -0
  74. package/dist/utils/managedBy.d.ts +27 -2
  75. package/dist/utils/normalize-list-view.d.ts +36 -6
  76. package/dist/utils/normalize-list-view.js +93 -9
  77. package/dist/utils/number-display.d.ts +155 -0
  78. package/dist/utils/number-display.js +71 -0
  79. package/dist/utils/predicate-fields.d.ts +29 -4
  80. package/dist/utils/predicate-fields.js +44 -4
  81. package/dist/utils/resolve-view-id.d.ts +31 -0
  82. package/dist/utils/resolve-view-id.js +45 -0
  83. package/dist/utils/retired-field-types.d.ts +90 -0
  84. package/dist/utils/retired-field-types.js +124 -0
  85. package/dist/utils/sort-query.d.ts +57 -0
  86. package/dist/utils/sort-query.js +35 -0
  87. package/dist/utils/unmaterialized-fields.d.ts +52 -0
  88. package/dist/utils/unmaterialized-fields.js +57 -0
  89. package/dist/validation/index.d.ts +1 -0
  90. package/dist/validation/index.js +1 -0
  91. package/dist/validation/schema-validator.js +105 -0
  92. package/dist/validation/server-owned-value.d.ts +62 -0
  93. package/dist/validation/server-owned-value.js +99 -0
  94. package/dist/validation/validators/object-validation-engine.js +0 -2
  95. package/package.json +5 -5
  96. package/dist/__benchmarks__/core.bench.d.ts +0 -8
  97. package/dist/__benchmarks__/core.bench.js +0 -53
  98. package/dist/utils/merge-views-into-objects.d.ts +0 -19
  99. package/dist/utils/merge-views-into-objects.js +0 -39
@@ -0,0 +1,77 @@
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
+ * Seeding an action's `recordIdParam` from the row — and refusing to dispatch
10
+ * when the row cannot supply it (objectstack#8018).
11
+ *
12
+ * An `api` action declaring `recordIdParam` names the request key that carries
13
+ * the record it acts on; `recordIdField` names the row field whose value seeds
14
+ * it (default `id`). Every call site used to spell the read the same permissive
15
+ * way:
16
+ *
17
+ * ```ts
18
+ * const rowValue = rowRecord[action.recordIdField || 'id'];
19
+ * if (rowValue != null) body[action.recordIdParam] = rowValue; // else: nothing
20
+ * ```
21
+ *
22
+ * The `else` branch is the defect. It cannot distinguish "this row has no such
23
+ * key" from "this row's value is null", and in both cases it drops the parameter
24
+ * and lets the request go out anyway. What reaches the server is a mutation with
25
+ * no record named — and a backend that treats a missing selector as "match
26
+ * nothing" answers `{ status: true }` for having deleted nothing. The user is
27
+ * told the action succeeded. The measured instance was a session-revocation
28
+ * control that revoked nothing, which is the worst shape this can take: a
29
+ * security affordance that reports success while no-op'ing.
30
+ *
31
+ * The absent-key half is reachable from ordinary authoring, not just from bad
32
+ * data: the grid projects `$select` from the listView columns, so until
33
+ * `recordIdField` was harvested into it (see `listViewPredicates`) any action
34
+ * keyed on a non-column field got a row without that key. The harvest closes the
35
+ * common route; this guard closes the class, because a row can still lack the key
36
+ * for reasons projection cannot fix — a server-side read mask that strips the
37
+ * field regardless of projection (`internal: true`), a partial payload handed
38
+ * down from a host, a field the principal cannot read.
39
+ *
40
+ * Contract-first (AGENTS.md #0.1): the answer to an under-specified request is to
41
+ * refuse it at the producer, loudly, not to send it and hope the server objects.
42
+ * The two failures are named separately because they point at different repairs —
43
+ * an absent key is a projection/visibility problem, a null value is a data one.
44
+ */
45
+ /** The minimal action shape this resolution reads. */
46
+ export interface RecordIdParamAction {
47
+ /** Request key that carries the record id. Absent ⇒ no injection is declared. */
48
+ recordIdParam?: string;
49
+ /** Row field whose value seeds it. Defaults to `id`. */
50
+ recordIdField?: string;
51
+ /** Used only to make the refusal message name the action. */
52
+ name?: string;
53
+ label?: string;
54
+ }
55
+ /**
56
+ * Either the value to inject, or the reason the action must not be dispatched.
57
+ * `{}` (neither key) means no injection is declared — carry on unchanged.
58
+ */
59
+ export type RecordIdParamSeed = {
60
+ value: unknown;
61
+ error?: undefined;
62
+ } | {
63
+ value?: undefined;
64
+ error: string;
65
+ } | {
66
+ value?: undefined;
67
+ error?: undefined;
68
+ };
69
+ /**
70
+ * Resolve the value an action's `recordIdParam` should carry, or the refusal.
71
+ *
72
+ * Pure — reads the action and the row, mutates nothing. Returns `{}` when the
73
+ * action declares no `recordIdParam` (nothing to inject) or when there is no row
74
+ * record at all (the caller is on a path where no row context exists; that is not
75
+ * this guard's business).
76
+ */
77
+ export declare function resolveRecordIdParamSeed(action: RecordIdParamAction | null | undefined, rowRecord: Record<string, unknown> | null | undefined): RecordIdParamSeed;
@@ -0,0 +1,37 @@
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
+ * Resolve the value an action's `recordIdParam` should carry, or the refusal.
10
+ *
11
+ * Pure — reads the action and the row, mutates nothing. Returns `{}` when the
12
+ * action declares no `recordIdParam` (nothing to inject) or when there is no row
13
+ * record at all (the caller is on a path where no row context exists; that is not
14
+ * this guard's business).
15
+ */
16
+ export function resolveRecordIdParamSeed(action, rowRecord) {
17
+ const param = action?.recordIdParam;
18
+ if (!param || !rowRecord || typeof rowRecord !== 'object')
19
+ return {};
20
+ const field = action?.recordIdField || 'id';
21
+ const label = action?.label || action?.name || param;
22
+ if (!(field in rowRecord)) {
23
+ return {
24
+ error: `Action "${label}" identifies its record by "${field}", but that field is not ` +
25
+ `present on the row. Add it to the view's columns, or check that it is readable ` +
26
+ `— running without it would send a request that names no record.`,
27
+ };
28
+ }
29
+ const value = rowRecord[field];
30
+ if (value == null) {
31
+ return {
32
+ error: `Action "${label}" identifies its record by "${field}", but this record has no ` +
33
+ `value for it — running without it would send a request that names no record.`,
34
+ };
35
+ }
36
+ return { value };
37
+ }
@@ -52,7 +52,7 @@
52
52
  * // <ActionProvider handlers={{ script }} … /> (or runner.registerHandler('script', script))
53
53
  * ```
54
54
  */
55
- import type { ActionContext, ActionDef, ActionResult } from './ActionRunner';
55
+ import type { ActionContext, ActionDef, ActionResult } from './ActionRunner.js';
56
56
  /** The response surface the dispatcher needs — satisfied by a WHATWG `Response`. */
57
57
  export interface ServerActionFetchResponse {
58
58
  ok: boolean;
@@ -5,7 +5,7 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
- import { interpretActionResponse } from './actionResponse';
8
+ import { interpretActionResponse } from './actionResponse.js';
9
9
  /**
10
10
  * An action that also mounts on list rows (`list_item`) or record pages is
11
11
  * designed to run against a single record. When such an action is launched
@@ -18,6 +18,7 @@
18
18
  * @module data-scope
19
19
  * @packageDocumentation
20
20
  */
21
+ import { type ElementDataSourceConfig, type ElementSavedView } from './element-data-source.js';
21
22
  /** ViewData union — matches @objectstack/spec ViewDataSchema */
22
23
  export type ViewDataConfig = {
23
24
  provider: 'object';
@@ -38,17 +39,12 @@ export type ViewDataConfig = {
38
39
  provider: 'value';
39
40
  items: any[];
40
41
  };
41
- /** Element-level data source — matches @objectstack/spec ElementDataSourceSchema */
42
- export interface ElementDataSourceConfig {
43
- object: string;
44
- view?: string;
45
- filter?: Record<string, any>;
46
- sort?: Array<{
47
- field: string;
48
- order: 'asc' | 'desc';
49
- }>;
50
- limit?: number;
51
- }
42
+ /**
43
+ * Re-exported from `./element-data-source.js`, where the binding's type now
44
+ * lives alongside the logic that consumes it. Kept on this module path because
45
+ * that is where it was declared before objectstack#5576.
46
+ */
47
+ export type { ElementDataSourceConfig };
52
48
  /** Fetcher interface — consumers provide the actual fetch implementation */
53
49
  export interface DataFetcher {
54
50
  /** Fetch records for an object */
@@ -82,6 +78,19 @@ export interface DataFetcher {
82
78
  headers?: Record<string, string>;
83
79
  body?: any;
84
80
  }): Promise<any>;
81
+ /**
82
+ * Fetch the object's saved views, so an `ElementDataSource.view` name can be
83
+ * resolved to real columns / filter / sort (objectstack#5576).
84
+ *
85
+ * Optional because most fetchers only read records. A fetcher WITHOUT it
86
+ * cannot honour a named view, and {@link ViewDataProvider.resolveElementDataSource}
87
+ * says so in `error` rather than dropping the key — the whole point of #5576
88
+ * is that a `view` the runtime cannot apply must never look applied.
89
+ *
90
+ * Either shape stored views come in is accepted: a map keyed by view id, or
91
+ * the array of overlay rows an adapter's `listViews()` returns.
92
+ */
93
+ fetchViews?(object: string): Promise<Record<string, ElementSavedView> | ElementSavedView[]>;
85
94
  }
86
95
  /** Resolved data result */
87
96
  export interface ResolvedData {
@@ -138,6 +147,22 @@ export declare class ViewDataProvider {
138
147
  private resolveApi;
139
148
  /** Resolve object-based data (metadata-driven) */
140
149
  private resolveObject;
141
- /** Resolve element-level data source */
150
+ /**
151
+ * Resolve an element-level data source (`PageComponentSchema.dataSource`).
152
+ *
153
+ * `view` is HONOURED here, not dropped. Until objectstack#5576 this method
154
+ * forwarded `filter`/`sort`/`limit` and silently discarded `view`, so a page
155
+ * component that named a saved view got every record the object had — the
156
+ * declared binding was inert while looking applied.
157
+ *
158
+ * Three outcomes, all explicit:
159
+ * - no `view` → unchanged behaviour, the binding's own keys are forwarded;
160
+ * - `view` resolved → its columns become the `fields` projection and its
161
+ * filter/sort/limit the baseline the binding's keys compose with (see
162
+ * {@link composeElementDataSource} for the per-key rule);
163
+ * - `view` unresolvable, or the fetcher cannot list views at all → an
164
+ * `error` naming the view and what the object does have. NOT an empty
165
+ * result, and NOT a silent fall back to the unfiltered object.
166
+ */
142
167
  resolveElementDataSource(config: ElementDataSourceConfig): Promise<ResolvedData>;
143
168
  }
@@ -5,6 +5,20 @@
5
5
  * This source code is licensed under the MIT license found in the
6
6
  * LICENSE file in the root directory of this source tree.
7
7
  */
8
+ /**
9
+ * @object-ui/core - View Data Provider
10
+ *
11
+ * Resolves @objectstack/spec ViewData discriminated union into
12
+ * a unified data access interface. Supports three data modes:
13
+ *
14
+ * 1. 'object' — Metadata-driven via ObjectStack API (object name → fields/records)
15
+ * 2. 'api' — Custom REST API endpoints (read/write URLs)
16
+ * 3. 'value' — Static data (pre-loaded items array)
17
+ *
18
+ * @module data-scope
19
+ * @packageDocumentation
20
+ */
21
+ import { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, resolveSavedView, } from './element-data-source.js';
8
22
  /** Extract records array from various common API response shapes */
9
23
  function extractRecords(data) {
10
24
  if (Array.isArray(data))
@@ -142,12 +156,76 @@ export class ViewDataProvider {
142
156
  };
143
157
  }
144
158
  }
145
- /** Resolve element-level data source */
159
+ /**
160
+ * Resolve an element-level data source (`PageComponentSchema.dataSource`).
161
+ *
162
+ * `view` is HONOURED here, not dropped. Until objectstack#5576 this method
163
+ * forwarded `filter`/`sort`/`limit` and silently discarded `view`, so a page
164
+ * component that named a saved view got every record the object had — the
165
+ * declared binding was inert while looking applied.
166
+ *
167
+ * Three outcomes, all explicit:
168
+ * - no `view` → unchanged behaviour, the binding's own keys are forwarded;
169
+ * - `view` resolved → its columns become the `fields` projection and its
170
+ * filter/sort/limit the baseline the binding's keys compose with (see
171
+ * {@link composeElementDataSource} for the per-key rule);
172
+ * - `view` unresolvable, or the fetcher cannot list views at all → an
173
+ * `error` naming the view and what the object does have. NOT an empty
174
+ * result, and NOT a silent fall back to the unfiltered object.
175
+ */
146
176
  async resolveElementDataSource(config) {
147
- return this.resolve({ provider: 'object', object: config.object }, {
148
- filter: config.filter,
149
- sort: config.sort,
150
- limit: config.limit,
177
+ let view;
178
+ if (config.view) {
179
+ if (!this.fetcher?.fetchViews) {
180
+ return {
181
+ records: [],
182
+ total: 0,
183
+ provider: 'object',
184
+ loading: false,
185
+ error: `dataSource.view "${config.view}" cannot be resolved: this data ` +
186
+ `fetcher does not implement fetchViews().`,
187
+ };
188
+ }
189
+ let views;
190
+ try {
191
+ views = collectSavedViews(await this.fetcher.fetchViews(config.object));
192
+ }
193
+ catch (error) {
194
+ return {
195
+ records: [],
196
+ total: 0,
197
+ provider: 'object',
198
+ loading: false,
199
+ error: error.message,
200
+ };
201
+ }
202
+ view = resolveSavedView(views, config.view, config.object);
203
+ if (!view) {
204
+ return {
205
+ records: [],
206
+ total: 0,
207
+ provider: 'object',
208
+ loading: false,
209
+ error: elementDataSourceViewNotFoundMessage(config.object, config.view, Object.keys(views)),
210
+ };
211
+ }
212
+ }
213
+ const composed = composeElementDataSource(config, view);
214
+ const fields = Array.isArray(composed.columns)
215
+ ? composed.columns.filter((c) => typeof c === 'string' && !!c)
216
+ : undefined;
217
+ return this.resolve({ provider: 'object', object: composed.object }, {
218
+ // `DataFetcher.fetchRecords`'s `filter` is declared
219
+ // `Record<string, any>`, which predates the three filter shapes that
220
+ // legitimately circulate here: a MongoDB-style condition object, an
221
+ // ObjectQL AST node array, and a spec `ViewFilterRule[]`. The last two
222
+ // are arrays, so a saved view's filter does not fit the declared option
223
+ // — the cast is that gap, not a coercion. Widening the declared option
224
+ // is a separate change with its own consumers.
225
+ filter: composed.filter,
226
+ sort: composed.sort,
227
+ limit: composed.limit,
228
+ ...(fields && fields.length > 0 ? { fields } : {}),
151
229
  });
152
230
  }
153
231
  }
@@ -0,0 +1,113 @@
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
+ /** Sort entry, as both the spec's `SortItem` and objectui's view configs spell it. */
9
+ export interface ElementDataSourceSort {
10
+ field: string;
11
+ order: 'asc' | 'desc';
12
+ }
13
+ /**
14
+ * Element-level data source — matches `@objectstack/spec` `ElementDataSourceSchema`.
15
+ *
16
+ * `filter` is typed `unknown` rather than the spec's `FilterCondition` because
17
+ * three shapes legitimately reach a renderer here (a MongoDB-style condition
18
+ * object, an ObjectQL AST node array, a spec `ViewFilterRule[]`) and
19
+ * {@link mergeFilterNodes} is the single sink that lowers all three. Narrowing
20
+ * the type here would only move the cast, not remove it.
21
+ */
22
+ export interface ElementDataSourceConfig {
23
+ object: string;
24
+ view?: string;
25
+ filter?: unknown;
26
+ sort?: ElementDataSourceSort[];
27
+ limit?: number;
28
+ }
29
+ /**
30
+ * The subset of a saved view's config that an `ElementDataSource` `view` name
31
+ * contributes. Deliberately loose: stored view metadata is user data and carries
32
+ * both the spec vocabulary and the legacy objectui spellings that
33
+ * `normalizeListViewSchema` folds at the component boundary.
34
+ */
35
+ export type ElementSavedView = Record<string, unknown>;
36
+ /** What {@link composeElementDataSource} produces for a renderer to consume. */
37
+ export interface ComposedElementDataSource {
38
+ /** Object to query. */
39
+ object: string;
40
+ /** Columns the named view shows; absent when no view was named. */
41
+ columns?: unknown;
42
+ /** View filter AND binding filter, combined; `undefined` when neither. */
43
+ filter?: unknown;
44
+ /** Binding sort if given, else the view's. */
45
+ sort?: unknown;
46
+ /** Binding limit if given, else the view's page size. */
47
+ limit?: number;
48
+ /** The view's render kind (grid / kanban / …), when the view declares one. */
49
+ viewType?: string;
50
+ }
51
+ /**
52
+ * Is this value the spec's `dataSource` METADATA, rather than a runtime
53
+ * data-source adapter?
54
+ *
55
+ * This guard exists because the two collide by name and the collision was live:
56
+ * `SchemaRenderer` spreads every non-metadata schema key onto the component as a
57
+ * React prop, so a spec-compliant `dataSource: { object, view }` on a page
58
+ * component landed on `ListView`'s `dataSource` PROP — which is the injected
59
+ * adapter — and shadowed it. The first `dataSource.find(…)` then threw
60
+ * `dataSource.find is not a function`, which the list reported as
61
+ * "Couldn't load records": writing the binding the spec documents BROKE the
62
+ * component (objectstack#5576).
63
+ *
64
+ * The test is positive on the metadata rather than negative on the adapter: a
65
+ * binding is a plain object with a non-empty string `object` and no `find`
66
+ * method. An adapter never has a string `object`; metadata never has `find`.
67
+ */
68
+ export declare function isElementDataSourceConfig(value: unknown): value is ElementDataSourceConfig;
69
+ /**
70
+ * Flatten every place saved views for one object can live into `id → config`.
71
+ *
72
+ * Two sources are real and both are read, in the order app-shell's `ObjectView`
73
+ * reads them: the object definition's embedded `listViews` / `list_views` map
74
+ * (metadata-defined) and the metadata overlay rows an adapter returns from
75
+ * `listViews()` (user-defined). Later sources win, so a saved view shadows a
76
+ * metadata view of the same id — the same precedence `ObjectView` applies when
77
+ * it dedups its tab list.
78
+ *
79
+ * Accepts a record keyed by view id or an array of view configs (overlay rows
80
+ * are an array and carry their identity in `name`, falling back to `id`).
81
+ */
82
+ export declare function collectSavedViews(...sources: unknown[]): Record<string, ElementSavedView>;
83
+ /**
84
+ * Find the saved view a `dataSource.view` name refers to.
85
+ *
86
+ * Name matching is {@link resolveViewId}'s — the same short/qualified tolerance
87
+ * the object page applies to a URL-requested view — so a name that works in one
88
+ * surface works in the other. Returns `undefined` when nothing matches; the
89
+ * caller must REPORT that rather than fall back to the object's default view.
90
+ * Silently widening a named view to "all records" is the failure mode this whole
91
+ * binding exists to remove.
92
+ */
93
+ export declare function resolveSavedView(views: Record<string, ElementSavedView>, requested: string | undefined | null, object: string): ElementSavedView | undefined;
94
+ /**
95
+ * Compose an `ElementDataSource` binding with the saved view it names.
96
+ *
97
+ * Pure — the caller resolves the view (see {@link resolveSavedView}) and owns
98
+ * the "named a view that does not exist" decision. Passing `undefined` /`null`
99
+ * for `view` composes a binding with no view, which is exactly the pre-#5576
100
+ * behaviour for the other four keys.
101
+ *
102
+ * See the module doc for the per-key rule table; `filter` is the only key that
103
+ * combines rather than overrides, because the spec describes the binding's
104
+ * filter as *additional*.
105
+ */
106
+ export declare function composeElementDataSource(config: ElementDataSourceConfig, view?: ElementSavedView | null): ComposedElementDataSource;
107
+ /**
108
+ * The message shown when `dataSource.view` names a view the object does not
109
+ * have. Built here so the render path and `ViewDataProvider` report the same
110
+ * defect the same way, and so the known-view list (the thing that actually gets
111
+ * an author unstuck) is never omitted by one caller.
112
+ */
113
+ export declare function elementDataSourceViewNotFoundMessage(object: string, view: string, knownViews: readonly string[]): string;
@@ -0,0 +1,216 @@
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
+ * `@object-ui/core` — ElementDataSource resolution (objectstack#5576).
10
+ *
11
+ * `PageComponentSchema.dataSource` (the spec's `ElementDataSourceSchema`) is the
12
+ * per-element data binding a page component uses to query a different object
13
+ * than the page's own context:
14
+ *
15
+ * ```json
16
+ * { "object": "account", "view": "hot", "filter": { … }, "sort": [ … ], "limit": 20 }
17
+ * ```
18
+ *
19
+ * This module owns the two pure halves of consuming it — telling the METADATA
20
+ * apart from a runtime data-source adapter, and composing a named saved view
21
+ * with the binding's own keys. Both are needed in two places (the render path in
22
+ * `@object-ui/plugin-list`, and `ViewDataProvider.resolveElementDataSource`), so
23
+ * they live here rather than being written twice.
24
+ *
25
+ * ## Composition semantics
26
+ *
27
+ * The spec's own `describe()` text settles the one case that could go either
28
+ * way. `filter` is documented as "**Additional** filter criteria" — additional
29
+ * to what the named view already restricts — so a view filter and a binding
30
+ * filter COMBINE under `and`; the binding can only narrow the view, never widen
31
+ * it. That is also the safe direction for AI-authored metadata: a typo in a
32
+ * per-element filter cannot silently expose rows the saved view excluded.
33
+ *
34
+ * Nothing else in the binding is described as additional, and the remaining keys
35
+ * are single-valued (one ordering, one cap), so for those the rule is **the view
36
+ * provides the baseline and an explicit key overrides it**:
37
+ *
38
+ * | key | source | rule |
39
+ * |------------|-----------------------------------------|-----------------------------|
40
+ * | `object` | binding (required) | binding wins |
41
+ * | `columns` | view only — the binding has no such key | view |
42
+ * | `filter` | view + binding | AND-combined ("additional") |
43
+ * | `sort` | view or binding | binding overrides view |
44
+ * | `limit` | view (`pagination.pageSize`) or binding | binding overrides view |
45
+ * | `viewType` | view only | view |
46
+ *
47
+ * A lone `filter` — only the view has one, or only the binding does — is passed
48
+ * through in the shape it was stored in; only the two-source case is lowered to
49
+ * an ObjectQL AST, because that is what combining requires.
50
+ *
51
+ * `viewType` rides along because `list-view` IS the multi-kind view container
52
+ * (its registry `inputs` enumerate grid/kanban/gallery/…): naming a saved kanban
53
+ * view and rendering a grid would be a silently wrong answer.
54
+ */
55
+ import { mergeFilterNodes } from '../utils/filter-converter.js';
56
+ import { resolveViewId } from '../utils/resolve-view-id.js';
57
+ const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
58
+ /**
59
+ * Is this value the spec's `dataSource` METADATA, rather than a runtime
60
+ * data-source adapter?
61
+ *
62
+ * This guard exists because the two collide by name and the collision was live:
63
+ * `SchemaRenderer` spreads every non-metadata schema key onto the component as a
64
+ * React prop, so a spec-compliant `dataSource: { object, view }` on a page
65
+ * component landed on `ListView`'s `dataSource` PROP — which is the injected
66
+ * adapter — and shadowed it. The first `dataSource.find(…)` then threw
67
+ * `dataSource.find is not a function`, which the list reported as
68
+ * "Couldn't load records": writing the binding the spec documents BROKE the
69
+ * component (objectstack#5576).
70
+ *
71
+ * The test is positive on the metadata rather than negative on the adapter: a
72
+ * binding is a plain object with a non-empty string `object` and no `find`
73
+ * method. An adapter never has a string `object`; metadata never has `find`.
74
+ */
75
+ export function isElementDataSourceConfig(value) {
76
+ if (!isPlainObject(value))
77
+ return false;
78
+ if (typeof value.find === 'function')
79
+ return false;
80
+ const object = value.object;
81
+ return typeof object === 'string' && object.length > 0;
82
+ }
83
+ /**
84
+ * Flatten every place saved views for one object can live into `id → config`.
85
+ *
86
+ * Two sources are real and both are read, in the order app-shell's `ObjectView`
87
+ * reads them: the object definition's embedded `listViews` / `list_views` map
88
+ * (metadata-defined) and the metadata overlay rows an adapter returns from
89
+ * `listViews()` (user-defined). Later sources win, so a saved view shadows a
90
+ * metadata view of the same id — the same precedence `ObjectView` applies when
91
+ * it dedups its tab list.
92
+ *
93
+ * Accepts a record keyed by view id or an array of view configs (overlay rows
94
+ * are an array and carry their identity in `name`, falling back to `id`).
95
+ */
96
+ export function collectSavedViews(...sources) {
97
+ const out = {};
98
+ for (const source of sources) {
99
+ if (Array.isArray(source)) {
100
+ for (const entry of source) {
101
+ if (!isPlainObject(entry))
102
+ continue;
103
+ const id = entry.name ?? entry.id;
104
+ if (typeof id === 'string' && id)
105
+ out[id] = entry;
106
+ }
107
+ }
108
+ else if (isPlainObject(source)) {
109
+ for (const [id, entry] of Object.entries(source)) {
110
+ if (isPlainObject(entry))
111
+ out[id] = entry;
112
+ }
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ /**
118
+ * Find the saved view a `dataSource.view` name refers to.
119
+ *
120
+ * Name matching is {@link resolveViewId}'s — the same short/qualified tolerance
121
+ * the object page applies to a URL-requested view — so a name that works in one
122
+ * surface works in the other. Returns `undefined` when nothing matches; the
123
+ * caller must REPORT that rather than fall back to the object's default view.
124
+ * Silently widening a named view to "all records" is the failure mode this whole
125
+ * binding exists to remove.
126
+ */
127
+ export function resolveSavedView(views, requested, object) {
128
+ const id = resolveViewId(requested, Object.keys(views), object);
129
+ return id === undefined ? undefined : views[id];
130
+ }
131
+ /** Read a saved view's row cap — `pagination.pageSize`, or a flat `limit`. */
132
+ function savedViewLimit(view) {
133
+ if (!view)
134
+ return undefined;
135
+ const pagination = view.pagination;
136
+ if (isPlainObject(pagination) && typeof pagination.pageSize === 'number') {
137
+ return pagination.pageSize;
138
+ }
139
+ return typeof view.limit === 'number' ? view.limit : undefined;
140
+ }
141
+ /**
142
+ * Read a saved view's render kind. `type` is the spec's key (and what
143
+ * `ObjectView` reads); `viewType` is objectui's legacy spelling, folded by
144
+ * `normalizeListViewSchema` downstream but accepted here so a view stored either
145
+ * way resolves to the same kind.
146
+ */
147
+ function savedViewType(view) {
148
+ if (!view)
149
+ return undefined;
150
+ const kind = view.type ?? view.viewType;
151
+ return typeof kind === 'string' && kind ? kind : undefined;
152
+ }
153
+ /** Read a saved view's column list — canonical `columns`, legacy `fields`. */
154
+ function savedViewColumns(view) {
155
+ if (!view)
156
+ return undefined;
157
+ if (Array.isArray(view.columns))
158
+ return view.columns;
159
+ if (Array.isArray(view.fields))
160
+ return view.fields;
161
+ return undefined;
162
+ }
163
+ /**
164
+ * Compose an `ElementDataSource` binding with the saved view it names.
165
+ *
166
+ * Pure — the caller resolves the view (see {@link resolveSavedView}) and owns
167
+ * the "named a view that does not exist" decision. Passing `undefined` /`null`
168
+ * for `view` composes a binding with no view, which is exactly the pre-#5576
169
+ * behaviour for the other four keys.
170
+ *
171
+ * See the module doc for the per-key rule table; `filter` is the only key that
172
+ * combines rather than overrides, because the spec describes the binding's
173
+ * filter as *additional*.
174
+ */
175
+ export function composeElementDataSource(config, view) {
176
+ const composed = { object: config.object };
177
+ const columns = savedViewColumns(view);
178
+ if (columns !== undefined)
179
+ composed.columns = columns;
180
+ // "Additional filter criteria" (spec) — the view restricts, the binding
181
+ // restricts further, so two filters COMBINE under `and` via the repo's single
182
+ // combiner. A lone source passes through VERBATIM: lowering a stored filter to
183
+ // an ObjectQL AST belongs at the last hop before the wire and nowhere earlier
184
+ // (see `toFilterNode`'s doc on why), and every consumer of this result hands
185
+ // the value to that hop anyway. Combining is the one case that cannot preserve
186
+ // the input shape — putting two sources under one `and` requires AST — and
187
+ // `mergeFilterNodes` is the sanctioned place for it.
188
+ const viewFilter = view?.filter;
189
+ const filter = viewFilter !== undefined && config.filter !== undefined
190
+ ? mergeFilterNodes(viewFilter, config.filter)
191
+ : (config.filter ?? viewFilter);
192
+ if (filter !== undefined)
193
+ composed.filter = filter;
194
+ const sort = config.sort ?? view?.sort;
195
+ if (sort !== undefined)
196
+ composed.sort = sort;
197
+ const limit = config.limit ?? savedViewLimit(view);
198
+ if (limit !== undefined)
199
+ composed.limit = limit;
200
+ const viewType = savedViewType(view);
201
+ if (viewType !== undefined)
202
+ composed.viewType = viewType;
203
+ return composed;
204
+ }
205
+ /**
206
+ * The message shown when `dataSource.view` names a view the object does not
207
+ * have. Built here so the render path and `ViewDataProvider` report the same
208
+ * defect the same way, and so the known-view list (the thing that actually gets
209
+ * an author unstuck) is never omitted by one caller.
210
+ */
211
+ export function elementDataSourceViewNotFoundMessage(object, view, knownViews) {
212
+ const known = knownViews.length
213
+ ? `Known views: ${[...knownViews].sort().join(', ')}.`
214
+ : 'This object has no saved views.';
215
+ return `dataSource.view "${view}" was not found on object "${object}". ${known}`;
216
+ }
@@ -8,4 +8,5 @@
8
8
  * @packageDocumentation
9
9
  */
10
10
  export { DataScopeManager, defaultDataScopeManager, type RowLevelFilter, type DataScopeConfig, } from './DataScopeManager.js';
11
- export { ViewDataProvider, type ViewDataConfig, type ElementDataSourceConfig, type DataFetcher, type ResolvedData, } from './ViewDataProvider.js';
11
+ export { ViewDataProvider, type ViewDataConfig, type DataFetcher, type ResolvedData, } from './ViewDataProvider.js';
12
+ export { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, type ComposedElementDataSource, type ElementDataSourceConfig, type ElementDataSourceSort, type ElementSavedView, } from './element-data-source.js';
@@ -9,3 +9,4 @@
9
9
  */
10
10
  export { DataScopeManager, defaultDataScopeManager, } from './DataScopeManager.js';
11
11
  export { ViewDataProvider, } from './ViewDataProvider.js';
12
+ export { collectSavedViews, composeElementDataSource, elementDataSourceViewNotFoundMessage, isElementDataSourceConfig, resolveSavedView, } from './element-data-source.js';