@object-ui/core 17.6.0 → 17.7.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 +6610 -0
- package/README.md +101 -14
- package/dist/actions/ActionEngine.d.ts +4 -2
- package/dist/actions/ActionEngine.js +13 -8
- package/dist/actions/ActionRunner.d.ts +288 -27
- package/dist/actions/ActionRunner.js +443 -63
- package/dist/actions/TransactionManager.d.ts +7 -2
- package/dist/actions/actionKeys.d.ts +9 -7
- package/dist/actions/actionKeys.js +57 -14
- package/dist/actions/actionResponse.js +2 -2
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/recordDelete.d.ts +118 -0
- package/dist/actions/recordDelete.js +112 -0
- 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/builder/schema-builder.d.ts +38 -81
- package/dist/builder/schema-builder.js +33 -152
- package/dist/data-scope/ViewDataProvider.d.ts +1 -1
- package/dist/data-scope/ViewDataProvider.js +21 -1
- 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/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 +203 -31
- package/dist/evaluator/index.d.ts +1 -0
- package/dist/evaluator/index.js +1 -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 +76 -0
- package/dist/evaluator/subjectPermissions.js +44 -0
- package/dist/evaluator/unevaluablePredicate.d.ts +57 -0
- package/dist/evaluator/unevaluablePredicate.js +70 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +80 -2
- package/dist/protocols/index.d.ts +0 -1
- package/dist/protocols/index.js +9 -1
- package/dist/registry/Registry.d.ts +358 -31
- package/dist/registry/Registry.js +263 -11
- 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/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 +131 -5
- package/dist/utils/chart-series.js +98 -16
- 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 +354 -0
- package/dist/utils/date-display.js +463 -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 +76 -6
- package/dist/utils/expand-fields.js +76 -6
- package/dist/utils/extract-records.d.ts +65 -5
- package/dist/utils/extract-records.js +65 -8
- package/dist/utils/filter-converter.d.ts +152 -4
- package/dist/utils/filter-converter.js +1092 -22
- 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 +121 -1
- package/dist/utils/normalize-list-view.js +186 -6
- 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 +137 -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 +200 -82
- 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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — the field-read rule for a single record
|
|
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 two members of a loaded permission policy the field-read rule asks.
|
|
10
|
+
*
|
|
11
|
+
* Structural on purpose: `usePermissions()` from `@object-ui/permissions`
|
|
12
|
+
* satisfies it as it is, and so does any caller-supplied policy of the same
|
|
13
|
+
* shape, so this package depends on neither React nor the permissions package.
|
|
14
|
+
*/
|
|
15
|
+
interface FieldReadPolicy {
|
|
16
|
+
/** False until a policy has loaded; also false with no provider mounted. */
|
|
17
|
+
readonly isLoaded: boolean;
|
|
18
|
+
/** May the viewer READ `field` on `object`? */
|
|
19
|
+
checkField(object: string, field: string, action: 'read'): boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* `record` as the viewer may READ it on `objectName`: every field the loaded
|
|
23
|
+
* `policy` denies is removed, which leaves the row ObjectStack's `FieldMasker`
|
|
24
|
+
* already serves (objectui#10594).
|
|
25
|
+
*
|
|
26
|
+
* The renderer-side field-level-security rulings (objectui#7215 /
|
|
27
|
+
* objectui#7230) say FLS gates the OUTPUT: a value the loaded policy denies is
|
|
28
|
+
* not drawn, even when a backend serves it. Every surface that builds a display
|
|
29
|
+
* value from a whole row (a record title, a lookup label, a search-hit label, a
|
|
30
|
+
* person's name) calls this one function, so a denied field reads as an absent
|
|
31
|
+
* field on every surface at once, and the display ladder falls through to its
|
|
32
|
+
* next source exactly as it does for the stripped row. Field-LIST filters (which
|
|
33
|
+
* columns to head, which relations to `$expand`) are a different shape and are
|
|
34
|
+
* not this function.
|
|
35
|
+
*
|
|
36
|
+
* - `id` and `_id` are never judged, nor is any key in `extraKeep` (a declared
|
|
37
|
+
* id field): an id addresses the record and is not a field value the policy
|
|
38
|
+
* withholds, and the `Record #<id>` floor reads it.
|
|
39
|
+
* - Before a policy loads, with no policy, with no object to judge the fields
|
|
40
|
+
* against, or for a value that is not an object, `record` comes back as it
|
|
41
|
+
* is.
|
|
42
|
+
* - When nothing is withheld, the SAME object comes back, so a caller can tell
|
|
43
|
+
* "nothing withheld" from "something withheld" by identity and a cache keyed
|
|
44
|
+
* on the served row keeps hitting.
|
|
45
|
+
*
|
|
46
|
+
* Pure: it reads `policy.checkField` and never writes to `record`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withoutDeniedFields<T>(record: T, policy: FieldReadPolicy | null | undefined, objectName: string | undefined, extraKeep?: readonly string[]): T;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI — the field-read rule for a single record
|
|
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
|
+
* `record` as the viewer may READ it on `objectName`: every field the loaded
|
|
10
|
+
* `policy` denies is removed, which leaves the row ObjectStack's `FieldMasker`
|
|
11
|
+
* already serves (objectui#10594).
|
|
12
|
+
*
|
|
13
|
+
* The renderer-side field-level-security rulings (objectui#7215 /
|
|
14
|
+
* objectui#7230) say FLS gates the OUTPUT: a value the loaded policy denies is
|
|
15
|
+
* not drawn, even when a backend serves it. Every surface that builds a display
|
|
16
|
+
* value from a whole row (a record title, a lookup label, a search-hit label, a
|
|
17
|
+
* person's name) calls this one function, so a denied field reads as an absent
|
|
18
|
+
* field on every surface at once, and the display ladder falls through to its
|
|
19
|
+
* next source exactly as it does for the stripped row. Field-LIST filters (which
|
|
20
|
+
* columns to head, which relations to `$expand`) are a different shape and are
|
|
21
|
+
* not this function.
|
|
22
|
+
*
|
|
23
|
+
* - `id` and `_id` are never judged, nor is any key in `extraKeep` (a declared
|
|
24
|
+
* id field): an id addresses the record and is not a field value the policy
|
|
25
|
+
* withholds, and the `Record #<id>` floor reads it.
|
|
26
|
+
* - Before a policy loads, with no policy, with no object to judge the fields
|
|
27
|
+
* against, or for a value that is not an object, `record` comes back as it
|
|
28
|
+
* is.
|
|
29
|
+
* - When nothing is withheld, the SAME object comes back, so a caller can tell
|
|
30
|
+
* "nothing withheld" from "something withheld" by identity and a cache keyed
|
|
31
|
+
* on the served row keeps hitting.
|
|
32
|
+
*
|
|
33
|
+
* Pure: it reads `policy.checkField` and never writes to `record`.
|
|
34
|
+
*/
|
|
35
|
+
export function withoutDeniedFields(record, policy, objectName, extraKeep) {
|
|
36
|
+
if (!policy?.isLoaded || !objectName || !record || typeof record !== 'object')
|
|
37
|
+
return record;
|
|
38
|
+
const shown = {};
|
|
39
|
+
let withheld = false;
|
|
40
|
+
for (const [key, value] of Object.entries(record)) {
|
|
41
|
+
if (key === 'id' ||
|
|
42
|
+
key === '_id' ||
|
|
43
|
+
extraKeep?.includes(key) ||
|
|
44
|
+
policy.checkField(objectName, key, 'read')) {
|
|
45
|
+
shown[key] = value;
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
withheld = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return withheld ? shown : record;
|
|
52
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @object-ui/core - Validation Module
|
|
3
3
|
*
|
|
4
|
-
* Phase 3.5: Validation
|
|
4
|
+
* Phase 3.5: Validation module
|
|
5
5
|
*
|
|
6
6
|
* Object-level validation. The rule vocabulary is owned by
|
|
7
7
|
* `@objectstack/spec/data` and derived in `@object-ui/types`; canonicity is
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The object-level rule ENGINE (`validators/`) is @deprecated (#3110) — the
|
|
11
11
|
* server is the single implementation of rule enforcement. Field/record SHAPE
|
|
12
|
-
* validation (`schema-validator.js
|
|
13
|
-
*
|
|
12
|
+
* validation (`schema-validator.js`) is unaffected: it is client-side form
|
|
13
|
+
* validation, not a mirror of a server rule.
|
|
14
|
+
*
|
|
15
|
+
* The field-rule engine that once sat beside it (`ValidationEngine`, a second
|
|
16
|
+
* snake_case rule vocabulary no renderer consumed) was retired under ADR-0049
|
|
17
|
+
* enforce-or-remove (objectui#7659). Client-side field rules are compiled by
|
|
18
|
+
* `buildValidationRules` in `@object-ui/fields`.
|
|
14
19
|
*/
|
|
15
20
|
export * from './required-presence.js';
|
|
16
21
|
export * from './server-owned-value.js';
|
|
17
|
-
export * from './validation-engine.js';
|
|
18
22
|
export * from './schema-validator.js';
|
|
19
23
|
export * from './validators/index.js';
|
package/dist/validation/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @object-ui/core - Validation Module
|
|
3
3
|
*
|
|
4
|
-
* Phase 3.5: Validation
|
|
4
|
+
* Phase 3.5: Validation module
|
|
5
5
|
*
|
|
6
6
|
* Object-level validation. The rule vocabulary is owned by
|
|
7
7
|
* `@objectstack/spec/data` and derived in `@object-ui/types`; canonicity is
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
*
|
|
10
10
|
* The object-level rule ENGINE (`validators/`) is @deprecated (#3110) — the
|
|
11
11
|
* server is the single implementation of rule enforcement. Field/record SHAPE
|
|
12
|
-
* validation (`schema-validator.js
|
|
13
|
-
*
|
|
12
|
+
* validation (`schema-validator.js`) is unaffected: it is client-side form
|
|
13
|
+
* validation, not a mirror of a server rule.
|
|
14
|
+
*
|
|
15
|
+
* The field-rule engine that once sat beside it (`ValidationEngine`, a second
|
|
16
|
+
* snake_case rule vocabulary no renderer consumed) was retired under ADR-0049
|
|
17
|
+
* enforce-or-remove (objectui#7659). Client-side field rules are compiled by
|
|
18
|
+
* `buildValidationRules` in `@object-ui/fields`.
|
|
14
19
|
*/
|
|
15
20
|
export * from './required-presence.js';
|
|
16
21
|
export * from './server-owned-value.js';
|
|
17
|
-
export * from './validation-engine.js';
|
|
18
22
|
export * from './schema-validator.js';
|
|
19
23
|
export * from './validators/index.js';
|
|
@@ -69,7 +69,7 @@ export interface SchemaNodeValidationResult {
|
|
|
69
69
|
* }
|
|
70
70
|
* ```
|
|
71
71
|
*/
|
|
72
|
-
export declare function validateSchema(schema:
|
|
72
|
+
export declare function validateSchema(schema: unknown, path?: string): SchemaNodeValidationResult;
|
|
73
73
|
/**
|
|
74
74
|
* Assert that a schema is valid, throwing an error if not
|
|
75
75
|
*
|
|
@@ -5,6 +5,82 @@
|
|
|
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 { hasDeclaredPredicate } from '../evaluator/declaredPredicate.js';
|
|
9
|
+
import { isUnevaluablePredicate } from '../evaluator/unevaluablePredicate.js';
|
|
10
|
+
/**
|
|
11
|
+
* The rule for a PREDICATE GATE key (`visible` / `disabled`) — objectui#6505.
|
|
12
|
+
*
|
|
13
|
+
* ## What this rule used to say, and what it cost
|
|
14
|
+
*
|
|
15
|
+
* Both keys read `typeof value === 'boolean'`, message `"<key> must be a
|
|
16
|
+
* boolean"`. Both keys are EXPRESSIONS everywhere else in the system: AGENTS.md
|
|
17
|
+
* §4 declares the protocol as `hidden?: string; // expression` /
|
|
18
|
+
* `disabled?: string; // expression`, `SchemaRenderer` evaluates them through
|
|
19
|
+
* `hasDeclaredPredicate` + `evaluateCondition`, `@objectstack/spec` normalizes
|
|
20
|
+
* every authored predicate into a `{ dialect, source }` envelope, and the
|
|
21
|
+
* objectui#3862 / objectui#3955 rulings are entirely about which EXPRESSION
|
|
22
|
+
* spellings count as declared. So the node the docs teach —
|
|
23
|
+
* `{ type: 'button', disabled: "${record.stage == 'closed'}" }` — was reported
|
|
24
|
+
* `disabled must be a boolean` by the dev-mode validator and its host element
|
|
25
|
+
* got `data-obj-schema-invalid`, the cue apps hang a red outline off.
|
|
26
|
+
*
|
|
27
|
+
* `BASE_SCHEMA_RULES` was the ONE place in the repo that disagreed with the
|
|
28
|
+
* protocol, so this restores declared = enforced rather than widening a
|
|
29
|
+
* contract: the accept set moves to exactly what the runtime already accepts.
|
|
30
|
+
*
|
|
31
|
+
* ## Why `hasDeclaredPredicate` and not a check written here
|
|
32
|
+
*
|
|
33
|
+
* `evaluator/declaredPredicate.ts` is the repo's single definition of "is a
|
|
34
|
+
* predicate gate DECLARED on this value?" (objectui#3850's ruling; nothing in
|
|
35
|
+
* the repo asks that question anywhere else). A second, hand-rolled answer in
|
|
36
|
+
* this table is how the validator and the renderer come to disagree about the
|
|
37
|
+
* same value — which is the defect class this rule was already an instance of.
|
|
38
|
+
* The delegation is behavioural, so it is pinned behaviourally: the drift pin in
|
|
39
|
+
* `__tests__/predicate-valued-gate-rules.test.ts` asserts this rule's verdict
|
|
40
|
+
* equals `boolean || (declared && !unevaluable)` across every probe — the second
|
|
41
|
+
* half since objectui#11358, see below.
|
|
42
|
+
*
|
|
43
|
+
* ## Why the boolean arm survives even though it is subsumed
|
|
44
|
+
*
|
|
45
|
+
* `hasDeclaredPredicate(true)` and `hasDeclaredPredicate(false)` are both
|
|
46
|
+
* `true` today (`toPredicateInput` returns a boolean unchanged), so the first
|
|
47
|
+
* arm changes no verdict. It is kept because it makes this rule's accept set a
|
|
48
|
+
* provable SUPERSET of the one it replaces, locally: a future narrowing on the
|
|
49
|
+
* declaredness side cannot silently start reporting `disabled: false` — the
|
|
50
|
+
* most explicit gate an author can write — as an invalid schema.
|
|
51
|
+
*
|
|
52
|
+
* ## What it still REFUSES (the half that is not negotiable)
|
|
53
|
+
*
|
|
54
|
+
* `null`, `''`, whitespace-only predicate text and the empty / blank-`source`
|
|
55
|
+
* envelope (objectui#3960) — what `hasDeclaredPredicate` calls "not declared" —
|
|
56
|
+
* and a value that IS declared but cannot be evaluated: a number, `{}`, an
|
|
57
|
+
* array, an envelope with no string `source` (`isUnevaluablePredicate`).
|
|
58
|
+
* Every one of those was refused before this change too — the accept set
|
|
59
|
+
* widens and nothing refused becomes accepted. Dropping the two keys from this
|
|
60
|
+
* table was the option this fix was explicitly forbidden to take: an absent
|
|
61
|
+
* rule reports nothing at all, which is gate weakening wearing the same green.
|
|
62
|
+
*
|
|
63
|
+
* ## Why "declared" alone stopped being this rule's answer (objectui#11358)
|
|
64
|
+
*
|
|
65
|
+
* Until objectui#11358 every declared gate was also an evaluable one, so
|
|
66
|
+
* "declared" answered both questions. That ruling made a present value with no
|
|
67
|
+
* evaluable `source` DECLARED (and faulting) so that the runtime fails it in
|
|
68
|
+
* its key's fault direction instead of reading it as no gate — and this rule
|
|
69
|
+
* asks a different question, "is this a predicate the runtime can evaluate?".
|
|
70
|
+
* Reading `hasDeclaredPredicate` alone would have widened the accept set to
|
|
71
|
+
* exactly the shapes that ruling exists to stop. So the rule refuses the
|
|
72
|
+
* unevaluable state by the same internal definition the runtime uses for it,
|
|
73
|
+
* not by a list written here.
|
|
74
|
+
*/
|
|
75
|
+
function predicateGateRule(key) {
|
|
76
|
+
return {
|
|
77
|
+
required: false,
|
|
78
|
+
validate: (value) => typeof value === 'boolean' || (!isUnevaluablePredicate(value) && hasDeclaredPredicate(value)),
|
|
79
|
+
message: `${key} must be a boolean or a declared predicate: an expression string ` +
|
|
80
|
+
`(bare, e.g. "record.stage == 'closed'", or the "\${...}" template ` +
|
|
81
|
+
`spelling), or a { dialect, source } expression envelope`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
8
84
|
/**
|
|
9
85
|
* Validation rules for base schema
|
|
10
86
|
*/
|
|
@@ -24,31 +100,39 @@ const BASE_SCHEMA_RULES = {
|
|
|
24
100
|
validate: (value) => typeof value === 'string',
|
|
25
101
|
message: 'className must be a string'
|
|
26
102
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
},
|
|
32
|
-
disabled: {
|
|
33
|
-
required: false,
|
|
34
|
-
validate: (value) => typeof value === 'boolean',
|
|
35
|
-
message: 'disabled must be a boolean'
|
|
36
|
-
}
|
|
103
|
+
// objectui#6505 — both keys are EXPRESSIONS in the protocol, not booleans.
|
|
104
|
+
// See `predicateGateRule` above for the accept set and what it still refuses.
|
|
105
|
+
visible: predicateGateRule('visible'),
|
|
106
|
+
disabled: predicateGateRule('disabled')
|
|
37
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* The ONE narrowing between {@link validateSchema}'s `unknown` parameter and
|
|
110
|
+
* every rule below it (objectui#8416).
|
|
111
|
+
*
|
|
112
|
+
* The VERDICT is the guard this replaces, spelled the same way: the
|
|
113
|
+
* `!schema || typeof schema !== 'object'` test that used to sit at the top of
|
|
114
|
+
* `validateBaseSchema`. `null`, `undefined` and every primitive are refused;
|
|
115
|
+
* arrays and plain objects pass, exactly as before.
|
|
116
|
+
*
|
|
117
|
+
* What changes is WHERE it runs. Only ONE of the four rules ever had it, and
|
|
118
|
+
* `validateSchema` calls all four unconditionally: `validateFormSchema` read
|
|
119
|
+
* `schema.type` and `validateChildren` read `schema.children` with no guard at
|
|
120
|
+
* all, so `validateSchema(null)` and `validateSchema(undefined)` threw a raw
|
|
121
|
+
* `TypeError: Cannot read properties of null (reading 'type')` — measured on
|
|
122
|
+
* 256c709e2 — instead of returning the `INVALID_SCHEMA` result this function's
|
|
123
|
+
* own contract promises, and `isValidSchema(null)` threw instead of answering
|
|
124
|
+
* `false`. `SchemaRenderer` wraps its call in a `try`/`catch` whose comment
|
|
125
|
+
* reads "Validator itself failed — surface but don't crash render"; that catch
|
|
126
|
+
* is where the crash was landing.
|
|
127
|
+
*/
|
|
128
|
+
function isSchemaNodeShape(value) {
|
|
129
|
+
return !!value && typeof value === 'object';
|
|
130
|
+
}
|
|
38
131
|
/**
|
|
39
132
|
* Validate a schema against base rules
|
|
40
133
|
*/
|
|
41
134
|
function validateBaseSchema(schema, path = 'schema') {
|
|
42
135
|
const errors = [];
|
|
43
|
-
if (!schema || typeof schema !== 'object') {
|
|
44
|
-
errors.push({
|
|
45
|
-
path,
|
|
46
|
-
message: 'Schema must be an object',
|
|
47
|
-
type: 'error',
|
|
48
|
-
code: 'INVALID_SCHEMA'
|
|
49
|
-
});
|
|
50
|
-
return errors;
|
|
51
|
-
}
|
|
52
136
|
// Validate required and optional properties
|
|
53
137
|
Object.entries(BASE_SCHEMA_RULES).forEach(([key, rule]) => {
|
|
54
138
|
const value = schema[key];
|
|
@@ -72,56 +156,66 @@ function validateBaseSchema(schema, path = 'schema') {
|
|
|
72
156
|
return errors;
|
|
73
157
|
}
|
|
74
158
|
/**
|
|
75
|
-
*
|
|
159
|
+
* TOMBSTONE table — node `type` spellings this renderer has RETIRED, mapped to
|
|
160
|
+
* the prescription an author must follow instead (ADR-0049 enforce-or-remove).
|
|
161
|
+
*
|
|
162
|
+
* A retired spelling is not merely absent from this file. Absence here means
|
|
163
|
+
* {@link validateSchema} runs `validateBaseSchema` — which only asks that
|
|
164
|
+
* `type` be a non-empty string — finds nothing type-specific to say, and
|
|
165
|
+
* returns `valid: true`. That silence is the failure mode this table exists to
|
|
166
|
+
* prevent: the author is told their schema is fine and then gets the OBJUI-001
|
|
167
|
+
* "Unknown component type" panel at render time, one layer too late to act on.
|
|
168
|
+
* So a retired spelling is refused BY NAME, with the migration in the message.
|
|
169
|
+
*
|
|
170
|
+
* `crud` (objectui#5373, maintainer ruling of 2026-08-20): `CRUDSchema` carried
|
|
171
|
+
* FOUR declaration faces — the TS interface, the zod mirror, a dedicated branch
|
|
172
|
+
* in this very function, and `CRUDBuilder` — and never once had a registered
|
|
173
|
+
* renderer, for the whole life of the key. Every face taught an author (and an
|
|
174
|
+
* AI author reading the published reference page) that the type existed. The
|
|
175
|
+
* branch that used to sit here is what made this validator the loudest of the
|
|
176
|
+
* four lies: it read `schema.type === 'crud'`, checked that `columns` was an
|
|
177
|
+
* array, and returned no error — an affirmative "your CRUD schema is valid"
|
|
178
|
+
* for a node that renders nothing.
|
|
179
|
+
*
|
|
180
|
+
* Keyed by the authored spelling, and quantified over the table rather than
|
|
181
|
+
* written per spelling, so the next retirement closes this face the day it
|
|
182
|
+
* lands here.
|
|
76
183
|
*/
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
schema.fields.forEach((field, index) => {
|
|
113
|
-
if (!field.name) {
|
|
114
|
-
errors.push({
|
|
115
|
-
path: `${path}.fields[${index}]`,
|
|
116
|
-
message: 'Field requires name property',
|
|
117
|
-
type: 'error',
|
|
118
|
-
code: 'MISSING_FIELD_NAME'
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return errors;
|
|
184
|
+
const RETIRED_NODE_TYPES = Object.freeze({
|
|
185
|
+
crud: "Node type `crud` was RETIRED (objectui#5373, ADR-0049 enforce-or-remove). " +
|
|
186
|
+
"`CRUDSchema` declared it in four places and no renderer ever registered it, " +
|
|
187
|
+
"so a node spelling it painted the OBJUI-001 \"Unknown component type\" panel. " +
|
|
188
|
+
"Compose the shapes that DO render instead: `object-grid` for the record " +
|
|
189
|
+
"table with its toolbar, filters, pagination and row/batch actions, " +
|
|
190
|
+
"`object-form` for the create/edit form, and `detail` for the record view.",
|
|
191
|
+
});
|
|
192
|
+
/**
|
|
193
|
+
* Refuse a retired node-type spelling by name.
|
|
194
|
+
*
|
|
195
|
+
* Severity is `error`, not `warning`, and that is the whole point of the
|
|
196
|
+
* retirement: a warning leaves `result.valid === true`, so `assertValidSchema`
|
|
197
|
+
* would still not throw and `isValidSchema` would still answer `true` — the
|
|
198
|
+
* same silence, one console line louder.
|
|
199
|
+
*
|
|
200
|
+
* `hasOwnProperty` rather than a plain index read: a `type` of `'constructor'`
|
|
201
|
+
* or `'toString'` reaches `Object.prototype` and would answer truthy.
|
|
202
|
+
*
|
|
203
|
+
* Reached for every node in the tree, not just the root — {@link validateSchema}
|
|
204
|
+
* is what `validateChildren` recurses with, so a retired spelling nested inside
|
|
205
|
+
* a `children` array is refused with its own path.
|
|
206
|
+
*/
|
|
207
|
+
function validateRetiredNodeType(schema, path = 'schema') {
|
|
208
|
+
const type = schema.type;
|
|
209
|
+
if (typeof type !== 'string')
|
|
210
|
+
return [];
|
|
211
|
+
if (!Object.prototype.hasOwnProperty.call(RETIRED_NODE_TYPES, type))
|
|
212
|
+
return [];
|
|
213
|
+
return [{
|
|
214
|
+
path: `${path}.type`,
|
|
215
|
+
message: RETIRED_NODE_TYPES[type],
|
|
216
|
+
type: 'error',
|
|
217
|
+
code: 'RETIRED_TYPE'
|
|
218
|
+
}];
|
|
125
219
|
}
|
|
126
220
|
/**
|
|
127
221
|
* The ONE namespace a form field's widget id may name — objectui#5254,
|
|
@@ -228,8 +322,15 @@ function validateFieldWidgetNamespace(field, path) {
|
|
|
228
322
|
function validateFormSchema(schema, path = 'schema') {
|
|
229
323
|
const errors = [];
|
|
230
324
|
if (schema.type === 'form') {
|
|
231
|
-
|
|
232
|
-
|
|
325
|
+
const fields = schema.fields;
|
|
326
|
+
if (Array.isArray(fields)) {
|
|
327
|
+
fields.forEach((entry, index) => {
|
|
328
|
+
// Narrowed before it is read, for the same reason the root is: a
|
|
329
|
+
// `fields` array is authored data too, and reading `field.name` off
|
|
330
|
+
// whatever an author put there made `fields: [null]` throw the same raw
|
|
331
|
+
// `TypeError` a `null` root did. A non-object entry keeps its old
|
|
332
|
+
// verdict — every key reads `undefined`, so `MISSING_FIELD_NAME`.
|
|
333
|
+
const field = isSchemaNodeShape(entry) ? entry : {};
|
|
233
334
|
if (!field.name) {
|
|
234
335
|
errors.push({
|
|
235
336
|
path: `${path}.fields[${index}]`,
|
|
@@ -245,11 +346,11 @@ function validateFormSchema(schema, path = 'schema') {
|
|
|
245
346
|
errors.push(namespaceError);
|
|
246
347
|
}
|
|
247
348
|
// Check for duplicate field names
|
|
248
|
-
const duplicates =
|
|
349
|
+
const duplicates = fields.filter((other) => (isSchemaNodeShape(other) ? other.name : undefined) === field.name);
|
|
249
350
|
if (duplicates.length > 1) {
|
|
250
351
|
errors.push({
|
|
251
352
|
path: `${path}.fields[${index}]`,
|
|
252
|
-
message: `Duplicate field name: ${field.name}`,
|
|
353
|
+
message: `Duplicate field name: ${String(field.name)}`,
|
|
253
354
|
type: 'warning',
|
|
254
355
|
code: 'DUPLICATE_FIELD_NAME'
|
|
255
356
|
});
|
|
@@ -264,17 +365,21 @@ function validateFormSchema(schema, path = 'schema') {
|
|
|
264
365
|
*/
|
|
265
366
|
function validateChildren(schema, path = 'schema') {
|
|
266
367
|
const errors = [];
|
|
267
|
-
|
|
368
|
+
// One spelling. This walker resolved `children || body` for ANY node type,
|
|
369
|
+
// so it outlived every per-registration read of the dialect — objectui#6771
|
|
370
|
+
// retired it, and a recursive validator that still descended `body` would
|
|
371
|
+
// keep validating a child list no renderer puts on the page.
|
|
372
|
+
const children = schema.children;
|
|
268
373
|
if (children) {
|
|
269
374
|
if (Array.isArray(children)) {
|
|
270
375
|
children.forEach((child, index) => {
|
|
271
|
-
if (
|
|
376
|
+
if (isSchemaNodeShape(child)) {
|
|
272
377
|
const childResult = validateSchema(child, `${path}.children[${index}]`);
|
|
273
378
|
errors.push(...childResult.errors, ...childResult.warnings);
|
|
274
379
|
}
|
|
275
380
|
});
|
|
276
381
|
}
|
|
277
|
-
else if (
|
|
382
|
+
else if (isSchemaNodeShape(children)) {
|
|
278
383
|
const childResult = validateSchema(children, `${path}.children`);
|
|
279
384
|
errors.push(...childResult.errors, ...childResult.warnings);
|
|
280
385
|
}
|
|
@@ -304,13 +409,26 @@ function validateChildren(schema, path = 'schema') {
|
|
|
304
409
|
*/
|
|
305
410
|
export function validateSchema(schema, path = 'schema') {
|
|
306
411
|
const allErrors = [];
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
//
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
412
|
+
// Narrow ONCE, here, before any rule reads a key off the value — see
|
|
413
|
+
// `isSchemaNodeShape`. Every rule below is handed a value already proven to
|
|
414
|
+
// be an object, which is why none of them re-tests it.
|
|
415
|
+
if (!isSchemaNodeShape(schema)) {
|
|
416
|
+
allErrors.push({
|
|
417
|
+
path,
|
|
418
|
+
message: 'Schema must be an object',
|
|
419
|
+
type: 'error',
|
|
420
|
+
code: 'INVALID_SCHEMA'
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
// Validate base schema
|
|
425
|
+
allErrors.push(...validateBaseSchema(schema, path));
|
|
426
|
+
// Validate type-specific schemas
|
|
427
|
+
allErrors.push(...validateRetiredNodeType(schema, path));
|
|
428
|
+
allErrors.push(...validateFormSchema(schema, path));
|
|
429
|
+
// Validate children recursively
|
|
430
|
+
allErrors.push(...validateChildren(schema, path));
|
|
431
|
+
}
|
|
314
432
|
const errors = allErrors.filter(e => e.type === 'error');
|
|
315
433
|
const warnings = allErrors.filter(e => e.type === 'warning');
|
|
316
434
|
return {
|
|
@@ -186,7 +186,7 @@ export declare class ObjectValidationEngine {
|
|
|
186
186
|
* - no `initialStates` declared → allow (legacy no-op)
|
|
187
187
|
* - field absent from the write → allow (requiredness is not our job)
|
|
188
188
|
* - value empty → allow (ditto)
|
|
189
|
-
* - value ∉ initialStates → reject (the FSM entry point, #3165)
|
|
189
|
+
* - value ∉ initialStates → reject (the FSM entry point, objectstack-ai/objectstack#3165)
|
|
190
190
|
*
|
|
191
191
|
* UPDATE
|
|
192
192
|
* - field absent from the write → allow (no transition to police)
|
|
@@ -53,7 +53,15 @@ function priorityOf(rule) {
|
|
|
53
53
|
* Simple expression evaluator using a simple parser (no dynamic code execution)
|
|
54
54
|
*
|
|
55
55
|
* SECURITY: This implementation parses expressions into an AST and evaluates them
|
|
56
|
-
* without using eval() or new Function().
|
|
56
|
+
* without using eval() or new Function(). That is a remediation, not a style
|
|
57
|
+
* preference: an earlier revision compiled the predicate with
|
|
58
|
+
* `new Function(...contextKeys, "'use strict'; return (" + expression + ")")`,
|
|
59
|
+
* and CodeQL flagged it as "Unsafe code constructed from library input" —
|
|
60
|
+
* expressions reach this class from validation metadata, so that shape is a
|
|
61
|
+
* code-injection vector in any consumer that renders metadata it does not
|
|
62
|
+
* control. Never reintroduce eval(), new Function() or any other dynamic-code
|
|
63
|
+
* path here to buy expressiveness or speed; extend the parser, or inject a
|
|
64
|
+
* richer evaluator through the constructor (see NOT CEL below). Supports:
|
|
57
65
|
* - Comparison operators: ==, !=, >, <, >=, <=
|
|
58
66
|
* - Logical operators: &&, ||, !
|
|
59
67
|
* - Property access: record.field, record['field']
|
|
@@ -72,7 +80,10 @@ function priorityOf(rule) {
|
|
|
72
80
|
* pre-check agree with the server on richer predicates, pass a CEL-backed
|
|
73
81
|
* `ValidationExpressionEvaluator` to the constructor.
|
|
74
82
|
*
|
|
75
|
-
*
|
|
83
|
+
* The write-up this block used to link to (`SECURITY_FIX_SUMMARY.md`) was
|
|
84
|
+
* deleted with no successor; its load-bearing reasoning is inline above, and
|
|
85
|
+
* the original text remains readable at
|
|
86
|
+
* `git show ea72f1886^:SECURITY_FIX_SUMMARY.md`.
|
|
76
87
|
*/
|
|
77
88
|
class SimpleExpressionEvaluator {
|
|
78
89
|
evaluate(expression, context) {
|
|
@@ -384,7 +395,7 @@ export class ObjectValidationEngine {
|
|
|
384
395
|
* - no `initialStates` declared → allow (legacy no-op)
|
|
385
396
|
* - field absent from the write → allow (requiredness is not our job)
|
|
386
397
|
* - value empty → allow (ditto)
|
|
387
|
-
* - value ∉ initialStates → reject (the FSM entry point, #3165)
|
|
398
|
+
* - value ∉ initialStates → reject (the FSM entry point, objectstack-ai/objectstack#3165)
|
|
388
399
|
*
|
|
389
400
|
* UPDATE
|
|
390
401
|
* - field absent from the write → allow (no transition to police)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/core",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,15 +25,14 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@objectstack/formula": "^17.
|
|
29
|
-
"@objectstack/spec": "^17.
|
|
30
|
-
"
|
|
31
|
-
"zod": "^4.4.3",
|
|
32
|
-
"@object-ui/types": "17.6.0"
|
|
28
|
+
"@objectstack/formula": "^17.5.0",
|
|
29
|
+
"@objectstack/spec": "^17.6.0",
|
|
30
|
+
"@object-ui/types": "17.7.0"
|
|
33
31
|
},
|
|
34
32
|
"devDependencies": {
|
|
35
33
|
"typescript": "^6.0.3",
|
|
36
|
-
"vitest": "^4.1.10"
|
|
34
|
+
"vitest": "^4.1.10",
|
|
35
|
+
"@object-ui/test-support": "17.4.0"
|
|
37
36
|
},
|
|
38
37
|
"keywords": [
|
|
39
38
|
"objectui",
|
|
@@ -60,8 +59,8 @@
|
|
|
60
59
|
"LICENSE"
|
|
61
60
|
],
|
|
62
61
|
"scripts": {
|
|
63
|
-
"build": "tsc",
|
|
64
|
-
"test": "vitest run",
|
|
62
|
+
"build": "tsc && node ../../scripts/check-dist-completeness.mjs",
|
|
63
|
+
"test": "vitest run --root ../.. packages/core/",
|
|
65
64
|
"type-check": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
66
65
|
"lint": "eslint ."
|
|
67
66
|
}
|