@object-ui/core 17.2.0 → 17.3.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 +212 -0
- package/README.md +29 -0
- package/dist/actions/ActionEngine.d.ts +11 -6
- package/dist/actions/ActionEngine.js +26 -19
- package/dist/actions/ActionRunner.d.ts +16 -3
- package/dist/actions/ActionRunner.js +2 -1
- package/dist/actions/actionErrorDetail.d.ts +26 -0
- package/dist/actions/actionErrorDetail.js +37 -0
- package/dist/actions/actionKeys.d.ts +1 -1
- package/dist/actions/actionKeys.js +11 -0
- package/dist/actions/actionResponse.d.ts +42 -0
- package/dist/actions/actionResponse.js +104 -0
- package/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +3 -0
- package/dist/actions/serverActionHandler.d.ts +143 -0
- package/dist/actions/serverActionHandler.js +137 -0
- package/dist/evaluator/ExpressionEvaluator.js +4 -2
- package/dist/evaluator/fieldRules.d.ts +26 -2
- package/dist/evaluator/fieldRules.js +63 -8
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -0
- package/dist/evaluator/listConditional.d.ts +13 -1
- package/dist/evaluator/listConditional.js +0 -0
- package/dist/evaluator/optionRules.js +5 -1
- package/dist/evaluator/predicateInput.d.ts +84 -0
- package/dist/evaluator/predicateInput.js +72 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +12 -0
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +8 -1
- package/dist/utils/dataset-format.d.ts +6 -1
- package/dist/utils/dataset-pivot.d.ts +57 -0
- package/dist/utils/dataset-pivot.js +57 -0
- package/dist/utils/filter-converter.d.ts +39 -11
- package/dist/utils/filter-converter.js +87 -7
- package/dist/utils/predicate-fields.d.ts +96 -0
- package/dist/utils/predicate-fields.js +163 -0
- package/dist/utils/predicate-record.d.ts +32 -0
- package/dist/utils/predicate-record.js +120 -0
- package/dist/utils/record-title.js +0 -0
- package/package.json +4 -4
- package/dist/protocols/NotificationProtocol.d.ts +0 -71
- package/dist/protocols/NotificationProtocol.js +0 -89
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The normalized shape {@link ExpressionEvaluator.evaluateCondition} consumes:
|
|
10
|
+
* a boolean (short-circuits), a `${…}` template string (legacy JS path), a
|
|
11
|
+
* `{ dialect: 'cel', source }` envelope (canonical `@objectstack/formula`
|
|
12
|
+
* path), or `undefined` for "no predicate declared".
|
|
13
|
+
*
|
|
14
|
+
* ## Why not `PredicateInput` (objectstack#4115 / objectui#3074)
|
|
15
|
+
*
|
|
16
|
+
* `@objectstack/spec` owns `PredicateInput` — and it is a genuinely DIFFERENT
|
|
17
|
+
* concept, so wearing that name would be the exact drift this repo's
|
|
18
|
+
* `check:spec-symbols` gate exists to stop. The spec's is
|
|
19
|
+
* `z.input<typeof PredicateInputSchema>`: what an author WRITES, before
|
|
20
|
+
* normalization — a bare string, or an envelope over the full dialect set
|
|
21
|
+
* (`cel` | `cron` | `template`) with optional `source` / `ast` / `meta`.
|
|
22
|
+
*
|
|
23
|
+
* This one is the OUTPUT of normalization: what the evaluator ACCEPTS. It
|
|
24
|
+
* admits `boolean` and the `${…}` template spelling (neither of which the spec
|
|
25
|
+
* models as a predicate), narrows `dialect` to `cel` alone (every other dialect
|
|
26
|
+
* has already been flattened onto the legacy path by `toPredicateInput`),
|
|
27
|
+
* requires a non-empty `source`, and carries no `ast` / `meta`. The two unions
|
|
28
|
+
* are not mutually assignable in either direction.
|
|
29
|
+
*
|
|
30
|
+
* The rename is pinned from both sides by the tripwire in
|
|
31
|
+
* `packages/core/src/utils/__tests__/spec-symbol-batch4.test.ts`: the spec must
|
|
32
|
+
* still own `PredicateInput` (else this rename has lost its reason), and must
|
|
33
|
+
* still NOT own `EvaluatorPredicateInput` (the objectui#3074 mistake of
|
|
34
|
+
* renaming onto another name the spec already holds).
|
|
35
|
+
*/
|
|
36
|
+
export type EvaluatorPredicateInput = string | boolean | {
|
|
37
|
+
dialect: 'cel';
|
|
38
|
+
source: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
|
|
42
|
+
* `hidden`) into the form {@link ExpressionEvaluator.evaluateCondition}
|
|
43
|
+
* expects.
|
|
44
|
+
*
|
|
45
|
+
* Accepts:
|
|
46
|
+
* - `boolean` → returned as-is (predicate evaluation short-circuits).
|
|
47
|
+
* - `string` → wrapped as `${string}` (legacy DX shorthand).
|
|
48
|
+
* - `Expression` envelope `{ dialect, source }` (the normalized form
|
|
49
|
+
* `@objectstack/spec`'s `ExpressionInputSchema` produces for every
|
|
50
|
+
* authored predicate, including bare strings) → a `cel` dialect keeps
|
|
51
|
+
* its envelope; every other dialect is unwrapped and wrapped as
|
|
52
|
+
* `${source}`.
|
|
53
|
+
* - `null` / `undefined` / empty / anything else → `undefined`
|
|
54
|
+
* (default visible/enabled).
|
|
55
|
+
*
|
|
56
|
+
* ## Why the `cel` envelope must survive normalization (#2661 / #3314)
|
|
57
|
+
*
|
|
58
|
+
* `evaluateCondition` routes to the canonical `@objectstack/formula` engine —
|
|
59
|
+
* the one the SERVER enforces with — only while the argument is still a
|
|
60
|
+
* `{ dialect: 'cel' }` object. Collapse it to `${source}` first and the
|
|
61
|
+
* predicate silently falls to the legacy JS evaluator, whose semantics
|
|
62
|
+
* differ: CEL has no `<` overload for `null`, so `null < null` faults and
|
|
63
|
+
* fail-closed hides, while JS quietly yields `false`; CEL-only builtins
|
|
64
|
+
* (`today()`, objectstack#3205) do not exist on the JS path at all. Two
|
|
65
|
+
* normalizers that disagree on this one branch therefore make the SAME
|
|
66
|
+
* `visible:` predicate reach different verdicts depending on whether the
|
|
67
|
+
* action was surfaced by `ActionEngine.getActionsForLocation` or rendered
|
|
68
|
+
* standalone (#3314).
|
|
69
|
+
*
|
|
70
|
+
* This is THE implementation, not one of two — `@object-ui/core` is the common
|
|
71
|
+
* dependency of every consumer, so engine-side code and renderer-side code
|
|
72
|
+
* share one normalization instead of hand-rolling envelope unwrapping per call
|
|
73
|
+
* site. `@object-ui/react`'s `toPredicateInput` is a re-export of this function
|
|
74
|
+
* (since #3367; it used to be an independent twin held in step by a 14-shape
|
|
75
|
+
* normalization parity table, which is exactly the arrangement the paragraph
|
|
76
|
+
* above describes the failure mode of). What pins that now is the identity
|
|
77
|
+
* assertion in
|
|
78
|
+
* `packages/react/src/hooks/__tests__/actionPredicate.parity.test.tsx` — the
|
|
79
|
+
* react export must BE this function object — alongside the engine-path vs
|
|
80
|
+
* renderer-path verdict parity suite in the same file, which is a separate
|
|
81
|
+
* claim and still earns its keep: sharing a normalizer does not by itself
|
|
82
|
+
* prove the two call paths reach the same verdict.
|
|
83
|
+
*/
|
|
84
|
+
export declare function toPredicateInput(value: unknown): EvaluatorPredicateInput;
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
* Normalize a schema-supplied predicate (`visible` / `enabled` / `disabled` /
|
|
10
|
+
* `hidden`) into the form {@link ExpressionEvaluator.evaluateCondition}
|
|
11
|
+
* expects.
|
|
12
|
+
*
|
|
13
|
+
* Accepts:
|
|
14
|
+
* - `boolean` → returned as-is (predicate evaluation short-circuits).
|
|
15
|
+
* - `string` → wrapped as `${string}` (legacy DX shorthand).
|
|
16
|
+
* - `Expression` envelope `{ dialect, source }` (the normalized form
|
|
17
|
+
* `@objectstack/spec`'s `ExpressionInputSchema` produces for every
|
|
18
|
+
* authored predicate, including bare strings) → a `cel` dialect keeps
|
|
19
|
+
* its envelope; every other dialect is unwrapped and wrapped as
|
|
20
|
+
* `${source}`.
|
|
21
|
+
* - `null` / `undefined` / empty / anything else → `undefined`
|
|
22
|
+
* (default visible/enabled).
|
|
23
|
+
*
|
|
24
|
+
* ## Why the `cel` envelope must survive normalization (#2661 / #3314)
|
|
25
|
+
*
|
|
26
|
+
* `evaluateCondition` routes to the canonical `@objectstack/formula` engine —
|
|
27
|
+
* the one the SERVER enforces with — only while the argument is still a
|
|
28
|
+
* `{ dialect: 'cel' }` object. Collapse it to `${source}` first and the
|
|
29
|
+
* predicate silently falls to the legacy JS evaluator, whose semantics
|
|
30
|
+
* differ: CEL has no `<` overload for `null`, so `null < null` faults and
|
|
31
|
+
* fail-closed hides, while JS quietly yields `false`; CEL-only builtins
|
|
32
|
+
* (`today()`, objectstack#3205) do not exist on the JS path at all. Two
|
|
33
|
+
* normalizers that disagree on this one branch therefore make the SAME
|
|
34
|
+
* `visible:` predicate reach different verdicts depending on whether the
|
|
35
|
+
* action was surfaced by `ActionEngine.getActionsForLocation` or rendered
|
|
36
|
+
* standalone (#3314).
|
|
37
|
+
*
|
|
38
|
+
* This is THE implementation, not one of two — `@object-ui/core` is the common
|
|
39
|
+
* dependency of every consumer, so engine-side code and renderer-side code
|
|
40
|
+
* share one normalization instead of hand-rolling envelope unwrapping per call
|
|
41
|
+
* site. `@object-ui/react`'s `toPredicateInput` is a re-export of this function
|
|
42
|
+
* (since #3367; it used to be an independent twin held in step by a 14-shape
|
|
43
|
+
* normalization parity table, which is exactly the arrangement the paragraph
|
|
44
|
+
* above describes the failure mode of). What pins that now is the identity
|
|
45
|
+
* assertion in
|
|
46
|
+
* `packages/react/src/hooks/__tests__/actionPredicate.parity.test.tsx` — the
|
|
47
|
+
* react export must BE this function object — alongside the engine-path vs
|
|
48
|
+
* renderer-path verdict parity suite in the same file, which is a separate
|
|
49
|
+
* claim and still earns its keep: sharing a normalizer does not by itself
|
|
50
|
+
* prove the two call paths reach the same verdict.
|
|
51
|
+
*/
|
|
52
|
+
export function toPredicateInput(value) {
|
|
53
|
+
if (value === null || value === undefined || value === '')
|
|
54
|
+
return undefined;
|
|
55
|
+
if (typeof value === 'boolean')
|
|
56
|
+
return value;
|
|
57
|
+
if (typeof value === 'string')
|
|
58
|
+
return `\${${value}}`;
|
|
59
|
+
if (typeof value === 'object' && typeof value.source === 'string') {
|
|
60
|
+
const src = value.source;
|
|
61
|
+
if (!src)
|
|
62
|
+
return undefined;
|
|
63
|
+
// Preserve a CEL-dialect envelope so `evaluateCondition` routes it to the
|
|
64
|
+
// canonical `@objectstack/formula` engine (identical verdict to the
|
|
65
|
+
// server) instead of collapsing it onto the legacy JS path.
|
|
66
|
+
if (value.dialect === 'cel')
|
|
67
|
+
return { dialect: 'cel', source: src };
|
|
68
|
+
// Every other dialect (template / unset) keeps the legacy `${…}` behavior.
|
|
69
|
+
return `\${${src}}`;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -48,7 +48,10 @@ export * from './utils/merge-filters.js';
|
|
|
48
48
|
export * from './utils/compare-to.js';
|
|
49
49
|
export * from './utils/chart-series.js';
|
|
50
50
|
export * from './utils/dataset-format.js';
|
|
51
|
+
export * from './utils/dataset-pivot.js';
|
|
51
52
|
export * from './utils/record-title.js';
|
|
52
53
|
export * from './utils/export-filename.js';
|
|
53
54
|
export * from './utils/reference-keys.js';
|
|
55
|
+
export * from './utils/predicate-record.js';
|
|
56
|
+
export * from './utils/predicate-fields.js';
|
|
54
57
|
export * from './utils/normalize-list-view.js';
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,19 @@ export * from './utils/merge-filters.js';
|
|
|
50
50
|
export * from './utils/compare-to.js';
|
|
51
51
|
export * from './utils/chart-series.js';
|
|
52
52
|
export * from './utils/dataset-format.js';
|
|
53
|
+
// Pivot lookup-key encoders, shared by every cross-tab renderer so the
|
|
54
|
+
// dashboard widget and the report renderer key their buckets identically
|
|
55
|
+
// (objectstack#5473 / objectstack#5665).
|
|
56
|
+
export * from './utils/dataset-pivot.js';
|
|
53
57
|
export * from './utils/record-title.js';
|
|
54
58
|
export * from './utils/export-filename.js';
|
|
55
59
|
export * from './utils/reference-keys.js';
|
|
60
|
+
// Binds a fetched record into an expression scope the way the SERVER binds it
|
|
61
|
+
// (a relation is its foreign key, never the expanded record) — see
|
|
62
|
+
// `toPredicateRecord` for why an unnormalized one gives the same predicate
|
|
63
|
+
// different verdicts on different surfaces.
|
|
64
|
+
export * from './utils/predicate-record.js';
|
|
65
|
+
// The other half of a view's field appetite: the fields its PREDICATES read,
|
|
66
|
+
// which the column-derived `$select` never asked the server for.
|
|
67
|
+
export * from './utils/predicate-fields.js';
|
|
56
68
|
export * from './utils/normalize-list-view.js';
|
package/dist/protocols/index.js
CHANGED
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
export * from './DndProtocol.js';
|
|
9
9
|
export * from './KeyboardProtocol.js';
|
|
10
|
-
|
|
10
|
+
// `NotificationProtocol` removed in the 17.0.0-rc.2 uptake: it bridged
|
|
11
|
+
// `@objectstack/spec/ui`'s `Notification` / `NotificationConfig`, which
|
|
12
|
+
// objectstack#4610 deleted with no successor. The bridge had zero consumers in
|
|
13
|
+
// this repo — the live notification config resolution is
|
|
14
|
+
// `resolveNotificationConfig` in `@object-ui/react`'s `NotificationContext`,
|
|
15
|
+
// which declares its own `NotificationSystemConfig` and is what every surface
|
|
16
|
+
// reads. Re-typing the bridge would have meant re-declaring a vocabulary the
|
|
17
|
+
// spec had just retired (AGENTS.md #0.1).
|
|
11
18
|
export * from './ResponsiveProtocol.js';
|
|
12
19
|
export * from './SharingProtocol.js';
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* Pure (no React / i18n) so it can live in `@object-ui/core` and be imported
|
|
16
16
|
* by both `@object-ui/plugin-dashboard` and `@object-ui/plugin-report`.
|
|
17
17
|
*/
|
|
18
|
+
import type { PercentScale } from '@objectstack/spec/data';
|
|
18
19
|
/**
|
|
19
20
|
* Column metadata the analytics server returns alongside the rows: a display
|
|
20
21
|
* `label` for both dimensions and measures, plus a measure's numeral `format`
|
|
@@ -38,8 +39,12 @@ export interface DatasetResultField {
|
|
|
38
39
|
* field's scale) and carried on the result column, so display never has to
|
|
39
40
|
* infer it from the value's magnitude — the inference that printed a ratio of
|
|
40
41
|
* exactly 1 as "1.0%" (#3136).
|
|
42
|
+
*
|
|
43
|
+
* Spec-owned since 17.0.0-rc.2 (`@objectstack/spec/data` exports the identical
|
|
44
|
+
* union); re-exported here so existing `@object-ui/core` consumers keep their
|
|
45
|
+
* import path.
|
|
41
46
|
*/
|
|
42
|
-
export type PercentScale
|
|
47
|
+
export type { PercentScale } from '@objectstack/spec/data';
|
|
43
48
|
/**
|
|
44
49
|
* Scale a stored `percent`-field value to its DISPLAY magnitude.
|
|
45
50
|
*
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
* dataset-pivot — the lookup-key encoders every cross-tab over a semantic-layer
|
|
10
|
+
* `queryDataset` result shares (ADR-0021).
|
|
11
|
+
*
|
|
12
|
+
* A pivot keys three lookups off dimension-value tuples: the DOWN bucket, the
|
|
13
|
+
* ACROSS bucket, and the cell where the two meet. Every one of them used to be
|
|
14
|
+
* spelled by concatenating values with a character the data was ASSUMED never
|
|
15
|
+
* to contain — an empty string, a plain space, U+0001 — and each assumption
|
|
16
|
+
* failed on ordinary data: `"x"` + `"yz"` and `"xy"` + `"z"` are one id under an
|
|
17
|
+
* empty join, and `"New"` × `"York Q1"` and `"New York"` × `"Q1"` are one cell
|
|
18
|
+
* key under a space join. The later row silently overwrote the earlier one, so
|
|
19
|
+
* the cell showed a different row's measure, the overwritten row's value was
|
|
20
|
+
* unreachable, and drill-through followed the same wrong index into the wrong
|
|
21
|
+
* records — all without an error (objectstack#5473, objectstack#5665).
|
|
22
|
+
*
|
|
23
|
+
* `JSON.stringify` carries the boundary in its own quoting rather than in a
|
|
24
|
+
* character the values must avoid, so these are unambiguous for ANY value a
|
|
25
|
+
* dimension can hold. The ids are opaque lookup keys, never displayed — visible
|
|
26
|
+
* text comes from the separately-formatted labels.
|
|
27
|
+
*
|
|
28
|
+
* Both encoders live here, in `@object-ui/core`, because the same pivot is
|
|
29
|
+
* rendered by two packages (`plugin-dashboard`'s `DatasetWidget` and
|
|
30
|
+
* `plugin-report`'s `DatasetReportRenderer`). Each having written its own is
|
|
31
|
+
* exactly why both carried the defect and why fixing one left the other broken.
|
|
32
|
+
* The same rule applies WITHIN a renderer: the cell index and the subtotal
|
|
33
|
+
* lookups key the same buckets, so they must all call these — a second
|
|
34
|
+
* hand-rolled encoding of the same id agrees with the first only for the
|
|
35
|
+
* dimension counts someone happened to test, which is what kept the original
|
|
36
|
+
* bug invisible.
|
|
37
|
+
*
|
|
38
|
+
* Pure (no React / i18n), like its `dataset-format` neighbour.
|
|
39
|
+
*
|
|
40
|
+
* Known residual, tracked separately in objectstack#5666: callers encode a
|
|
41
|
+
* null/undefined dimension value as a placeholder string, so it still collides
|
|
42
|
+
* with a value that literally equals that placeholder. That is a property of
|
|
43
|
+
* the placeholder, not of the encoding below.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Encode a pivot BUCKET id from its dimension values.
|
|
47
|
+
*
|
|
48
|
+
* Axis-neutral on purpose: a DOWN bucket and an ACROSS bucket are the same kind
|
|
49
|
+
* of thing (a dimension-value tuple), and a cross-tab with multiple across
|
|
50
|
+
* dimensions collides on that axis just as readily as on the down axis.
|
|
51
|
+
*/
|
|
52
|
+
export declare const pivotBucketId: (dimensionValues: string[]) => string;
|
|
53
|
+
/**
|
|
54
|
+
* Encode the cell key for a (down bucket, across bucket) pair — the key of the
|
|
55
|
+
* map a renderer reads to place a measure and to resolve a drill-through.
|
|
56
|
+
*/
|
|
57
|
+
export declare const pivotCellKey: (rowId: string, colId: string) => string;
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
* dataset-pivot — the lookup-key encoders every cross-tab over a semantic-layer
|
|
10
|
+
* `queryDataset` result shares (ADR-0021).
|
|
11
|
+
*
|
|
12
|
+
* A pivot keys three lookups off dimension-value tuples: the DOWN bucket, the
|
|
13
|
+
* ACROSS bucket, and the cell where the two meet. Every one of them used to be
|
|
14
|
+
* spelled by concatenating values with a character the data was ASSUMED never
|
|
15
|
+
* to contain — an empty string, a plain space, U+0001 — and each assumption
|
|
16
|
+
* failed on ordinary data: `"x"` + `"yz"` and `"xy"` + `"z"` are one id under an
|
|
17
|
+
* empty join, and `"New"` × `"York Q1"` and `"New York"` × `"Q1"` are one cell
|
|
18
|
+
* key under a space join. The later row silently overwrote the earlier one, so
|
|
19
|
+
* the cell showed a different row's measure, the overwritten row's value was
|
|
20
|
+
* unreachable, and drill-through followed the same wrong index into the wrong
|
|
21
|
+
* records — all without an error (objectstack#5473, objectstack#5665).
|
|
22
|
+
*
|
|
23
|
+
* `JSON.stringify` carries the boundary in its own quoting rather than in a
|
|
24
|
+
* character the values must avoid, so these are unambiguous for ANY value a
|
|
25
|
+
* dimension can hold. The ids are opaque lookup keys, never displayed — visible
|
|
26
|
+
* text comes from the separately-formatted labels.
|
|
27
|
+
*
|
|
28
|
+
* Both encoders live here, in `@object-ui/core`, because the same pivot is
|
|
29
|
+
* rendered by two packages (`plugin-dashboard`'s `DatasetWidget` and
|
|
30
|
+
* `plugin-report`'s `DatasetReportRenderer`). Each having written its own is
|
|
31
|
+
* exactly why both carried the defect and why fixing one left the other broken.
|
|
32
|
+
* The same rule applies WITHIN a renderer: the cell index and the subtotal
|
|
33
|
+
* lookups key the same buckets, so they must all call these — a second
|
|
34
|
+
* hand-rolled encoding of the same id agrees with the first only for the
|
|
35
|
+
* dimension counts someone happened to test, which is what kept the original
|
|
36
|
+
* bug invisible.
|
|
37
|
+
*
|
|
38
|
+
* Pure (no React / i18n), like its `dataset-format` neighbour.
|
|
39
|
+
*
|
|
40
|
+
* Known residual, tracked separately in objectstack#5666: callers encode a
|
|
41
|
+
* null/undefined dimension value as a placeholder string, so it still collides
|
|
42
|
+
* with a value that literally equals that placeholder. That is a property of
|
|
43
|
+
* the placeholder, not of the encoding below.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Encode a pivot BUCKET id from its dimension values.
|
|
47
|
+
*
|
|
48
|
+
* Axis-neutral on purpose: a DOWN bucket and an ACROSS bucket are the same kind
|
|
49
|
+
* of thing (a dimension-value tuple), and a cross-tab with multiple across
|
|
50
|
+
* dimensions collides on that axis just as readily as on the down axis.
|
|
51
|
+
*/
|
|
52
|
+
export const pivotBucketId = (dimensionValues) => JSON.stringify(dimensionValues);
|
|
53
|
+
/**
|
|
54
|
+
* Encode the cell key for a (down bucket, across bucket) pair — the key of the
|
|
55
|
+
* map a renderer reads to place a measure and to resolve a drill-through.
|
|
56
|
+
*/
|
|
57
|
+
export const pivotCellKey = (rowId, colId) => JSON.stringify([rowId, colId]);
|
|
@@ -5,12 +5,6 @@
|
|
|
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
|
-
* Filter Converter Utilities
|
|
10
|
-
*
|
|
11
|
-
* Shared utilities for converting MongoDB-like filter operators
|
|
12
|
-
* to ObjectStack FilterNode AST format.
|
|
13
|
-
*/
|
|
14
8
|
/**
|
|
15
9
|
* FilterNode AST type definition
|
|
16
10
|
* Represents a filter condition or a logical combination of conditions
|
|
@@ -83,6 +77,36 @@ export declare function convertFiltersToAST(filter: Record<string, any>): Filter
|
|
|
83
77
|
* `table.defaultFilters` (declared `Record<string, any>`) was DROPPED and the
|
|
84
78
|
* view returned every record. Silently: no error, just a wider answer.
|
|
85
79
|
*
|
|
80
|
+
* The FIRST never worked at all (objectui#3431). The array branch returned
|
|
81
|
+
* every array VERBATIM, so a saved view's `ViewFilterRule[]` travelled to
|
|
82
|
+
* `$filter` as bare rule objects — which the server refuses: `isFilterAST` is
|
|
83
|
+
* false for an array of objects, and the wire face answers `400
|
|
84
|
+
* INVALID_FILTER`. Verified against a real backend on the showcase's SHIPPED
|
|
85
|
+
* `showcase_task.in_progress` view (`filter: [{field:'status',
|
|
86
|
+
* operator:'equals', value:'in_progress'}]`): 400 as sent, 200 with its 2 rows
|
|
87
|
+
* once lowered to `[['status','equals','in_progress']]`. Every saved view
|
|
88
|
+
* carrying a filter was a failed list. `mergeFilterNodes` below has warned
|
|
89
|
+
* about exactly this hazard since it was written — the warning was accurate,
|
|
90
|
+
* and the sink it warns for never implemented the lowering it describes.
|
|
91
|
+
*
|
|
92
|
+
* **Why the fold lives HERE and not at the producer.** `ListViewSchema.filter`
|
|
93
|
+
* / `ViewTab.filter` are spec-declared `z.array(ViewFilterRuleSchema)`, and a
|
|
94
|
+
* view hands a renderer a `ListViewSchema` — so folding one hop earlier (in
|
|
95
|
+
* `app-shell`'s ObjectView, say) would write ObjectQL AST triples INTO a
|
|
96
|
+
* spec-declared rule-array slot: an off-spec value in a spec field, AGENTS.md
|
|
97
|
+
* #0.1 inverted. This function is the last hop before the wire, where the value
|
|
98
|
+
* legitimately leaves the spec's view vocabulary and becomes an ObjectQL AST.
|
|
99
|
+
* It is also the single sink both producers already share — `plugin-list`'s
|
|
100
|
+
* `buildEffectiveFilter` (which feeds the grid AND the export) and
|
|
101
|
+
* `plugin-view`'s ObjectView (calendar / kanban / gallery / timeline). One
|
|
102
|
+
* lowering, one place; the same reason the MongoDB-style shape is lowered here
|
|
103
|
+
* rather than at each of its callers.
|
|
104
|
+
*
|
|
105
|
+
* Mixed arrays fold ELEMENT-WISE, because that is what reaches this function in
|
|
106
|
+
* practice: `ObjectView` concatenates a saved view's rules with the
|
|
107
|
+
* `?filter[<field>]=<value>` URL triples into one array. Triples pass through
|
|
108
|
+
* untouched.
|
|
109
|
+
*
|
|
86
110
|
* Returns `undefined` for an absent or empty source, so callers can skip
|
|
87
111
|
* `$filter` rather than sending an empty array.
|
|
88
112
|
*/
|
|
@@ -92,11 +116,15 @@ export declare function toFilterNode(source: unknown): FilterNode | Record<strin
|
|
|
92
116
|
*
|
|
93
117
|
* Wrapping rather than spreading, on purpose. `['and', ...rules]` looks
|
|
94
118
|
* equivalent and is not: spreading a `ViewFilterRule[]` puts bare rule OBJECTS
|
|
95
|
-
* where the AST expects nodes, and the server
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
119
|
+
* where the AST expects nodes, and the server does not accept that —
|
|
120
|
+
* `isFilterAST` says no and the wire face answers `400 INVALID_FILTER`
|
|
121
|
+
* (objectstack#4121). Spreading is only correct when the source happens to be
|
|
122
|
+
* an array of nodes, which is why it survived.
|
|
123
|
+
*
|
|
124
|
+
* Since objectui#3431 the rule objects never reach that position anyway:
|
|
125
|
+
* `toFilterNode` lowers a `ViewFilterRule[]` to AST nodes on the way in. The
|
|
126
|
+
* paragraph above is kept because it is why each source stays its own child —
|
|
127
|
+
* and because it correctly diagnosed the bug the sink itself was carrying.
|
|
100
128
|
*
|
|
101
129
|
* Sources that normalize to nothing are skipped; one surviving source is
|
|
102
130
|
* returned as-is rather than wrapped in a pointless `and`.
|
|
@@ -5,6 +5,13 @@
|
|
|
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
|
+
* Filter Converter Utilities
|
|
10
|
+
*
|
|
11
|
+
* Shared utilities for converting MongoDB-like filter operators
|
|
12
|
+
* to ObjectStack FilterNode AST format.
|
|
13
|
+
*/
|
|
14
|
+
import { normalizeFilterOperator } from '@objectstack/spec/ui';
|
|
8
15
|
/**
|
|
9
16
|
* Map MongoDB-like operators to ObjectStack filter operators.
|
|
10
17
|
*
|
|
@@ -152,6 +159,37 @@ export function convertFiltersToAST(filter) {
|
|
|
152
159
|
// Multiple conditions: combine with 'and'
|
|
153
160
|
return ['and', ...conditions];
|
|
154
161
|
}
|
|
162
|
+
function isViewFilterRule(value) {
|
|
163
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
164
|
+
return false;
|
|
165
|
+
const field = value.field;
|
|
166
|
+
return typeof field === 'string' && field !== '';
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Lower ONE spec `ViewFilterRule` to an ObjectQL AST comparison node.
|
|
170
|
+
*
|
|
171
|
+
* The operator goes through the spec's OWN {@link normalizeFilterOperator} —
|
|
172
|
+
* the exact exit the WRITE side uses (`app-shell/views/viewFilterFold.ts`), so
|
|
173
|
+
* the two directions cannot drift into two dialects. No second canonical map is
|
|
174
|
+
* introduced, and none is needed: all 19 `VIEW_FILTER_OPERATORS` are already
|
|
175
|
+
* members of the wire's `VALID_AST_OPERATORS`, so the lowering is purely
|
|
176
|
+
* structural. An operator the spec does not know is passed through VERBATIM, so
|
|
177
|
+
* `isFilterAST` still refuses it and the server still answers `400
|
|
178
|
+
* INVALID_FILTER` — a misspelling must not be coerced into a valid one.
|
|
179
|
+
*
|
|
180
|
+
* `value` is emitted only when the rule carries one. A rule that omits it
|
|
181
|
+
* (`is_empty` / `is_null`, whose direction comes from the operator NAME) would
|
|
182
|
+
* otherwise gain an invented `null` the author never wrote — `JSON.stringify`
|
|
183
|
+
* turns a hole in an array into `null`, and `['x', 'equals', null]` is a real
|
|
184
|
+
* `{x: null}` predicate, i.e. a silently-wrong filter. Same rule the write side
|
|
185
|
+
* applies (`if (c.value !== undefined)`).
|
|
186
|
+
*/
|
|
187
|
+
function viewFilterRuleToNode(rule) {
|
|
188
|
+
const operator = normalizeFilterOperator(rule.operator);
|
|
189
|
+
return (rule.value === undefined
|
|
190
|
+
? [rule.field, operator]
|
|
191
|
+
: [rule.field, operator, rule.value]);
|
|
192
|
+
}
|
|
155
193
|
/**
|
|
156
194
|
* Normalize ONE filter source into a single filter node.
|
|
157
195
|
*
|
|
@@ -167,14 +205,52 @@ export function convertFiltersToAST(filter) {
|
|
|
167
205
|
* `table.defaultFilters` (declared `Record<string, any>`) was DROPPED and the
|
|
168
206
|
* view returned every record. Silently: no error, just a wider answer.
|
|
169
207
|
*
|
|
208
|
+
* The FIRST never worked at all (objectui#3431). The array branch returned
|
|
209
|
+
* every array VERBATIM, so a saved view's `ViewFilterRule[]` travelled to
|
|
210
|
+
* `$filter` as bare rule objects — which the server refuses: `isFilterAST` is
|
|
211
|
+
* false for an array of objects, and the wire face answers `400
|
|
212
|
+
* INVALID_FILTER`. Verified against a real backend on the showcase's SHIPPED
|
|
213
|
+
* `showcase_task.in_progress` view (`filter: [{field:'status',
|
|
214
|
+
* operator:'equals', value:'in_progress'}]`): 400 as sent, 200 with its 2 rows
|
|
215
|
+
* once lowered to `[['status','equals','in_progress']]`. Every saved view
|
|
216
|
+
* carrying a filter was a failed list. `mergeFilterNodes` below has warned
|
|
217
|
+
* about exactly this hazard since it was written — the warning was accurate,
|
|
218
|
+
* and the sink it warns for never implemented the lowering it describes.
|
|
219
|
+
*
|
|
220
|
+
* **Why the fold lives HERE and not at the producer.** `ListViewSchema.filter`
|
|
221
|
+
* / `ViewTab.filter` are spec-declared `z.array(ViewFilterRuleSchema)`, and a
|
|
222
|
+
* view hands a renderer a `ListViewSchema` — so folding one hop earlier (in
|
|
223
|
+
* `app-shell`'s ObjectView, say) would write ObjectQL AST triples INTO a
|
|
224
|
+
* spec-declared rule-array slot: an off-spec value in a spec field, AGENTS.md
|
|
225
|
+
* #0.1 inverted. This function is the last hop before the wire, where the value
|
|
226
|
+
* legitimately leaves the spec's view vocabulary and becomes an ObjectQL AST.
|
|
227
|
+
* It is also the single sink both producers already share — `plugin-list`'s
|
|
228
|
+
* `buildEffectiveFilter` (which feeds the grid AND the export) and
|
|
229
|
+
* `plugin-view`'s ObjectView (calendar / kanban / gallery / timeline). One
|
|
230
|
+
* lowering, one place; the same reason the MongoDB-style shape is lowered here
|
|
231
|
+
* rather than at each of its callers.
|
|
232
|
+
*
|
|
233
|
+
* Mixed arrays fold ELEMENT-WISE, because that is what reaches this function in
|
|
234
|
+
* practice: `ObjectView` concatenates a saved view's rules with the
|
|
235
|
+
* `?filter[<field>]=<value>` URL triples into one array. Triples pass through
|
|
236
|
+
* untouched.
|
|
237
|
+
*
|
|
170
238
|
* Returns `undefined` for an absent or empty source, so callers can skip
|
|
171
239
|
* `$filter` rather than sending an empty array.
|
|
172
240
|
*/
|
|
173
241
|
export function toFilterNode(source) {
|
|
174
242
|
if (source === null || source === undefined)
|
|
175
243
|
return undefined;
|
|
176
|
-
if (Array.isArray(source))
|
|
177
|
-
|
|
244
|
+
if (Array.isArray(source)) {
|
|
245
|
+
if (source.length === 0)
|
|
246
|
+
return undefined;
|
|
247
|
+
// Spec `ViewFilterRule[]` (possibly mixed with AST nodes) → AST nodes. Left
|
|
248
|
+
// untouched when the array holds no rule objects, which is the common case:
|
|
249
|
+
// user-filter conditions and URL triples are already nodes.
|
|
250
|
+
return (source.some(isViewFilterRule)
|
|
251
|
+
? source.map((el) => (isViewFilterRule(el) ? viewFilterRuleToNode(el) : el))
|
|
252
|
+
: source);
|
|
253
|
+
}
|
|
178
254
|
if (typeof source !== 'object')
|
|
179
255
|
return undefined;
|
|
180
256
|
const obj = source;
|
|
@@ -188,11 +264,15 @@ export function toFilterNode(source) {
|
|
|
188
264
|
*
|
|
189
265
|
* Wrapping rather than spreading, on purpose. `['and', ...rules]` looks
|
|
190
266
|
* equivalent and is not: spreading a `ViewFilterRule[]` puts bare rule OBJECTS
|
|
191
|
-
* where the AST expects nodes, and the server
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
267
|
+
* where the AST expects nodes, and the server does not accept that —
|
|
268
|
+
* `isFilterAST` says no and the wire face answers `400 INVALID_FILTER`
|
|
269
|
+
* (objectstack#4121). Spreading is only correct when the source happens to be
|
|
270
|
+
* an array of nodes, which is why it survived.
|
|
271
|
+
*
|
|
272
|
+
* Since objectui#3431 the rule objects never reach that position anyway:
|
|
273
|
+
* `toFilterNode` lowers a `ViewFilterRule[]` to AST nodes on the way in. The
|
|
274
|
+
* paragraph above is kept because it is why each source stays its own child —
|
|
275
|
+
* and because it correctly diagnosed the bug the sink itself was carrying.
|
|
196
276
|
*
|
|
197
277
|
* Sources that normalize to nothing are skipped; one surviving source is
|
|
198
278
|
* returned as-is rather than wrapped in a pointless `and`.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — predicate field harvesting
|
|
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
|
+
* Which record fields a view's PREDICATES read — the half of a view's field
|
|
10
|
+
* appetite that is not its columns.
|
|
11
|
+
*
|
|
12
|
+
* A list view projects what it DISPLAYS: `$select` is built from the column
|
|
13
|
+
* list (plus `id` and the expanded relation roots). That is correct for
|
|
14
|
+
* rendering and wrong for gating, because a `visible` / `disabled` /
|
|
15
|
+
* conditional-formatting predicate reads fields nobody put on screen — gating a
|
|
16
|
+
* row action on `record.owner` while showing Title / Status is the ordinary
|
|
17
|
+
* case, not an exotic one.
|
|
18
|
+
*
|
|
19
|
+
* The field then simply is not in the payload, and CEL does not treat an absent
|
|
20
|
+
* key as null: `record.owner == os.user.id` FAULTS with `No such key: owner`.
|
|
21
|
+
* Fault handling differs per surface and both answers are wrong — fail-CLOSED
|
|
22
|
+
* on the row kebab and the selection bar (the button silently disappears for
|
|
23
|
+
* everyone), fail-OPEN on the lenient paths (it appears for everyone). Nothing
|
|
24
|
+
* in either outcome points at a projection.
|
|
25
|
+
*
|
|
26
|
+
* Harvesting the names a predicate mentions and adding them to `$select` is
|
|
27
|
+
* what closes that. Note what it deliberately does NOT add: `$expand`. A
|
|
28
|
+
* predicate wants the stored foreign key, which is exactly what an unexpanded
|
|
29
|
+
* relation column already is — so a predicate-only relation field costs one
|
|
30
|
+
* more column and no extra round trip. (A relation that IS displayed still gets
|
|
31
|
+
* expanded for its label, and `toPredicateRecord` collapses it back for the
|
|
32
|
+
* predicate.)
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* The columns the framework provisions on EVERY business object
|
|
36
|
+
* (`applySystemFields`) but does not publish in the object's `fields` metadata.
|
|
37
|
+
*
|
|
38
|
+
* They matter here because the most ordinary ownership predicate there is —
|
|
39
|
+
* `record.owner_id == os.user.id` — names one of them. Checking a harvested
|
|
40
|
+
* name against the declared fields alone would drop it, leaving the predicate
|
|
41
|
+
* with the same missing key the harvest exists to prevent.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately the columns OBSERVED on a live payload rather than
|
|
44
|
+
* `SYSTEM_MANAGED_FIELD_NAMES` (`@object-ui/types`), which is the broader
|
|
45
|
+
* DISPLAY-oriented set and carries legacy aliases (`_id`, `createdAt`,
|
|
46
|
+
* `modified`, `space`) that are not columns anywhere. An alias in `$select` is
|
|
47
|
+
* exactly the unknown key the guard exists to keep out.
|
|
48
|
+
*/
|
|
49
|
+
export declare const PLATFORM_RECORD_COLUMNS: ReadonlySet<string>;
|
|
50
|
+
/**
|
|
51
|
+
* Whether a harvested name is safe to put in a `$select` projection: either the
|
|
52
|
+
* object declares it, or it is one of the platform columns every object has.
|
|
53
|
+
*
|
|
54
|
+
* The check is not pedantry — an unknown key is not ignored by every backend
|
|
55
|
+
* (the cloud multi-tenant runtime answers with an EMPTY result set), so without
|
|
56
|
+
* it one typo'd predicate could blank a whole list.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isProjectableField(name: string, declaredFields: Record<string, unknown> | null | undefined): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Collect the record field names referenced by a set of predicates.
|
|
61
|
+
*
|
|
62
|
+
* Accepts predicates in every authored shape (bare CEL string, the
|
|
63
|
+
* `{ dialect, source }` envelope, `null`/`undefined`/boolean — the last three
|
|
64
|
+
* contribute nothing). Returns names in first-seen order, deduplicated.
|
|
65
|
+
*
|
|
66
|
+
* The result is a set of CANDIDATES, not verified fields: a typo'd predicate
|
|
67
|
+
* yields a name this object does not have. Callers must intersect it with the
|
|
68
|
+
* object's declared fields before putting it in a query — an unknown key in
|
|
69
|
+
* `$select` is not ignored by every backend (the cloud runtime answers with an
|
|
70
|
+
* empty result set), which would turn a typo into a blank list.
|
|
71
|
+
*/
|
|
72
|
+
export declare function collectPredicateFieldRefs(predicates: readonly unknown[]): string[];
|
|
73
|
+
/**
|
|
74
|
+
* Every predicate a list-ish view carries, flattened for
|
|
75
|
+
* {@link collectPredicateFieldRefs}.
|
|
76
|
+
*
|
|
77
|
+
* One reader so the projection cannot fall behind the surfaces: each entry here
|
|
78
|
+
* is a place a predicate is actually evaluated against a row — conditional
|
|
79
|
+
* formatting, the row kebab (custom defs AND the built-in Edit/Delete
|
|
80
|
+
* overrides), the selection bar, and the object's declared actions.
|
|
81
|
+
*
|
|
82
|
+
* `objectActions` is deliberately the object's WHOLE action set rather than
|
|
83
|
+
* only the ones this view promotes. Narrowing it would mean re-running the
|
|
84
|
+
* name resolution `resolveLegacyRowActions` / `resolveBulkActions` already do,
|
|
85
|
+
* at a point in the fetch where the view's `rowActions` / `bulkActions` lists
|
|
86
|
+
* have not been folded yet — and the two directions of error are not
|
|
87
|
+
* symmetric: an extra column costs bytes, a missing one silently breaks the
|
|
88
|
+
* predicate that needed it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function listViewPredicates(view: {
|
|
91
|
+
conditionalFormatting?: readonly unknown[] | null;
|
|
92
|
+
rowActionDefs?: readonly unknown[] | null;
|
|
93
|
+
bulkActionDefs?: readonly unknown[] | null;
|
|
94
|
+
objectActions?: readonly unknown[] | null;
|
|
95
|
+
userActions?: Record<string, unknown> | null;
|
|
96
|
+
}): unknown[];
|