@object-ui/core 17.6.0 → 18.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7260 -0
- package/README.md +228 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +473 -69
- package/dist/actions/ActionRunner.js +792 -125
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/UndoManager.d.ts +8 -13
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +67 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/navigationTarget.d.ts +41 -0
- package/dist/actions/navigationTarget.js +20 -0
- package/dist/actions/recordDelete.d.ts +154 -0
- package/dist/actions/recordDelete.js +110 -0
- package/dist/adapters/ApiDataSource.d.ts +8 -4
- package/dist/adapters/ApiDataSource.js +19 -7
- package/dist/adapters/ValueDataSource.d.ts +6 -1
- package/dist/adapters/ValueDataSource.js +1090 -98
- package/dist/adapters/batchTransaction.d.ts +1 -1
- package/dist/adapters/batchTransaction.js +1 -1
- package/dist/adapters/resolveDataSource.d.ts +9 -6
- package/dist/adapters/resolveDataSource.js +9 -12
- package/dist/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +8 -25
- package/dist/data-scope/ViewDataProvider.js +27 -54
- package/dist/data-scope/element-data-source.d.ts +261 -7
- package/dist/data-scope/element-data-source.js +342 -52
- package/dist/data-scope/index.d.ts +9 -4
- package/dist/data-scope/index.js +9 -4
- package/dist/evaluator/ExpressionEvaluator.d.ts +86 -0
- package/dist/evaluator/ExpressionEvaluator.js +184 -12
- package/dist/evaluator/FormulaFunctions.d.ts +0 -10
- package/dist/evaluator/FormulaFunctions.js +166 -27
- package/dist/evaluator/deadlineSettled.d.ts +40 -0
- package/dist/evaluator/deadlineSettled.js +45 -0
- package/dist/evaluator/declaredPredicate.d.ts +93 -9
- package/dist/evaluator/declaredPredicate.js +117 -16
- package/dist/evaluator/fieldRules.d.ts +88 -4
- package/dist/evaluator/fieldRules.js +208 -31
- package/dist/evaluator/index.d.ts +2 -0
- package/dist/evaluator/index.js +2 -0
- package/dist/evaluator/listConditional.d.ts +103 -34
- package/dist/evaluator/listConditional.js +127 -50
- package/dist/evaluator/optionRules.d.ts +77 -6
- package/dist/evaluator/optionRules.js +87 -10
- package/dist/evaluator/predicateInput.d.ts +15 -4
- package/dist/evaluator/predicateInput.js +35 -2
- package/dist/evaluator/rowPredicateCanon.d.ts +7 -18
- package/dist/evaluator/rowPredicateCanon.js +65 -115
- package/dist/evaluator/subjectPermissions.d.ts +105 -0
- package/dist/evaluator/subjectPermissions.js +86 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +83 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +382 -34
- package/dist/registry/Registry.js +247 -12
- package/dist/registry/WidgetRegistry.js +7 -3
- package/dist/registry/html-tier-intrinsics.d.ts +90 -0
- package/dist/registry/html-tier-intrinsics.js +110 -0
- package/dist/registry/public-blocks.d.ts +8 -0
- package/dist/registry/public-blocks.js +57 -0
- package/dist/theme/ThemeEngine.js +1 -1
- package/dist/utils/affordanceGrants.d.ts +371 -0
- package/dist/utils/affordanceGrants.js +252 -0
- package/dist/utils/chart-category-key.d.ts +82 -0
- package/dist/utils/chart-category-key.js +84 -0
- package/dist/utils/chart-measure-key.d.ts +64 -0
- package/dist/utils/chart-measure-key.js +65 -0
- package/dist/utils/chart-presentation.d.ts +95 -112
- package/dist/utils/chart-presentation.js +169 -104
- package/dist/utils/chart-series.d.ts +165 -5
- package/dist/utils/chart-series.js +123 -18
- package/dist/utils/column-sortability.d.ts +157 -0
- package/dist/utils/column-sortability.js +217 -0
- package/dist/utils/dashboard-filters.d.ts +38 -6
- package/dist/utils/dashboard-filters.js +107 -61
- package/dist/utils/dataset-format.d.ts +211 -30
- package/dist/utils/dataset-format.js +389 -46
- package/dist/utils/date-display.d.ts +346 -0
- package/dist/utils/date-display.js +644 -0
- package/dist/utils/date-macros.js +7 -0
- package/dist/utils/drill-down.d.ts +78 -0
- package/dist/utils/drill-down.js +93 -0
- package/dist/utils/emptiness.d.ts +96 -0
- package/dist/utils/emptiness.js +101 -0
- package/dist/utils/expand-fields.d.ts +81 -9
- package/dist/utils/expand-fields.js +81 -9
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +158 -6
- package/dist/utils/filter-converter.js +1098 -24
- package/dist/utils/filter-tokens.d.ts +49 -5
- package/dist/utils/filter-tokens.js +105 -33
- package/dist/utils/grouping-fields.d.ts +61 -0
- package/dist/utils/grouping-fields.js +84 -0
- package/dist/utils/managedBy.d.ts +3 -3
- package/dist/utils/managedBy.js +5 -5
- package/dist/utils/native-date-value.d.ts +48 -0
- package/dist/utils/native-date-value.js +167 -0
- package/dist/utils/non-grid-row-ceiling.d.ts +143 -0
- package/dist/utils/non-grid-row-ceiling.js +141 -0
- package/dist/utils/normalize-list-view.d.ts +135 -11
- package/dist/utils/normalize-list-view.js +243 -64
- package/dist/utils/number-display.d.ts +63 -14
- package/dist/utils/number-display.js +34 -12
- package/dist/utils/object-aggregate-query.d.ts +113 -0
- package/dist/utils/object-aggregate-query.js +50 -0
- package/dist/utils/parent-scope.d.ts +123 -0
- package/dist/utils/parent-scope.js +140 -0
- package/dist/utils/predicate-fields.d.ts +45 -12
- package/dist/utils/predicate-fields.js +141 -12
- package/dist/utils/record-color.d.ts +75 -0
- package/dist/utils/record-color.js +65 -0
- package/dist/utils/record-source.d.ts +298 -0
- package/dist/utils/record-source.js +331 -0
- package/dist/utils/record-title.d.ts +148 -8
- package/dist/utils/record-title.js +220 -26
- package/dist/utils/reference-keys.d.ts +21 -22
- package/dist/utils/reference-keys.js +532 -29
- package/dist/utils/sort-query.d.ts +115 -15
- package/dist/utils/sort-query.js +240 -13
- package/dist/utils/text-comparand.d.ts +92 -0
- package/dist/utils/text-comparand.js +99 -0
- package/dist/utils/without-denied-fields.d.ts +49 -0
- package/dist/utils/without-denied-fields.js +52 -0
- package/dist/validation/index.d.ts +8 -4
- package/dist/validation/index.js +8 -4
- package/dist/validation/schema-validator.d.ts +1 -1
- package/dist/validation/schema-validator.js +225 -83
- package/dist/validation/server-owned-value.d.ts +5 -1
- package/dist/validation/server-owned-value.js +5 -1
- package/dist/validation/validators/object-validation-engine.d.ts +1 -1
- package/dist/validation/validators/object-validation-engine.js +14 -3
- package/package.json +8 -9
- package/dist/data-scope/DataScopeManager.d.ts +0 -127
- package/dist/data-scope/DataScopeManager.js +0 -229
- package/dist/protocols/ResponsiveProtocol.d.ts +0 -73
- package/dist/protocols/ResponsiveProtocol.js +0 -158
- package/dist/validation/validation-engine.d.ts +0 -88
- package/dist/validation/validation-engine.js +0 -434
|
@@ -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
|
+
/**
|
|
9
|
+
* object-aggregate-query — the ONE answer to "which CALL does an object-bound
|
|
10
|
+
* `aggregate` make when its authored `groupBy` is the structured node?"
|
|
11
|
+
* (objectui#8613).
|
|
12
|
+
*
|
|
13
|
+
* ## The two wires, and why the choice is not cosmetic
|
|
14
|
+
*
|
|
15
|
+
* An authored `aggregate.groupBy` is a union: a bare field name, or the
|
|
16
|
+
* structured date-bucketing node `{ field, dateGranularity?, alias? }` — the
|
|
17
|
+
* union `ChartGroupBySchema` declares and `GroupByNodeSchema` (the query AST)
|
|
18
|
+
* mirrors. The adapter routes on the SHAPE of the params it is handed, not on
|
|
19
|
+
* the shape of the authored value:
|
|
20
|
+
*
|
|
21
|
+
* - `{ groupBy: GroupByNode[], aggregations, where }` — the spec-shape query,
|
|
22
|
+
* which reaches `engine.aggregate` and runs the server-side date-bucket
|
|
23
|
+
* engine;
|
|
24
|
+
* - `{ field, function, groupBy, filter }` — the legacy cube/analytics query,
|
|
25
|
+
* whose `dimensions` the contract declares as an array of dimension NAMES
|
|
26
|
+
* and which does NOT honour `dateGranularity` at all.
|
|
27
|
+
*
|
|
28
|
+
* So a structured node forwarded on the legacy wire is posted as a dimension
|
|
29
|
+
* OBJECT where a name is declared: the author asks for monthly buckets and the
|
|
30
|
+
* platform answers a different question, with no diagnostic on the way. That is
|
|
31
|
+
* the defect objectui#8613 records on the metric path, and it was reachable
|
|
32
|
+
* there only because this payload was spelled INSIDE one renderer
|
|
33
|
+
* (`ObjectChart.runAggregate`) rather than anywhere a second renderer could
|
|
34
|
+
* reuse it.
|
|
35
|
+
*
|
|
36
|
+
* ## Why a shared module rather than a second copy
|
|
37
|
+
*
|
|
38
|
+
* The alternative was to transcribe the eight lines into the metric widget.
|
|
39
|
+
* That is the objectui#5042 / #7544 / #8193 / #8266 / #8269 drift shape — a
|
|
40
|
+
* second opinion of one question, with nothing keeping the two in agreement —
|
|
41
|
+
* and it is the same shape those cards' fixes (`chartMeasureKey`,
|
|
42
|
+
* `chartCategoryKey`) exist to end, one column at a time. This module is that
|
|
43
|
+
* move applied to the REQUEST rather than to a result column.
|
|
44
|
+
*
|
|
45
|
+
* ## What is NOT here
|
|
46
|
+
*
|
|
47
|
+
* The legacy bag itself. The two renderers do not agree on it and should not be
|
|
48
|
+
* made to: a chart passes the authored `groupBy` through untouched, while a
|
|
49
|
+
* metric floors an absent one at `'_all'` (one bucket) because it paints a
|
|
50
|
+
* single number. Folding that difference into a shared builder would put a
|
|
51
|
+
* renderer's own default into a module that has no business holding one, so the
|
|
52
|
+
* shared part stops exactly where the agreement stops.
|
|
53
|
+
*/
|
|
54
|
+
import type { ChartGroupBy } from '@objectstack/spec/ui';
|
|
55
|
+
import { type ChartAggregateLike } from './chart-measure-key.js';
|
|
56
|
+
/**
|
|
57
|
+
* The STRUCTURED arm of the contract's `groupBy` union — the date-bucketing
|
|
58
|
+
* node, as distinct from the bare field name the same union admits.
|
|
59
|
+
*
|
|
60
|
+
* Extracted from `ChartGroupBy` rather than restated, so a member added to the
|
|
61
|
+
* node upstream (as `alias` once was) arrives here without an edit.
|
|
62
|
+
*/
|
|
63
|
+
export type StructuredGroupBy = Extract<ChartGroupBy, {
|
|
64
|
+
field: string;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* The spec-shape aggregate query's `aggregations[]` entry, as
|
|
68
|
+
* `AggregationNodeSchema` declares it: `alias` is REQUIRED (it names the column
|
|
69
|
+
* the measure is projected under), `field` is optional because a `count` counts
|
|
70
|
+
* rows rather than a column.
|
|
71
|
+
*/
|
|
72
|
+
export interface ObjectAggregationNode {
|
|
73
|
+
function?: string;
|
|
74
|
+
alias: string;
|
|
75
|
+
field?: string;
|
|
76
|
+
}
|
|
77
|
+
/** The spec-shape query body — `{ groupBy: GroupByNode[], aggregations, where }`. */
|
|
78
|
+
export interface ObjectAggregateSpecQuery {
|
|
79
|
+
groupBy: StructuredGroupBy[];
|
|
80
|
+
aggregations: ObjectAggregationNode[];
|
|
81
|
+
where: unknown;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Is this authored `aggregate.groupBy` the structured node rather than a bare
|
|
85
|
+
* field name?
|
|
86
|
+
*
|
|
87
|
+
* ⚠️ An ARRAY answers `false`. It is an object, but it is not this union's
|
|
88
|
+
* object arm — neither `ChartGroupBySchema` nor `ObjectMetricPropsSchema`'s
|
|
89
|
+
* `aggregate` admits one at this position, and `data-objectstack`'s adapter
|
|
90
|
+
* already refuses it at the wire (`503cd8b89`). Letting it through here would
|
|
91
|
+
* wrap it into `groupBy: [[…]]` and turn a producer-side refusal into a
|
|
92
|
+
* malformed query.
|
|
93
|
+
*/
|
|
94
|
+
export declare function isStructuredGroupBy(groupBy: unknown): groupBy is StructuredGroupBy;
|
|
95
|
+
/**
|
|
96
|
+
* The spec-shape aggregate query for an object-bound `aggregate` whose
|
|
97
|
+
* `groupBy` is the structured node.
|
|
98
|
+
*
|
|
99
|
+
* The measure is projected under {@link chartMeasureKey}'s answer — the raw
|
|
100
|
+
* field name, or the literal `'count'` for a fieldless count — so a caller's
|
|
101
|
+
* existing result readback finds the value under the key it already looks for,
|
|
102
|
+
* and a chart's series binding names the column the rows actually carry.
|
|
103
|
+
*
|
|
104
|
+
* `field` is omitted for `count` so the engine emits `count(*)`: the dashboard
|
|
105
|
+
* relays default `field: 'value'` for a widget with no explicit measure column,
|
|
106
|
+
* and `count(value)` crashes SQL drivers with "no such column: value" on the
|
|
107
|
+
* count-the-rows case that is the common one.
|
|
108
|
+
*
|
|
109
|
+
* @param aggregate the authored inline aggregate
|
|
110
|
+
* @param groupBy its `groupBy`, already narrowed by {@link isStructuredGroupBy}
|
|
111
|
+
* @param where the resolved filter, in the spec Query DSL's `where` position
|
|
112
|
+
*/
|
|
113
|
+
export declare function objectAggregateSpecQuery(aggregate: ChartAggregateLike, groupBy: StructuredGroupBy, where: unknown): ObjectAggregateSpecQuery;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
import { chartMeasureKey } from './chart-measure-key.js';
|
|
9
|
+
/**
|
|
10
|
+
* Is this authored `aggregate.groupBy` the structured node rather than a bare
|
|
11
|
+
* field name?
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ An ARRAY answers `false`. It is an object, but it is not this union's
|
|
14
|
+
* object arm — neither `ChartGroupBySchema` nor `ObjectMetricPropsSchema`'s
|
|
15
|
+
* `aggregate` admits one at this position, and `data-objectstack`'s adapter
|
|
16
|
+
* already refuses it at the wire (`503cd8b89`). Letting it through here would
|
|
17
|
+
* wrap it into `groupBy: [[…]]` and turn a producer-side refusal into a
|
|
18
|
+
* malformed query.
|
|
19
|
+
*/
|
|
20
|
+
export function isStructuredGroupBy(groupBy) {
|
|
21
|
+
return !!groupBy && typeof groupBy === 'object' && !Array.isArray(groupBy);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The spec-shape aggregate query for an object-bound `aggregate` whose
|
|
25
|
+
* `groupBy` is the structured node.
|
|
26
|
+
*
|
|
27
|
+
* The measure is projected under {@link chartMeasureKey}'s answer — the raw
|
|
28
|
+
* field name, or the literal `'count'` for a fieldless count — so a caller's
|
|
29
|
+
* existing result readback finds the value under the key it already looks for,
|
|
30
|
+
* and a chart's series binding names the column the rows actually carry.
|
|
31
|
+
*
|
|
32
|
+
* `field` is omitted for `count` so the engine emits `count(*)`: the dashboard
|
|
33
|
+
* relays default `field: 'value'` for a widget with no explicit measure column,
|
|
34
|
+
* and `count(value)` crashes SQL drivers with "no such column: value" on the
|
|
35
|
+
* count-the-rows case that is the common one.
|
|
36
|
+
*
|
|
37
|
+
* @param aggregate the authored inline aggregate
|
|
38
|
+
* @param groupBy its `groupBy`, already narrowed by {@link isStructuredGroupBy}
|
|
39
|
+
* @param where the resolved filter, in the spec Query DSL's `where` position
|
|
40
|
+
*/
|
|
41
|
+
export function objectAggregateSpecQuery(aggregate, groupBy, where) {
|
|
42
|
+
const aggFn = aggregate.function;
|
|
43
|
+
const node = {
|
|
44
|
+
function: aggFn,
|
|
45
|
+
alias: chartMeasureKey(aggregate, aggFn || 'count'),
|
|
46
|
+
};
|
|
47
|
+
if (aggFn !== 'count' && aggregate.field)
|
|
48
|
+
node.field = aggregate.field;
|
|
49
|
+
return { groupBy: [groupBy], aggregations: [node], where };
|
|
50
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
* The parent-relationship condition a detail-page related list is scoped by —
|
|
9
|
+
* compiled ONCE, here, for every surface that asks the question.
|
|
10
|
+
*
|
|
11
|
+
* ## Why this is a seam and not a helper
|
|
12
|
+
*
|
|
13
|
+
* A related list asks its backend "which children belong to this parent?" from
|
|
14
|
+
* two places: the ROW query (`RelatedList`) and the tab-badge count probe
|
|
15
|
+
* (`RelatedCountStore`). Those two used to compile the condition separately,
|
|
16
|
+
* and objectui#8882 is what that costs: objectui#7299 taught the ROW side to
|
|
17
|
+
* compile by the relationship field's ARITY, the BADGE side kept sending bare
|
|
18
|
+
* equality, and on a multi-value relationship the page then rendered rows above
|
|
19
|
+
* a tab with no count at all — two implementations of one question, drifted.
|
|
20
|
+
*
|
|
21
|
+
* Patching the second copy to match the first would leave two copies. So the
|
|
22
|
+
* condition has one implementation, and both callers import it.
|
|
23
|
+
*
|
|
24
|
+
* ## The rule itself is not invented here
|
|
25
|
+
*
|
|
26
|
+
* The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`. That
|
|
27
|
+
* matters more here than anywhere: this function chooses `$contains` vs `=` and
|
|
28
|
+
* every surface that asks the question now reads that one choice, so two
|
|
29
|
+
* readers of one question disagreeing — the entire defect class — cannot
|
|
30
|
+
* recur INSIDE this repository. The spec's rule is BROADER than an eyeballed
|
|
31
|
+
* `multiple === true` in both directions — `multiselect` / `checkboxes` /
|
|
32
|
+
* `tags` persist an array with no flag at all, and `multiple: true` is INERT on
|
|
33
|
+
* a type outside the spec's multi-capable set (`master_detail`, say) — so a
|
|
34
|
+
* local approximation is wrong in both directions, not merely incomplete.
|
|
35
|
+
*
|
|
36
|
+
* ⛔ Do not add a local arity rule at any call site, however small, and ⛔ do
|
|
37
|
+
* not widen this function to accept an arity the caller computed: the parameter
|
|
38
|
+
* it takes is METADATA, and the verdict is drawn from it here.
|
|
39
|
+
*
|
|
40
|
+
* ## ⚠️ The STORAGE side does not read this predicate — the two rules DIVERGE
|
|
41
|
+
*
|
|
42
|
+
* This header used to say `isMultiValueField` is "the same predicate the driver
|
|
43
|
+
* that executes the query decides on". It is not, and the difference is
|
|
44
|
+
* observable (objectui#8937). Measured on objectstack `origin/main`,
|
|
45
|
+
* `driver-sql` gates the equality family on its own STORAGE question,
|
|
46
|
+
* `isJsonField`: a column is JSON when the field's type is in that driver's
|
|
47
|
+
* `JSON_COLUMN_TYPES` — the spec's `STRUCTURED_JSON_TYPES` and
|
|
48
|
+
* `MULTI_OPTION_TYPES`, plus the driver-internal `object` / `array` aliases —
|
|
49
|
+
* OR when `multiple` is merely TRUTHY, on ANY type; a single-value media type
|
|
50
|
+
* answers from the ADR-0104 dual-encoding window instead. The spec's predicate
|
|
51
|
+
* is `MULTI_OPTION_TYPES.has(type) || (MULTI_CAPABLE_TYPES.has(type) &&
|
|
52
|
+
* multiple === true)`.
|
|
53
|
+
*
|
|
54
|
+
* ⇒ They diverge for a type OUTSIDE `MULTI_CAPABLE_TYPES` carrying
|
|
55
|
+
* `multiple: true` (`master_detail` / `tree` / `text`): the spec says
|
|
56
|
+
* single-valued, so this seam compiles `=`, while the driver stores a JSON
|
|
57
|
+
* column and refuses `=` with the same `400 INVALID_FILTER` objectui#7299 was
|
|
58
|
+
* filed for. That is a KNOWN divergence and ⛔ not a regression — both
|
|
59
|
+
* surfaces sent `=` for that shape before objectui#7299 and objectui#8882 too.
|
|
60
|
+
*
|
|
61
|
+
* ⛔ Do NOT close it by widening the predicate here: which of the two rules is
|
|
62
|
+
* right is an upstream question, filed as objectstack#17469 (enforce-or-align),
|
|
63
|
+
* and this seam is deliberately not blocked on it. Widening on this side would
|
|
64
|
+
* move the disagreement across the wire rather than end it, and would make the
|
|
65
|
+
* ONE compiler this module exists to be disagree with the spec it delegates to.
|
|
66
|
+
* The divergence is re-derived from the installed spec on every run by
|
|
67
|
+
* `relatedListParentScopeResidue-8937.test.ts`, so this section reddens when the
|
|
68
|
+
* spec side moves.
|
|
69
|
+
*/
|
|
70
|
+
import { type ValueShapeFieldDef } from '@objectstack/spec/data';
|
|
71
|
+
import type { FieldContainerLike } from './predicate-record.js';
|
|
72
|
+
/**
|
|
73
|
+
* Look one field def up in either container shape the metadata API serves.
|
|
74
|
+
*
|
|
75
|
+
* The pair is the one {@link FieldContainerLike} names: the Record keyed by
|
|
76
|
+
* field name, and the array of defs carrying their own `name`. A reader that
|
|
77
|
+
* knows only one of them silently answers "no such field" for the other — and
|
|
78
|
+
* "no such field" here means "single-valued", which is this card's own bug
|
|
79
|
+
* spelled as a default.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parentRelationshipFieldDef(fields: FieldContainerLike, fieldName: string | undefined): ValueShapeFieldDef | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Does this relationship field store MANY parent ids rather than one?
|
|
84
|
+
*
|
|
85
|
+
* The seam's verdict, exposed on its own because a caller sometimes needs the
|
|
86
|
+
* ANSWER without the condition — the raw-URL related-list path, whose
|
|
87
|
+
* `filter[<field>]=<value>` grammar has no membership operator, has to know
|
|
88
|
+
* the arity in order to REFUSE. Deriving it a second time at that call site is
|
|
89
|
+
* how the two compilers this module exists to merge came about, so it is
|
|
90
|
+
* derived once here and read from both shapes below.
|
|
91
|
+
*/
|
|
92
|
+
export declare function isMultiValueRelationship(fields: FieldContainerLike, fieldName: string | undefined): boolean;
|
|
93
|
+
/**
|
|
94
|
+
* The parent-relationship condition, compiled to match the field's ARITY.
|
|
95
|
+
*
|
|
96
|
+
* - single-valued → `{ [relationshipField]: parentId }` — equality, byte for
|
|
97
|
+
* byte what both surfaces have always sent;
|
|
98
|
+
* - `multiple: true` (per the spec predicate) →
|
|
99
|
+
* `{ [relationshipField]: { $contains: parentId } }` — MEMBERSHIP, because
|
|
100
|
+
* the stored value is an ARRAY of ids and equality asks whether that whole
|
|
101
|
+
* array IS one id. `$contains` is the spelling the drivers execute for it,
|
|
102
|
+
* the one `driver-sql` names in the `400 INVALID_FILTER` it answers the
|
|
103
|
+
* equality form with.
|
|
104
|
+
*
|
|
105
|
+
* The author never writes either: they named a relationship, and its storage
|
|
106
|
+
* form is the renderer's business.
|
|
107
|
+
*
|
|
108
|
+
* The return value is the plain MongoDB-style object both call sites have
|
|
109
|
+
* always put on the wire, NOT a lowered ObjectQL AST. That is deliberate: with
|
|
110
|
+
* nothing else declared the query must stay byte-identical to what it was, and
|
|
111
|
+
* a freshly lowered AST would mean the same thing, be invisible on screen, and
|
|
112
|
+
* break every caller pinning the wire. Composition with a list's own declared
|
|
113
|
+
* scope stays the caller's step, through `mergeFilterNodes`.
|
|
114
|
+
*
|
|
115
|
+
* @param relationshipField The child field pointing back at the parent.
|
|
116
|
+
* @param parentId The parent record's primary key value.
|
|
117
|
+
* @param fields The CHILD object's field defs (`objectSchema.fields`),
|
|
118
|
+
* in either served shape. Without them the condition
|
|
119
|
+
* compiles to equality — the historical wire, and the
|
|
120
|
+
* only answer available to a caller that cannot see
|
|
121
|
+
* the metadata. A caller that CAN see it must pass it.
|
|
122
|
+
*/
|
|
123
|
+
export declare function composeParentScopeFilter(relationshipField: string, parentId: string | number, fields?: FieldContainerLike): Record<string, unknown>;
|
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
* The parent-relationship condition a detail-page related list is scoped by —
|
|
9
|
+
* compiled ONCE, here, for every surface that asks the question.
|
|
10
|
+
*
|
|
11
|
+
* ## Why this is a seam and not a helper
|
|
12
|
+
*
|
|
13
|
+
* A related list asks its backend "which children belong to this parent?" from
|
|
14
|
+
* two places: the ROW query (`RelatedList`) and the tab-badge count probe
|
|
15
|
+
* (`RelatedCountStore`). Those two used to compile the condition separately,
|
|
16
|
+
* and objectui#8882 is what that costs: objectui#7299 taught the ROW side to
|
|
17
|
+
* compile by the relationship field's ARITY, the BADGE side kept sending bare
|
|
18
|
+
* equality, and on a multi-value relationship the page then rendered rows above
|
|
19
|
+
* a tab with no count at all — two implementations of one question, drifted.
|
|
20
|
+
*
|
|
21
|
+
* Patching the second copy to match the first would leave two copies. So the
|
|
22
|
+
* condition has one implementation, and both callers import it.
|
|
23
|
+
*
|
|
24
|
+
* ## The rule itself is not invented here
|
|
25
|
+
*
|
|
26
|
+
* The arity verdict is `@objectstack/spec/data`'s own `isMultiValueField`. That
|
|
27
|
+
* matters more here than anywhere: this function chooses `$contains` vs `=` and
|
|
28
|
+
* every surface that asks the question now reads that one choice, so two
|
|
29
|
+
* readers of one question disagreeing — the entire defect class — cannot
|
|
30
|
+
* recur INSIDE this repository. The spec's rule is BROADER than an eyeballed
|
|
31
|
+
* `multiple === true` in both directions — `multiselect` / `checkboxes` /
|
|
32
|
+
* `tags` persist an array with no flag at all, and `multiple: true` is INERT on
|
|
33
|
+
* a type outside the spec's multi-capable set (`master_detail`, say) — so a
|
|
34
|
+
* local approximation is wrong in both directions, not merely incomplete.
|
|
35
|
+
*
|
|
36
|
+
* ⛔ Do not add a local arity rule at any call site, however small, and ⛔ do
|
|
37
|
+
* not widen this function to accept an arity the caller computed: the parameter
|
|
38
|
+
* it takes is METADATA, and the verdict is drawn from it here.
|
|
39
|
+
*
|
|
40
|
+
* ## ⚠️ The STORAGE side does not read this predicate — the two rules DIVERGE
|
|
41
|
+
*
|
|
42
|
+
* This header used to say `isMultiValueField` is "the same predicate the driver
|
|
43
|
+
* that executes the query decides on". It is not, and the difference is
|
|
44
|
+
* observable (objectui#8937). Measured on objectstack `origin/main`,
|
|
45
|
+
* `driver-sql` gates the equality family on its own STORAGE question,
|
|
46
|
+
* `isJsonField`: a column is JSON when the field's type is in that driver's
|
|
47
|
+
* `JSON_COLUMN_TYPES` — the spec's `STRUCTURED_JSON_TYPES` and
|
|
48
|
+
* `MULTI_OPTION_TYPES`, plus the driver-internal `object` / `array` aliases —
|
|
49
|
+
* OR when `multiple` is merely TRUTHY, on ANY type; a single-value media type
|
|
50
|
+
* answers from the ADR-0104 dual-encoding window instead. The spec's predicate
|
|
51
|
+
* is `MULTI_OPTION_TYPES.has(type) || (MULTI_CAPABLE_TYPES.has(type) &&
|
|
52
|
+
* multiple === true)`.
|
|
53
|
+
*
|
|
54
|
+
* ⇒ They diverge for a type OUTSIDE `MULTI_CAPABLE_TYPES` carrying
|
|
55
|
+
* `multiple: true` (`master_detail` / `tree` / `text`): the spec says
|
|
56
|
+
* single-valued, so this seam compiles `=`, while the driver stores a JSON
|
|
57
|
+
* column and refuses `=` with the same `400 INVALID_FILTER` objectui#7299 was
|
|
58
|
+
* filed for. That is a KNOWN divergence and ⛔ not a regression — both
|
|
59
|
+
* surfaces sent `=` for that shape before objectui#7299 and objectui#8882 too.
|
|
60
|
+
*
|
|
61
|
+
* ⛔ Do NOT close it by widening the predicate here: which of the two rules is
|
|
62
|
+
* right is an upstream question, filed as objectstack#17469 (enforce-or-align),
|
|
63
|
+
* and this seam is deliberately not blocked on it. Widening on this side would
|
|
64
|
+
* move the disagreement across the wire rather than end it, and would make the
|
|
65
|
+
* ONE compiler this module exists to be disagree with the spec it delegates to.
|
|
66
|
+
* The divergence is re-derived from the installed spec on every run by
|
|
67
|
+
* `relatedListParentScopeResidue-8937.test.ts`, so this section reddens when the
|
|
68
|
+
* spec side moves.
|
|
69
|
+
*/
|
|
70
|
+
import { isMultiValueField } from '@objectstack/spec/data';
|
|
71
|
+
/**
|
|
72
|
+
* Look one field def up in either container shape the metadata API serves.
|
|
73
|
+
*
|
|
74
|
+
* The pair is the one {@link FieldContainerLike} names: the Record keyed by
|
|
75
|
+
* field name, and the array of defs carrying their own `name`. A reader that
|
|
76
|
+
* knows only one of them silently answers "no such field" for the other — and
|
|
77
|
+
* "no such field" here means "single-valued", which is this card's own bug
|
|
78
|
+
* spelled as a default.
|
|
79
|
+
*/
|
|
80
|
+
export function parentRelationshipFieldDef(fields, fieldName) {
|
|
81
|
+
if (!fieldName || !fields || typeof fields !== 'object')
|
|
82
|
+
return undefined;
|
|
83
|
+
const def = Array.isArray(fields)
|
|
84
|
+
? fields.find((f) => f?.name === fieldName)
|
|
85
|
+
: fields[fieldName];
|
|
86
|
+
if (!def || typeof def !== 'object')
|
|
87
|
+
return undefined;
|
|
88
|
+
// `type` is the one member the spec's predicate reads besides `multiple`; a
|
|
89
|
+
// def without it answers `false` through both of the predicate's set lookups,
|
|
90
|
+
// which is the right answer for a field whose type nobody declared.
|
|
91
|
+
return def;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Does this relationship field store MANY parent ids rather than one?
|
|
95
|
+
*
|
|
96
|
+
* The seam's verdict, exposed on its own because a caller sometimes needs the
|
|
97
|
+
* ANSWER without the condition — the raw-URL related-list path, whose
|
|
98
|
+
* `filter[<field>]=<value>` grammar has no membership operator, has to know
|
|
99
|
+
* the arity in order to REFUSE. Deriving it a second time at that call site is
|
|
100
|
+
* how the two compilers this module exists to merge came about, so it is
|
|
101
|
+
* derived once here and read from both shapes below.
|
|
102
|
+
*/
|
|
103
|
+
export function isMultiValueRelationship(fields, fieldName) {
|
|
104
|
+
const def = parentRelationshipFieldDef(fields, fieldName);
|
|
105
|
+
return def !== undefined && isMultiValueField(def);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The parent-relationship condition, compiled to match the field's ARITY.
|
|
109
|
+
*
|
|
110
|
+
* - single-valued → `{ [relationshipField]: parentId }` — equality, byte for
|
|
111
|
+
* byte what both surfaces have always sent;
|
|
112
|
+
* - `multiple: true` (per the spec predicate) →
|
|
113
|
+
* `{ [relationshipField]: { $contains: parentId } }` — MEMBERSHIP, because
|
|
114
|
+
* the stored value is an ARRAY of ids and equality asks whether that whole
|
|
115
|
+
* array IS one id. `$contains` is the spelling the drivers execute for it,
|
|
116
|
+
* the one `driver-sql` names in the `400 INVALID_FILTER` it answers the
|
|
117
|
+
* equality form with.
|
|
118
|
+
*
|
|
119
|
+
* The author never writes either: they named a relationship, and its storage
|
|
120
|
+
* form is the renderer's business.
|
|
121
|
+
*
|
|
122
|
+
* The return value is the plain MongoDB-style object both call sites have
|
|
123
|
+
* always put on the wire, NOT a lowered ObjectQL AST. That is deliberate: with
|
|
124
|
+
* nothing else declared the query must stay byte-identical to what it was, and
|
|
125
|
+
* a freshly lowered AST would mean the same thing, be invisible on screen, and
|
|
126
|
+
* break every caller pinning the wire. Composition with a list's own declared
|
|
127
|
+
* scope stays the caller's step, through `mergeFilterNodes`.
|
|
128
|
+
*
|
|
129
|
+
* @param relationshipField The child field pointing back at the parent.
|
|
130
|
+
* @param parentId The parent record's primary key value.
|
|
131
|
+
* @param fields The CHILD object's field defs (`objectSchema.fields`),
|
|
132
|
+
* in either served shape. Without them the condition
|
|
133
|
+
* compiles to equality — the historical wire, and the
|
|
134
|
+
* only answer available to a caller that cannot see
|
|
135
|
+
* the metadata. A caller that CAN see it must pass it.
|
|
136
|
+
*/
|
|
137
|
+
export function composeParentScopeFilter(relationshipField, parentId, fields) {
|
|
138
|
+
const multi = isMultiValueRelationship(fields, relationshipField);
|
|
139
|
+
return { [relationshipField]: multi ? { $contains: parentId } : parentId };
|
|
140
|
+
}
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
* predicate.)
|
|
33
33
|
*
|
|
34
34
|
* Since objectstack#8018 the same reader also carries the non-predicate row keys
|
|
35
|
-
* a view's actions read
|
|
35
|
+
* a view's actions read: `recordIdField`, since objectui#10277 the field a
|
|
36
|
+
* `defaultFromRow` param seeds from and the `{field}` tokens of an action's
|
|
37
|
+
* `target`, and since objectui#10404 the fields an `undoable` action writes,
|
|
38
|
+
* whose prior values the Undo capture reads off the row. The name kept the
|
|
36
39
|
* "predicate" spelling because the mechanism is identical (harvest a name, gate
|
|
37
40
|
* it against the declared fields, add it to `$select`); see
|
|
38
41
|
* {@link listViewPredicates} for what is in the set and why.
|
|
@@ -85,24 +88,54 @@ export declare function collectPredicateFieldRefs(predicates: readonly unknown[]
|
|
|
85
88
|
* kebab (custom defs AND the built-in Edit/Delete overrides), the selection bar,
|
|
86
89
|
* and the object's declared actions.
|
|
87
90
|
*
|
|
88
|
-
* Most entries are predicates.
|
|
91
|
+
* Most entries are predicates. Five are not, and all are spelled as a synthetic
|
|
89
92
|
* `record.<name>` so the one harvester handles them: conditional formatting's
|
|
90
|
-
* native `{ field, operator, value }` shape,
|
|
91
|
-
* (objectstack#8018)
|
|
92
|
-
*
|
|
93
|
+
* native `{ field, operator, value }` shape, an action's `recordIdField`
|
|
94
|
+
* (objectstack#8018), the field each `defaultFromRow` param seeds from, each
|
|
95
|
+
* `{field}` token of the action's `target` (both objectui#10277), and the fields
|
|
96
|
+
* an `undoable` action writes (objectui#10404). The
|
|
97
|
+
* `recordIdField` case is the same *class* of bug the predicate harvest exists
|
|
98
|
+
* to close, one surface over — the action runtime reads
|
|
93
99
|
* `rowRecord[action.recordIdField]` to seed `recordIdParam`, so a key outside the
|
|
94
100
|
* listView columns arrived absent and the injection was silently skipped, which
|
|
95
101
|
* turns a record-scoped mutation into one that names no record while still
|
|
96
102
|
* reporting success. The default (`id`) is already projected unconditionally, so
|
|
97
103
|
* only an explicit declaration adds anything here.
|
|
98
104
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* and
|
|
105
|
+
* The last two are the same class again, and they were silent in the same way.
|
|
106
|
+
* `resolveActionParam` seeds a `defaultFromRow` param only when the row HAS the
|
|
107
|
+
* key (an own-property check), so an absent key opened the param dialog with the
|
|
108
|
+
* field blank; and the console's `api` handler fills a `{field}` token from the
|
|
109
|
+
* row, so an absent key put an empty path segment into the URL. The key a param
|
|
110
|
+
* seeds from is `field ?? name` — the runtime's precedence, not the param's
|
|
111
|
+
* payload name — and the token grammar is the handler's own. Neither is
|
|
112
|
+
* narrowed by the action's `type`, for the asymmetry stated below: an extra
|
|
113
|
+
* column costs bytes.
|
|
114
|
+
*
|
|
115
|
+
* The written fields are the same class once more, and the worst of them,
|
|
116
|
+
* because what went wrong was a write. The Undo capture of an `undoable` update
|
|
117
|
+
* reads each written field's prior value off the row; on a projected row a
|
|
118
|
+
* written field no column shows was absent, the capture recorded `null`, and
|
|
119
|
+
* Undo then wrote `null` over the stored value it existed to restore. The
|
|
120
|
+
* written keys are `patch`, each param's collected key and `bodyExtra` (see
|
|
121
|
+
* `undoableWrittenKeys`). This one IS narrowed, to `undoable`, and not for
|
|
122
|
+
* cost: the capture is the only reader of a written field's prior value, and it
|
|
123
|
+
* runs only under `undoable`, so without it the row reads none of these keys.
|
|
124
|
+
*
|
|
125
|
+
* A `recordIdField`, param key or written key that is not a bare identifier is
|
|
126
|
+
* dropped here
|
|
127
|
+
* (a `target` token is one by construction of its grammar), and a name the
|
|
128
|
+
* object does not declare is dropped by the caller's `isProjectableField`
|
|
129
|
+
* guard. Both drops are the safe direction: such a name is not a column
|
|
130
|
+
* anywhere, and an unknown key in `$select` is not ignored by every backend.
|
|
131
|
+
* The identifier gate is not decoration — without it the harvester's regex would
|
|
132
|
+
* read a PREFIX of a malformed name (`record.not a field` → `not`) and
|
|
133
|
+
* contribute a plausible wrong field instead of nothing.
|
|
134
|
+
*
|
|
135
|
+
* ⛔ Harvesting is not a read grant. Every name here is a CANDIDATE: each
|
|
136
|
+
* consumer still passes it through its own field-level read filter before it
|
|
137
|
+
* reaches `$select`, so a field the principal may not read is not requested
|
|
138
|
+
* because an action names it.
|
|
106
139
|
*
|
|
107
140
|
* `objectActions` is deliberately the object's WHOLE action set rather than
|
|
108
141
|
* only the ones this view promotes. Narrowing it would mean re-running the
|