@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.
- package/CHANGELOG.md +2018 -0
- package/README.md +38 -34
- package/dist/actions/ActionEngine.d.ts +43 -26
- package/dist/actions/ActionEngine.js +67 -54
- package/dist/actions/ActionRunner.d.ts +281 -31
- package/dist/actions/ActionRunner.js +458 -36
- package/dist/actions/actionKeys.d.ts +163 -27
- package/dist/actions/actionKeys.js +238 -27
- package/dist/actions/actionResponse.js +1 -1
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordIdParam.d.ts +77 -0
- package/dist/actions/recordIdParam.js +37 -0
- package/dist/actions/serverActionHandler.d.ts +1 -1
- package/dist/actions/serverActionHandler.js +1 -1
- package/dist/data-scope/ViewDataProvider.d.ts +37 -12
- package/dist/data-scope/ViewDataProvider.js +83 -5
- package/dist/data-scope/element-data-source.d.ts +113 -0
- package/dist/data-scope/element-data-source.js +216 -0
- package/dist/data-scope/index.d.ts +2 -1
- package/dist/data-scope/index.js +1 -0
- package/dist/evaluator/ExpressionEvaluator.d.ts +42 -1
- package/dist/evaluator/ExpressionEvaluator.js +42 -1
- package/dist/evaluator/SafeExpressionParser.js +0 -4
- package/dist/evaluator/declaredPredicate.d.ts +119 -0
- package/dist/evaluator/declaredPredicate.js +152 -0
- package/dist/evaluator/fieldRules.d.ts +35 -0
- package/dist/evaluator/fieldRules.js +79 -20
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +54 -4
- package/dist/evaluator/listConditional.js +125 -19
- package/dist/evaluator/optionRules.d.ts +55 -0
- package/dist/evaluator/optionRules.js +60 -0
- package/dist/evaluator/predicateInput.d.ts +51 -3
- package/dist/evaluator/predicateInput.js +62 -5
- package/dist/evaluator/rowPredicateCanon.d.ts +67 -0
- package/dist/evaluator/rowPredicateCanon.js +208 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +31 -1
- package/dist/protocols/DndProtocol.d.ts +42 -1
- package/dist/protocols/KeyboardProtocol.d.ts +31 -1
- package/dist/protocols/SharingProtocol.d.ts +11 -1
- package/dist/registry/Registry.d.ts +62 -16
- package/dist/theme/ThemeEngine.d.ts +0 -8
- package/dist/theme/ThemeEngine.js +22 -103
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +7 -1
- package/dist/types/index.d.ts +125 -12
- package/dist/utils/chart-presentation.d.ts +205 -0
- package/dist/utils/chart-presentation.js +280 -0
- package/dist/utils/chart-series.d.ts +390 -23
- package/dist/utils/chart-series.js +776 -32
- package/dist/utils/column-identity.d.ts +43 -0
- package/dist/utils/column-identity.js +47 -1
- package/dist/utils/compare-to.d.ts +40 -13
- package/dist/utils/compare-to.js +4 -32
- package/dist/utils/dashboard-filters.d.ts +54 -8
- package/dist/utils/dashboard-filters.js +261 -12
- package/dist/utils/dataset-format.d.ts +73 -14
- package/dist/utils/dataset-format.js +142 -9
- package/dist/utils/dataset-pivot.d.ts +28 -7
- package/dist/utils/dataset-pivot.js +27 -6
- package/dist/utils/debug.js +7 -0
- package/dist/utils/dom-props.d.ts +137 -0
- package/dist/utils/dom-props.js +153 -0
- package/dist/utils/expand-fields.d.ts +97 -7
- package/dist/utils/expand-fields.js +97 -7
- package/dist/utils/filter-tokens.js +0 -1
- package/dist/utils/freeze-schema.d.ts +38 -1
- package/dist/utils/freeze-schema.js +10 -0
- package/dist/utils/humanize-label.d.ts +60 -0
- package/dist/utils/humanize-label.js +62 -0
- package/dist/utils/managedBy.d.ts +27 -2
- package/dist/utils/normalize-list-view.d.ts +36 -6
- package/dist/utils/normalize-list-view.js +93 -9
- package/dist/utils/number-display.d.ts +155 -0
- package/dist/utils/number-display.js +71 -0
- package/dist/utils/predicate-fields.d.ts +29 -4
- package/dist/utils/predicate-fields.js +44 -4
- package/dist/utils/resolve-view-id.d.ts +31 -0
- package/dist/utils/resolve-view-id.js +45 -0
- package/dist/utils/retired-field-types.d.ts +90 -0
- package/dist/utils/retired-field-types.js +124 -0
- package/dist/utils/sort-query.d.ts +57 -0
- package/dist/utils/sort-query.js +35 -0
- package/dist/utils/unmaterialized-fields.d.ts +52 -0
- package/dist/utils/unmaterialized-fields.js +57 -0
- package/dist/validation/index.d.ts +1 -0
- package/dist/validation/index.js +1 -0
- package/dist/validation/schema-validator.js +105 -0
- package/dist/validation/server-owned-value.d.ts +62 -0
- package/dist/validation/server-owned-value.js +99 -0
- package/dist/validation/validators/object-validation-engine.js +0 -2
- package/package.json +5 -5
- package/dist/__benchmarks__/core.bench.d.ts +0 -8
- package/dist/__benchmarks__/core.bench.js +0 -53
- package/dist/utils/merge-views-into-objects.d.ts +0 -19
- 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
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
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';
|
package/dist/data-scope/index.js
CHANGED
|
@@ -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';
|