@metaobjectsdev/metadata 0.24.0-rc.1 → 0.24.1
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/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +17 -2
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/field/field-constants.d.ts +18 -5
- package/dist/core/field/field-constants.d.ts.map +1 -1
- package/dist/core/field/field-constants.js +21 -5
- package/dist/core/field/field-constants.js.map +1 -1
- package/dist/core/field/field-definition.embedded.d.ts.map +1 -1
- package/dist/core/field/field-definition.embedded.js +8 -3
- package/dist/core/field/field-definition.embedded.js.map +1 -1
- package/dist/core/field/validate-field-mutability.d.ts +22 -0
- package/dist/core/field/validate-field-mutability.d.ts.map +1 -0
- package/dist/core/field/validate-field-mutability.js +208 -0
- package/dist/core/field/validate-field-mutability.js.map +1 -0
- package/dist/core/identity/identity-constants.d.ts +12 -0
- package/dist/core/identity/identity-constants.d.ts.map +1 -1
- package/dist/core/identity/identity-constants.js +15 -0
- package/dist/core/identity/identity-constants.js.map +1 -1
- package/dist/core/identity/identity-definition.embedded.js +2 -2
- package/dist/core/identity/identity-definition.embedded.js.map +1 -1
- package/dist/core/index/index-definition.embedded.js +2 -2
- package/dist/core/index/index-definition.embedded.js.map +1 -1
- package/dist/core/object/object-definition.embedded.js +2 -2
- package/dist/core/object/object-definition.embedded.js.map +1 -1
- package/dist/core/requirement/meta-requirement.d.ts +1 -2
- package/dist/core/requirement/meta-requirement.d.ts.map +1 -1
- package/dist/core/requirement/meta-requirement.js +2 -6
- package/dist/core/requirement/meta-requirement.js.map +1 -1
- package/dist/core/requirement/requirement-constants.d.ts +3 -8
- package/dist/core/requirement/requirement-constants.d.ts.map +1 -1
- package/dist/core/requirement/requirement-constants.js +2 -9
- package/dist/core/requirement/requirement-constants.js.map +1 -1
- package/dist/core/requirement/requirement-definition.embedded.d.ts.map +1 -1
- package/dist/core/requirement/requirement-definition.embedded.js +8 -46
- package/dist/core/requirement/requirement-definition.embedded.js.map +1 -1
- package/dist/core/requirement/resolve-claim.d.ts +37 -0
- package/dist/core/requirement/resolve-claim.d.ts.map +1 -0
- package/dist/core/requirement/resolve-claim.js +83 -0
- package/dist/core/requirement/resolve-claim.js.map +1 -0
- package/dist/core/vocabulary-rewrite-yaml.d.ts +21 -0
- package/dist/core/vocabulary-rewrite-yaml.d.ts.map +1 -0
- package/dist/core/vocabulary-rewrite-yaml.js +245 -0
- package/dist/core/vocabulary-rewrite-yaml.js.map +1 -0
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +10 -10
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +37 -4
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/library/library-sources.d.ts +11 -0
- package/dist/library/library-sources.d.ts.map +1 -1
- package/dist/library/library-sources.js +13 -0
- package/dist/library/library-sources.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +14 -10
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +263 -45
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming-refs.d.ts.map +1 -1
- package/dist/naming-refs.js +2 -3
- package/dist/naming-refs.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +22 -5
- package/dist/parser-core.js.map +1 -1
- package/dist/persistence/origin/meta-origin.d.ts +0 -10
- package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
- package/dist/persistence/origin/meta-origin.js +1 -15
- package/dist/persistence/origin/meta-origin.js.map +1 -1
- package/dist/persistence/origin/origin-constants.d.ts +2 -4
- package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
- package/dist/persistence/origin/origin-constants.js +9 -7
- package/dist/persistence/origin/origin-constants.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.d.ts.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +5 -21
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/registry-manifest.d.ts +9 -4
- package/dist/registry-manifest.d.ts.map +1 -1
- package/dist/registry-manifest.js +9 -4
- package/dist/registry-manifest.js.map +1 -1
- package/dist/retired-vocabulary.d.ts +92 -0
- package/dist/retired-vocabulary.d.ts.map +1 -0
- package/dist/retired-vocabulary.js +204 -0
- package/dist/retired-vocabulary.js.map +1 -0
- package/dist/super-resolve.d.ts +33 -0
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +56 -1
- package/dist/super-resolve.js.map +1 -1
- package/dist/template/prompt-definition.embedded.js +1 -1
- package/dist/template/prompt-definition.embedded.js.map +1 -1
- package/dist/template/template-constants.d.ts +6 -0
- package/dist/template/template-constants.d.ts.map +1 -1
- package/dist/template/template-constants.js +24 -1
- package/dist/template/template-constants.js.map +1 -1
- package/dist/template/template-definition.embedded.d.ts.map +1 -1
- package/dist/template/template-definition.embedded.js +34 -21
- package/dist/template/template-definition.embedded.js.map +1 -1
- package/dist/vocabulary-rewrite.d.ts +33 -0
- package/dist/vocabulary-rewrite.d.ts.map +1 -0
- package/dist/vocabulary-rewrite.js +314 -0
- package/dist/vocabulary-rewrite.js.map +1 -0
- package/package.json +11 -1
- package/src/attr-schema-validate.ts +17 -2
- package/src/core/field/field-constants.ts +24 -5
- package/src/core/field/field-definition.embedded.ts +8 -3
- package/src/core/field/validate-field-mutability.ts +259 -0
- package/src/core/identity/identity-constants.ts +16 -0
- package/src/core/identity/identity-definition.embedded.ts +2 -2
- package/src/core/index/index-definition.embedded.ts +2 -2
- package/src/core/object/object-definition.embedded.ts +2 -2
- package/src/core/requirement/meta-requirement.ts +1 -7
- package/src/core/requirement/requirement-constants.ts +2 -9
- package/src/core/requirement/requirement-definition.embedded.ts +8 -46
- package/src/core/requirement/resolve-claim.ts +98 -0
- package/src/core/vocabulary-rewrite-yaml.ts +267 -0
- package/src/core-types.ts +9 -10
- package/src/errors.ts +37 -4
- package/src/index.ts +29 -1
- package/src/library/library-sources.ts +14 -0
- package/src/loader/meta-data-loader.ts +15 -10
- package/src/loader/validation-passes.ts +323 -50
- package/src/naming-refs.ts +1 -3
- package/src/parser-core.ts +27 -5
- package/src/persistence/origin/meta-origin.ts +0 -16
- package/src/persistence/origin/origin-constants.ts +9 -8
- package/src/persistence/origin/origin-definition.embedded.ts +5 -21
- package/src/registry-manifest.ts +9 -4
- package/src/retired-vocabulary.ts +280 -0
- package/src/super-resolve.ts +59 -1
- package/src/template/prompt-definition.embedded.ts +1 -1
- package/src/template/template-constants.ts +27 -1
- package/src/template/template-definition.embedded.ts +34 -21
- package/src/vocabulary-rewrite.ts +350 -0
- package/dist/core/field/validate-field-readonly.d.ts +0 -9
- package/dist/core/field/validate-field-readonly.d.ts.map +0 -1
- package/dist/core/field/validate-field-readonly.js +0 -128
- package/dist/core/field/validate-field-readonly.js.map +0 -1
- package/src/core/field/validate-field-readonly.ts +0 -154
|
@@ -15,13 +15,14 @@ import { PACKAGE_SEPARATOR, CHILD_REF_SEPARATOR } from "../shared/structural.js"
|
|
|
15
15
|
import { resolvedSource } from "../source.js";
|
|
16
16
|
import { TYPE_OBJECT, TYPE_FIELD, TYPE_LAYOUT, TYPE_IDENTITY, TYPE_ORIGIN, TYPE_RELATIONSHIP, TYPE_SOURCE, TYPE_TEMPLATE, TYPE_INDEX, } from "../shared/base-types.js";
|
|
17
17
|
import { INDEX_SUBTYPE_LOOKUP, INDEX_ATTR_FIELDS, } from "../core/index/index-constants.js";
|
|
18
|
+
import { IDENTITY_SUBTYPE_SECONDARY } from "../core/identity/identity-constants.js";
|
|
18
19
|
import { MetaIndex } from "../core/index/meta-index.js";
|
|
19
20
|
import { TEMPLATE_ATTR_PAYLOAD_REF, TEMPLATE_ATTR_RESPONSE_REF, TEMPLATE_ATTR_REQUIRED_SLOTS, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_KIND, TEMPLATE_KIND_EMAIL, TEMPLATE_ATTR_SUBJECT_REF, TEMPLATE_ATTR_HTML_BODY_REF, TEMPLATE_SUBTYPE_OUTPUT, TEMPLATE_SUBTYPE_PROMPT, } from "../template/template-constants.js";
|
|
20
21
|
import { OBJECT_SUBTYPE_ENTITY, OBJECT_SUBTYPE_VALUE, OBJECT_SUBTYPE_PROJECTION, OBJECT_PROJECTION_ATTR_FILTER, } from "../core/object/object-constants.js";
|
|
21
22
|
import { MetaSource } from "../persistence/source/meta-source.js";
|
|
22
23
|
import { LAYOUT_SUBTYPE_DATA_GRID, LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD, LAYOUT_DATA_GRID_ATTR_FILTER, } from "../presentation/layout/layout-constants.js";
|
|
23
|
-
import { FIELD_ATTR_FILTERABLE, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_STORAGE, STORAGE_FLATTENED, FIELD_ATTR_DEFAULT, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_OBJECT, FIELD_SUBTYPE_MAP, FIELD_ATTR_REQUIRED, FIELD_ATTR_VALUE_TYPE, FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_DATE, FIELD_SUBTYPE_TIME, FIELD_SUBTYPE_TIMESTAMP, FIELD_SUBTYPE_UUID, } from "../core/field/field-constants.js";
|
|
24
|
-
import { FIELD_ATTR_DB_INDEXED } from "../persistence/db/db-constants.js";
|
|
24
|
+
import { FIELD_ATTR_FILTERABLE, FIELD_ATTR_SORTABLE, FIELD_ATTR_OBJECT_REF, FIELD_ATTR_STORAGE, STORAGE_FLATTENED, FIELD_ATTR_DEFAULT, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT, FIELD_SUBTYPE_DECIMAL, FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_ENUM, FIELD_SUBTYPE_OBJECT, FIELD_SUBTYPE_MAP, FIELD_ATTR_REQUIRED, FIELD_ATTR_VALUE_TYPE, FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_DATE, FIELD_SUBTYPE_TIME, FIELD_SUBTYPE_TIMESTAMP, FIELD_SUBTYPE_UUID, } from "../core/field/field-constants.js";
|
|
25
|
+
import { FIELD_ATTR_DB_INDEXED, IDENTITY_ATTR_EXPR } from "../persistence/db/db-constants.js";
|
|
25
26
|
import { IDENTITY_ATTR_FIELDS, IDENTITY_SUBTYPE_REFERENCE, IDENTITY_REFERENCE_ATTR_REFERENCES, } from "../core/identity/identity-constants.js";
|
|
26
27
|
import { ORIGIN_SUBTYPE_PASSTHROUGH, ORIGIN_SUBTYPE_AGGREGATE, ORIGIN_SUBTYPE_COMPUTED, ORIGIN_SUBTYPE_FIRST, ORIGIN_PASSTHROUGH_ATTR_FROM, ORIGIN_PASSTHROUGH_ATTR_VIA, ORIGIN_PASSTHROUGH_ATTR_CONVERT, ORIGIN_AGGREGATE_ATTR_AGG, ORIGIN_AGGREGATE_ATTR_OF, ORIGIN_AGGREGATE_ATTR_VIA, ORIGIN_AGGREGATE_ATTR_FILTER, ORIGIN_ATTR_DISTINCT, ORIGIN_ATTR_ORDER_BY, ORIGIN_COMPUTED_ATTR_EXPR, ORIGIN_FIRST_ATTR_OF, ORIGIN_FIRST_ATTR_VIA, ORIGIN_FIRST_ATTR_FILTER, AGG_ANY, AGG_ALL, AGG_COLLECT, ASSEMBLY_ORIGIN_SUBTYPES, } from "../persistence/origin/origin-constants.js";
|
|
27
28
|
import { inferExprType, validateExprNode, } from "../core/attr/meta-attr-expression.js";
|
|
@@ -287,6 +288,19 @@ export function validateFilterableHasSupportedOps(root) {
|
|
|
287
288
|
// ADR-0039: resolving — a concrete field may inherit @filterable via extends.
|
|
288
289
|
if (field.attr(FIELD_ATTR_FILTERABLE) !== true)
|
|
289
290
|
continue;
|
|
291
|
+
// #335 Half B — an ARRAY field has no operator band either. Every FR-009
|
|
292
|
+
// operator (eq/ne/gt/gte/lt/lte/in/like/isNull) is a scalar comparison;
|
|
293
|
+
// none applies to a collection column. The allowlist template does not
|
|
294
|
+
// consult isArray and falls through to the "string" band, so this
|
|
295
|
+
// previously emitted a `like` rule against a text[] column — SQL that
|
|
296
|
+
// cannot execute. Same reason as the subtype check below, so same code.
|
|
297
|
+
// ADR-0039: resolvedIsArray(), never the own `isArray` flag.
|
|
298
|
+
if (field.resolvedIsArray()) {
|
|
299
|
+
errors.push(new ParseError(`Field "${obj.name}.${field.name}" has @filterable: true but is an array ` +
|
|
300
|
+
`(isArray: true). No filter operator applies to a collection column. ` +
|
|
301
|
+
`Remove @filterable from this field.`, { code: "ERR_FILTERABLE_UNSUPPORTED_SUBTYPE", source: field.source }));
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
290
304
|
if (opsForSubType(field.subType).length > 0)
|
|
291
305
|
continue;
|
|
292
306
|
errors.push(new ParseError(`Field "${obj.name}.${field.name}" has @filterable: true but its subtype ` +
|
|
@@ -297,6 +311,35 @@ export function validateFilterableHasSupportedOps(root) {
|
|
|
297
311
|
return errors;
|
|
298
312
|
}
|
|
299
313
|
// ---------------------------------------------------------------------------
|
|
314
|
+
// @sortable on a subtype or shape that cannot be ordered (#335 Half B)
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// @sortable defaults FROM @filterable, so it is independently set only when
|
|
317
|
+
// explicit — and nothing validated it, while @filterable has had a hard error
|
|
318
|
+
// since SP-H Unit9. A @sortable JSON or array column emits a sort entry over a
|
|
319
|
+
// column no dialect can ORDER BY meaningfully. → ERR_SORTABLE_UNSUPPORTED_SUBTYPE.
|
|
320
|
+
export function validateSortableHasSupportedSubtype(root) {
|
|
321
|
+
const errors = [];
|
|
322
|
+
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
323
|
+
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
324
|
+
// children() — inherited @sortable fields (via extends:/super:) are visible.
|
|
325
|
+
for (const field of obj.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
326
|
+
// ADR-0039: resolving — a concrete field may inherit @sortable via extends.
|
|
327
|
+
if (field.attr(FIELD_ATTR_SORTABLE) !== true)
|
|
328
|
+
continue;
|
|
329
|
+
// ADR-0039: resolvedIsArray(), never the own `isArray` flag.
|
|
330
|
+
const isArray = field.resolvedIsArray();
|
|
331
|
+
if (!isArray && opsForSubType(field.subType).length > 0)
|
|
332
|
+
continue;
|
|
333
|
+
errors.push(new ParseError(`Field "${obj.name}.${field.name}" has @sortable: true but ` +
|
|
334
|
+
(isArray
|
|
335
|
+
? `is an array (isArray: true) — a collection column has no ordering.`
|
|
336
|
+
: `its subtype "${field.subType}" cannot be ordered.`) +
|
|
337
|
+
` Remove @sortable from this field.`, { code: "ERR_SORTABLE_UNSUPPORTED_SUBTYPE", source: field.source }));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return errors;
|
|
341
|
+
}
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
300
343
|
// Origin path validation
|
|
301
344
|
//
|
|
302
345
|
// Walks every projection's fields, finds `origin` (TYPE_ORIGIN) children,
|
|
@@ -393,9 +436,13 @@ function _validateFromPath(fromAttr, root, projection, fieldName, originSource,
|
|
|
393
436
|
return { entity: sourceObj, field: sourceField };
|
|
394
437
|
}
|
|
395
438
|
/**
|
|
396
|
-
* Validate an explicit `@via` "Entity.rel[.rel...]" path.
|
|
397
|
-
* relationship hop nodes (in path order)
|
|
398
|
-
* cardinality checks over them —
|
|
439
|
+
* Validate an explicit `@via` "Entity.rel[.rel...]" path. On full success
|
|
440
|
+
* returns the walked relationship hop nodes (in path order) — FR-024 B5 runs
|
|
441
|
+
* the cardinality checks over them — together with the terminal entity node
|
|
442
|
+
* (#335: a whole-object `@agg:collect` has no `@of` entity, so `@orderBy` keys
|
|
443
|
+
* and value-object members resolve against the terminal instead). Returns
|
|
444
|
+
* undefined when any error was pushed, so `terminal` is defined exactly when
|
|
445
|
+
* `hops` is.
|
|
399
446
|
*/
|
|
400
447
|
function _validateViaPath(viaAttr, root, projection, fieldName, originSource, errors) {
|
|
401
448
|
const projectionName = projection.name;
|
|
@@ -470,7 +517,8 @@ function _validateViaPath(viaAttr, root, projection, fieldName, originSource, er
|
|
|
470
517
|
hops.push(rel);
|
|
471
518
|
currentObj = nextObj;
|
|
472
519
|
}
|
|
473
|
-
|
|
520
|
+
// currentObj is the terminal: every earlier exit returned undefined.
|
|
521
|
+
return { hops, terminal: currentObj };
|
|
474
522
|
}
|
|
475
523
|
// ---------------------------------------------------------------------------
|
|
476
524
|
// FR-024 B5 — base-entity derivation, single-hop-unique @via inference, and
|
|
@@ -663,6 +711,46 @@ function _checkAggregateCardinality(hops, obj, fieldName, originSource, errors)
|
|
|
663
711
|
`aggregating over a to-one path — you meant passthrough (ADR-0029).`, { code: "ERR_ORIGIN_CARDINALITY", source: originSource }));
|
|
664
712
|
}
|
|
665
713
|
}
|
|
714
|
+
/**
|
|
715
|
+
* #335 — a whole-object `@agg:collect` projects EXACTLY the declared value
|
|
716
|
+
* object's members, each matched by NAME against the `@via` terminal entity.
|
|
717
|
+
*
|
|
718
|
+
* Two rules, both fail-closed:
|
|
719
|
+
* - a member with no matching field on the terminal is unresolvable. Failing
|
|
720
|
+
* OPEN here is how #270 turned a curated value object into the full entity,
|
|
721
|
+
* invisible in a diff because the metadata still read as curated.
|
|
722
|
+
* - a matched member must agree on BOTH type axes (#185 type-preserving
|
|
723
|
+
* doctrine), so a scalar member cannot bind an array field or vice versa.
|
|
724
|
+
*
|
|
725
|
+
* Both refusals carry a whole-object-specific code — ERR_COLLECT_MEMBER_UNRESOLVED
|
|
726
|
+
* for the unmatched member, ERR_COLLECT_WHOLE_OBJECT for the type disagreement.
|
|
727
|
+
* The latter is deliberately NOT the scalar arm's ERR_INVALID_ORIGIN: a loader
|
|
728
|
+
* that still requires @of rejects this metadata with ERR_INVALID_ORIGIN too, so
|
|
729
|
+
* sharing the code would make a corpus fixture pass on a port that implements
|
|
730
|
+
* nothing (the corpus compares only code + source, never message text).
|
|
731
|
+
*/
|
|
732
|
+
function _checkCollectMembers(refTarget, terminal, obj, field, src, errors) {
|
|
733
|
+
// ADR-0039: resolving — a value object may inherit members via extends, and
|
|
734
|
+
// the terminal entity may inherit fields; own-only would silently skip
|
|
735
|
+
// inherited members, which is exactly the #270 bug class this guards.
|
|
736
|
+
const terminalFields = terminal.children().filter((c) => c.type === TYPE_FIELD);
|
|
737
|
+
for (const member of refTarget.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
738
|
+
const match = terminalFields.find((f) => f.name === member.name);
|
|
739
|
+
if (match === undefined) {
|
|
740
|
+
errors.push(new ParseError(`origin.aggregate @agg:collect on ${obj.name}.${field.name}: value-object member ` +
|
|
741
|
+
`'${member.name}' has no matching field on '${terminal.name}' — a whole-object ` +
|
|
742
|
+
`rollup projects exactly the declared members.`, { code: "ERR_COLLECT_MEMBER_UNRESOLVED", source: src }));
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
const memberLabel = _typeLabel(member);
|
|
746
|
+
const matchLabel = _typeLabel(match);
|
|
747
|
+
if (memberLabel !== matchLabel) {
|
|
748
|
+
errors.push(new ParseError(`origin.aggregate @agg:collect on ${obj.name}.${field.name}: value-object member ` +
|
|
749
|
+
`'${member.name}' is ${memberLabel} but '${terminal.name}.${match.name}' ` +
|
|
750
|
+
`is ${matchLabel} — a whole-object rollup preserves each member's type.`, { code: "ERR_COLLECT_WHOLE_OBJECT", source: src }));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
666
754
|
/**
|
|
667
755
|
* FR-024 B6 (spec §4; ADR-0029 decision 7) — extends/origin agreement.
|
|
668
756
|
*
|
|
@@ -718,14 +806,21 @@ function _checkExtendsOriginAgreement(field, fromField, fromAttr, obj, originSou
|
|
|
718
806
|
* FR-015 stored-proc parameter refs the retired ERR_PARAMETER_REF_PASSTHROUGH_
|
|
719
807
|
* TYPE_MISMATCH used to cover).
|
|
720
808
|
*/
|
|
809
|
+
/**
|
|
810
|
+
* Both type axes in one comparable token. Subtype names never contain "[]", so
|
|
811
|
+
* equal labels ⇔ same subType AND same array-ness. Nullability is deliberately
|
|
812
|
+
* NOT judged — an outer-join view legitimately widens NOT NULL.
|
|
813
|
+
* ADR-0039: resolvedIsArray(), never the own `isArray` flag — a field may
|
|
814
|
+
* inherit its array-ness via extends.
|
|
815
|
+
*/
|
|
816
|
+
function _typeLabel(field) {
|
|
817
|
+
return `field.${field.subType}${field.resolvedIsArray() ? "[]" : ""}`;
|
|
818
|
+
}
|
|
721
819
|
function _checkPassthroughType(field, fromField, fromAttr, convert, obj, originSource, errors) {
|
|
722
820
|
if (convert)
|
|
723
821
|
return; // deliberate type change acknowledged
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
// deliberately not judged — an outer-join view legitimately widens NOT NULL).
|
|
727
|
-
const declared = `field.${field.subType}${field.resolvedIsArray() ? "[]" : ""}`;
|
|
728
|
-
const source = `field.${fromField.subType}${fromField.resolvedIsArray() ? "[]" : ""}`;
|
|
822
|
+
const declared = _typeLabel(field);
|
|
823
|
+
const source = _typeLabel(fromField);
|
|
729
824
|
if (declared === source)
|
|
730
825
|
return;
|
|
731
826
|
errors.push(new ParseError(`origin.passthrough on ${obj.name}.${field.name}: field is ${declared} but its @from source ` +
|
|
@@ -742,8 +837,12 @@ function _checkPassthroughType(field, fromField, fromAttr, convert, obj, originS
|
|
|
742
837
|
* and carries no vocabulary. Shared by `@agg:collect` (element order) and
|
|
743
838
|
* `origin.first` (row selection). A missing related entity means a prior error
|
|
744
839
|
* already fired — skip silently.
|
|
840
|
+
*
|
|
841
|
+
* `code` lets the whole-object `@agg:collect` arm report ERR_COLLECT_WHOLE_OBJECT
|
|
842
|
+
* instead; it defaults to ERR_INVALID_ORIGIN so the scalar `@of` and `origin.first`
|
|
843
|
+
* call sites keep their existing envelope byte-for-byte.
|
|
745
844
|
*/
|
|
746
|
-
function _validateOrderByKeys(orderBy, relatedEntity, obj, fieldName, label, originSource, errors) {
|
|
845
|
+
function _validateOrderByKeys(orderBy, relatedEntity, obj, fieldName, label, originSource, errors, code = "ERR_INVALID_ORIGIN") {
|
|
747
846
|
if (!Array.isArray(orderBy) || relatedEntity === undefined)
|
|
748
847
|
return;
|
|
749
848
|
for (const raw of orderBy) {
|
|
@@ -755,10 +854,10 @@ function _validateOrderByKeys(orderBy, relatedEntity, obj, fieldName, label, ori
|
|
|
755
854
|
// ADR-0039: resolving — an ordering key may target an inherited field.
|
|
756
855
|
const target = relatedEntity.children().find((f) => f.type === TYPE_FIELD && f.name === key);
|
|
757
856
|
if (target === undefined) {
|
|
758
|
-
errors.push(new ParseError(`${label} on ${obj.name}.${fieldName}: @orderBy key "${raw}" — no such field "${key}" on ${relatedEntity.name}.`, { code
|
|
857
|
+
errors.push(new ParseError(`${label} on ${obj.name}.${fieldName}: @orderBy key "${raw}" — no such field "${key}" on ${relatedEntity.name}.`, { code, source: originSource }));
|
|
759
858
|
}
|
|
760
859
|
else if (dir !== undefined && !SORT_ORDER_VALUES.includes(dir)) {
|
|
761
|
-
errors.push(new ParseError(`${label} on ${obj.name}.${fieldName}: @orderBy key "${raw}" — direction must be one of ${SORT_ORDER_VALUES.join("|")}.`, { code
|
|
860
|
+
errors.push(new ParseError(`${label} on ${obj.name}.${fieldName}: @orderBy key "${raw}" — direction must be one of ${SORT_ORDER_VALUES.join("|")}.`, { code, source: originSource }));
|
|
762
861
|
}
|
|
763
862
|
}
|
|
764
863
|
}
|
|
@@ -778,8 +877,8 @@ export function validateOriginPaths(root) {
|
|
|
778
877
|
for (const field of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
779
878
|
for (const origin of field.ownChildren().filter((c) => c.type === TYPE_ORIGIN)) {
|
|
780
879
|
// #210 — assembly origins live on projections. A value-hosted field may
|
|
781
|
-
// not carry origin.aggregate / origin.computed / origin.
|
|
782
|
-
//
|
|
880
|
+
// not carry origin.aggregate / origin.computed / origin.first:
|
|
881
|
+
// a value is constructed — by a caller or by embedding —
|
|
783
882
|
// never assembled from a backing store. origin.passthrough STAYS legal
|
|
784
883
|
// on a value (FR-015 parameter lineage; the B5 exemption below).
|
|
785
884
|
if (isValueHost &&
|
|
@@ -813,9 +912,9 @@ export function validateOriginPaths(root) {
|
|
|
813
912
|
// ADR-0039: own — origin.* never inherits (ADR-0029).
|
|
814
913
|
const via = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_VIA);
|
|
815
914
|
if (typeof via === "string" && via !== "") {
|
|
816
|
-
const
|
|
817
|
-
if (
|
|
818
|
-
_checkPassthroughCardinality(hops, obj, field.name, origin.source, errors);
|
|
915
|
+
const walked = _validateViaPath(via, root, obj, field.name, origin.source, errors);
|
|
916
|
+
if (walked !== undefined) {
|
|
917
|
+
_checkPassthroughCardinality(walked.hops, obj, field.name, origin.source, errors);
|
|
819
918
|
}
|
|
820
919
|
}
|
|
821
920
|
else if (fromTarget !== undefined && !isValueHost) {
|
|
@@ -880,15 +979,78 @@ export function validateOriginPaths(root) {
|
|
|
880
979
|
errors.push(new ParseError(`origin.aggregate @agg:${String(agg)} on ${obj.name}.${field.name}: requires an explicit @via (a quantifier has no @of to infer the path from).`, { code: "ERR_INVALID_ORIGIN", source: src }));
|
|
881
980
|
}
|
|
882
981
|
else {
|
|
883
|
-
const
|
|
884
|
-
if (
|
|
885
|
-
_checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
982
|
+
const walked = _validateViaPath(via, root, obj, field.name, src, errors);
|
|
983
|
+
if (walked !== undefined)
|
|
984
|
+
_checkAggregateCardinality(walked.hops, obj, field.name, src, errors);
|
|
886
985
|
}
|
|
887
986
|
continue;
|
|
888
987
|
}
|
|
889
|
-
// --- count/sum/avg/min/max
|
|
988
|
+
// --- @of: REQUIRED for count/sum/avg/min/max; OPTIONAL for collect ---
|
|
989
|
+
// #335 — an @of-absent collect is a WHOLE-OBJECT rollup: collect the
|
|
990
|
+
// related rows as an array of the field's declared @objectRef value
|
|
991
|
+
// object rather than an array of one scalar column.
|
|
890
992
|
if (!ofPresent) {
|
|
891
|
-
|
|
993
|
+
if (!isCollect) {
|
|
994
|
+
errors.push(new ParseError(`origin.aggregate on ${obj.name}.${field.name}: missing @of.`, { code: "ERR_INVALID_ORIGIN", source: src }));
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
// Whole-object rollup. The carrying field must be a field.object
|
|
998
|
+
// naming a value object, and @via must be explicit (there is no @of
|
|
999
|
+
// entity to infer the single-hop relation from).
|
|
1000
|
+
// ADR-0039: resolving — @objectRef may be inherited via extends.
|
|
1001
|
+
const objectRef = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
1002
|
+
if (field.subType !== FIELD_SUBTYPE_OBJECT || typeof objectRef !== "string" || objectRef === "") {
|
|
1003
|
+
errors.push(new ParseError(`origin.aggregate @agg:collect on ${obj.name}.${field.name}: @of is omitted, so this is a ` +
|
|
1004
|
+
`whole-object rollup — the carrying field must be a field.object declaring @objectRef ` +
|
|
1005
|
+
`(add @of to collect a single column instead).`, { code: "ERR_COLLECT_WHOLE_OBJECT", source: src }));
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
// #210's value-only rule is PAYLOAD-scoped and never reaches a
|
|
1009
|
+
// projection-hosted field, so this branch enforces it itself.
|
|
1010
|
+
// Without it an @objectRef to an entity silently rolls up the FULL
|
|
1011
|
+
// entity — the #270 shape, this time baked into DDL.
|
|
1012
|
+
// ADR-0042 — a bare @objectRef resolves in the DECLARING owner's
|
|
1013
|
+
// package (an inherited field resolves in the package that
|
|
1014
|
+
// declared it) — same rule _checkNestedPayloadRefsValueOnly uses.
|
|
1015
|
+
const refOwner = field.parent ?? obj;
|
|
1016
|
+
const refPkg = refOwner.package ?? refOwner.fileDefaultPackage ?? "";
|
|
1017
|
+
const refTarget = resolveObjectRef(root, objectRef, refPkg).node;
|
|
1018
|
+
if (refTarget !== undefined && refTarget.subType !== OBJECT_SUBTYPE_VALUE) {
|
|
1019
|
+
errors.push(new ParseError(`origin.aggregate @agg:collect on ${obj.name}.${field.name}: @objectRef '${objectRef}' ` +
|
|
1020
|
+
`resolves to ${TYPE_OBJECT}.${refTarget.subType} — a whole-object rollup must target an ` +
|
|
1021
|
+
`object.value (#210, ADR-0028).`, { code: "ERR_SUBTYPE_RULE_VIOLATION", source: src }));
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
// ADR-0039: own — origin.* never inherits (ADR-0029).
|
|
1025
|
+
const viaAttr = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA);
|
|
1026
|
+
if (typeof viaAttr !== "string" || viaAttr === "") {
|
|
1027
|
+
errors.push(new ParseError(`origin.aggregate @agg:collect on ${obj.name}.${field.name}: @via is required on a ` +
|
|
1028
|
+
`whole-object rollup — there is no @of entity to infer the relationship from.`, { code: "ERR_COLLECT_WHOLE_OBJECT", source: src }));
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
// @distinct is refused on the object form. It is NOT an engine limit
|
|
1032
|
+
// (both engines dedupe JSON objects); it is a guaranteed no-op
|
|
1033
|
+
// whenever the value object carries the entity's primary key, which
|
|
1034
|
+
// is the common case, and a silent no-op is worse than a refusal.
|
|
1035
|
+
if (hasDistinct) {
|
|
1036
|
+
errors.push(new ParseError(`origin.aggregate @agg:collect on ${obj.name}.${field.name}: @distinct is not supported on a ` +
|
|
1037
|
+
`whole-object rollup (it is a no-op whenever the value object carries the primary key).`, { code: "ERR_COLLECT_WHOLE_OBJECT", source: src }));
|
|
1038
|
+
continue;
|
|
1039
|
+
}
|
|
1040
|
+
// One walk yields both the hops (cardinality) and the terminal
|
|
1041
|
+
// entity (@orderBy keys, member resolution). An invalid @via
|
|
1042
|
+
// (e.g. single-segment "A") returns undefined having already
|
|
1043
|
+
// pushed its own error, so everything downstream is skipped and
|
|
1044
|
+
// no second, misleadingly-scoped error is emitted.
|
|
1045
|
+
const via = _validateViaPath(viaAttr, root, obj, field.name, src, errors);
|
|
1046
|
+
if (via !== undefined) {
|
|
1047
|
+
_checkAggregateCardinality(via.hops, obj, field.name, src, errors);
|
|
1048
|
+
// @orderBy keys resolve against the @via TERMINAL entity, not @of.
|
|
1049
|
+
_validateOrderByKeys(orderBy, via.terminal, obj, field.name, "origin.aggregate @agg:collect", src, errors, "ERR_COLLECT_WHOLE_OBJECT");
|
|
1050
|
+
if (refTarget !== undefined) {
|
|
1051
|
+
_checkCollectMembers(refTarget, via.terminal, obj, field, src, errors);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
892
1054
|
continue;
|
|
893
1055
|
}
|
|
894
1056
|
// NOTE (FR-024 B6): NO extends/origin agreement check on aggregates —
|
|
@@ -907,9 +1069,9 @@ export function validateOriginPaths(root) {
|
|
|
907
1069
|
// ADR-0039: own — origin.* never inherits (ADR-0029).
|
|
908
1070
|
const via = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA);
|
|
909
1071
|
if (typeof via === "string" && via !== "") {
|
|
910
|
-
const
|
|
911
|
-
if (
|
|
912
|
-
_checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
1072
|
+
const walked = _validateViaPath(via, root, obj, field.name, src, errors);
|
|
1073
|
+
if (walked !== undefined) {
|
|
1074
|
+
_checkAggregateCardinality(walked.hops, obj, field.name, src, errors);
|
|
913
1075
|
}
|
|
914
1076
|
continue;
|
|
915
1077
|
}
|
|
@@ -987,9 +1149,9 @@ export function validateOriginPaths(root) {
|
|
|
987
1149
|
// @via — explicit (validated + cardinality) or single-hop-unique inferred.
|
|
988
1150
|
const via = origin.ownAttr(ORIGIN_FIRST_ATTR_VIA);
|
|
989
1151
|
if (typeof via === "string" && via !== "") {
|
|
990
|
-
const
|
|
991
|
-
if (
|
|
992
|
-
_checkAggregateCardinality(hops, obj, field.name, src, errors);
|
|
1152
|
+
const walked = _validateViaPath(via, root, obj, field.name, src, errors);
|
|
1153
|
+
if (walked !== undefined)
|
|
1154
|
+
_checkAggregateCardinality(walked.hops, obj, field.name, src, errors);
|
|
993
1155
|
}
|
|
994
1156
|
else if (ofTarget !== undefined) {
|
|
995
1157
|
// (A value host never reaches here — the #210 assembly-origin
|
|
@@ -1402,12 +1564,32 @@ export function validateRelationships(root) {
|
|
|
1402
1564
|
// NOTE: identity.reference @references resolution moved to the validation registry
|
|
1403
1565
|
// (defaultValidationRegistry → a declarative reference descriptor with dottedFieldPath).
|
|
1404
1566
|
// ---------------------------------------------------------------------------
|
|
1405
|
-
// index.lookup
|
|
1567
|
+
// Index-key resolution for index.lookup AND identity.secondary (#342)
|
|
1406
1568
|
//
|
|
1407
|
-
//
|
|
1408
|
-
//
|
|
1409
|
-
//
|
|
1410
|
-
//
|
|
1569
|
+
// An index declares its key EXACTLY ONE of two ways: plain columns (@fields) or
|
|
1570
|
+
// a key expression (@expr, e.g. `lower(email)` / `(payload->>'device_id')`).
|
|
1571
|
+
// The registry has always said so — @expr is described as "Used INSTEAD of
|
|
1572
|
+
// @fields" — and `migrate-ts` has always implemented it that way
|
|
1573
|
+
// (`columns: expr ? [] : cols`, expected-schema.ts). Only the LOADER disagreed,
|
|
1574
|
+
// requiring @fields unconditionally, which made an expression index
|
|
1575
|
+
// unreachable: omitting @fields failed to load, and the one spelling that DID
|
|
1576
|
+
// load (@fields AND @expr) had its @fields silently discarded by the engine.
|
|
1577
|
+
//
|
|
1578
|
+
// So the two rules below are one rule — the key is @fields XOR @expr:
|
|
1579
|
+
// - NEITHER: nothing declares the key.
|
|
1580
|
+
// - BOTH: contradictory, and previously half-honored. Rejected rather than
|
|
1581
|
+
// given a precedence rule, because an accepted-but-half-ignored declaration
|
|
1582
|
+
// is exactly the silent-wrong-output the sealed strict registry exists to
|
|
1583
|
+
// prevent (cf. ERR_SQL_BODY_WITH_UNMANAGED — @sql vs @unmanaged is the same
|
|
1584
|
+
// "two mutually exclusive non-default states of one axis" shape).
|
|
1585
|
+
//
|
|
1586
|
+
// Applies to identity.secondary too: per ADR-0040 uniqueness lives in the TYPE,
|
|
1587
|
+
// so identity.secondary IS a unique index and keys itself identically. Both
|
|
1588
|
+
// carry @expr from the same db provider, and migrate-ts branches on @expr for
|
|
1589
|
+
// both — the loader was the only tier treating them differently.
|
|
1590
|
+
//
|
|
1591
|
+
// ADR-0039: children() / MetaIndex.fields() — never own* — so a field inherited
|
|
1592
|
+
// via extends still resolves.
|
|
1411
1593
|
// ---------------------------------------------------------------------------
|
|
1412
1594
|
export function validateIndexLookupFields(root) {
|
|
1413
1595
|
const errors = [];
|
|
@@ -1415,22 +1597,58 @@ export function validateIndexLookupFields(root) {
|
|
|
1415
1597
|
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
1416
1598
|
// Effective (resolved) field names — includes inherited fields via extends.
|
|
1417
1599
|
const effectiveFieldNames = new Set(obj.children().filter((c) => c.type === TYPE_FIELD).map((f) => f.name));
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
//
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1600
|
+
const keyed = obj.children().filter((c) => (c.type === TYPE_INDEX && c.subType === INDEX_SUBTYPE_LOOKUP) ||
|
|
1601
|
+
(c.type === TYPE_IDENTITY && c.subType === IDENTITY_SUBTYPE_SECONDARY));
|
|
1602
|
+
for (const node of keyed) {
|
|
1603
|
+
const label = `${node.type}.${node.subType}`;
|
|
1604
|
+
// PRESENCE vs CONTENT are two different questions here, and conflating them
|
|
1605
|
+
// is a bug in both directions:
|
|
1606
|
+
//
|
|
1607
|
+
// - The CONTRADICTION check needs PRESENCE. `@fields: []` alongside @expr
|
|
1608
|
+
// is still a declaration of both, and keying it on non-emptiness let the
|
|
1609
|
+
// total-discard spelling load clean while `@fields: ["x"]` + @expr was
|
|
1610
|
+
// refused — the rule missing exactly the case it exists to catch.
|
|
1611
|
+
// - The KEY-RESOLUTION check needs normalized CONTENT, via the guarded
|
|
1612
|
+
// accessor. Reading the raw attr with a cast meant a scalar `@fields: 5`
|
|
1613
|
+
// threw an uncaught TypeError out of load() in TS while the other three
|
|
1614
|
+
// ports reported a clean error.
|
|
1615
|
+
//
|
|
1616
|
+
// The guarded accessor is the fix for the second and the OBSTACLE for the
|
|
1617
|
+
// first — it collapses absent, scalar and explicit `[]` to the same `[]` —
|
|
1618
|
+
// so the two questions are asked separately and never routed through one
|
|
1619
|
+
// predicate. ADR-0039: both reads resolve through extends.
|
|
1620
|
+
const hasFieldsAttr = node.attr(IDENTITY_ATTR_FIELDS) !== undefined;
|
|
1621
|
+
// MetaIndex.fields() / MetaIdentity.fields are the same guarded read
|
|
1622
|
+
// (`Array.isArray(f) ? f : []`); never re-hand-roll it.
|
|
1623
|
+
const fields = node instanceof MetaIndex ? node.fields() : node.fields;
|
|
1624
|
+
const exprRaw = node.attr(IDENTITY_ATTR_EXPR);
|
|
1625
|
+
const hasExpr = typeof exprRaw === "string" && exprRaw.trim().length > 0;
|
|
1626
|
+
// Rule 1a: exactly one of @fields / @expr may be DECLARED.
|
|
1627
|
+
if (hasFieldsAttr && hasExpr) {
|
|
1628
|
+
errors.push(new ParseError(`${label} "${node.name}" on "${obj.name}" declares BOTH ` +
|
|
1629
|
+
`@${INDEX_ATTR_FIELDS} and @${IDENTITY_ATTR_EXPR}; they are the two ` +
|
|
1630
|
+
`mutually exclusive ways to key an index. @${IDENTITY_ATTR_EXPR} is used ` +
|
|
1631
|
+
`INSTEAD of @${INDEX_ATTR_FIELDS} — drop one. ` +
|
|
1632
|
+
`(Declaring both previously loaded but silently discarded ` +
|
|
1633
|
+
`@${INDEX_ATTR_FIELDS}.)`, { code: "ERR_INVALID_INDEX", source: node.source }));
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
// Rule 1b: whichever is declared must actually supply a key.
|
|
1637
|
+
if (fields.length === 0 && !hasExpr) {
|
|
1638
|
+
errors.push(new ParseError(`${label} "${node.name}" on "${obj.name}" declares no key: ` +
|
|
1639
|
+
`it must have @${INDEX_ATTR_FIELDS} (one or more columns) or ` +
|
|
1640
|
+
`@${IDENTITY_ATTR_EXPR} (a key expression)`, { code: "ERR_INVALID_INDEX", source: node.source }));
|
|
1426
1641
|
continue;
|
|
1427
1642
|
}
|
|
1428
1643
|
// Rule 2: every named field must resolve against the entity's effective field set.
|
|
1644
|
+
// An expression index has no @fields to resolve — @expr is raw SQL over the
|
|
1645
|
+
// physical columns, deliberately not parsed here (ADR-0023 keeps the grammar
|
|
1646
|
+
// closed only where the loader owns it).
|
|
1429
1647
|
for (const fieldName of fields) {
|
|
1430
1648
|
if (!effectiveFieldNames.has(fieldName)) {
|
|
1431
|
-
errors.push(new ParseError(
|
|
1649
|
+
errors.push(new ParseError(`${label} "${node.name}" on "${obj.name}" references field "${fieldName}" ` +
|
|
1432
1650
|
`which does not exist on "${obj.name}". ` +
|
|
1433
|
-
`Available fields: ${[...effectiveFieldNames].join(", ") || "(none)"}`, { code: "ERR_INVALID_INDEX", source:
|
|
1651
|
+
`Available fields: ${[...effectiveFieldNames].join(", ") || "(none)"}`, { code: "ERR_INVALID_INDEX", source: node.source }));
|
|
1434
1652
|
}
|
|
1435
1653
|
}
|
|
1436
1654
|
}
|