@metaobjectsdev/metadata 0.9.0 → 0.11.0-rc.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 +1 -1
- package/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +96 -11
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/constraint-merge.d.ts +18 -0
- package/dist/constraint-merge.d.ts.map +1 -0
- package/dist/constraint-merge.js +0 -0
- package/dist/constraint-merge.js.map +1 -0
- package/dist/constraint-validate.d.ts +6 -0
- package/dist/constraint-validate.d.ts.map +1 -0
- package/dist/constraint-validate.js +274 -0
- package/dist/constraint-validate.js.map +1 -0
- package/dist/core/attr/attr-constants.d.ts +13 -3
- package/dist/core/attr/attr-constants.d.ts.map +1 -1
- package/dist/core/attr/attr-constants.js +11 -2
- package/dist/core/attr/attr-constants.js.map +1 -1
- package/dist/core/attr/attr-definition.embedded.d.ts +3 -0
- package/dist/core/attr/attr-definition.embedded.d.ts.map +1 -0
- package/dist/core/attr/attr-definition.embedded.js +60 -0
- package/dist/core/attr/attr-definition.embedded.js.map +1 -0
- package/dist/core/documentation/doc-constants.d.ts +3 -2
- package/dist/core/documentation/doc-constants.d.ts.map +1 -1
- package/dist/core/documentation/doc-constants.js +3 -1
- package/dist/core/documentation/doc-constants.js.map +1 -1
- package/dist/core/documentation/doc-provider.d.ts.map +1 -1
- package/dist/core/documentation/doc-provider.js +6 -2
- package/dist/core/documentation/doc-provider.js.map +1 -1
- package/dist/core/documentation/documentation-definition.embedded.d.ts +3 -0
- package/dist/core/documentation/documentation-definition.embedded.d.ts.map +1 -0
- package/dist/core/documentation/documentation-definition.embedded.js +79 -0
- package/dist/core/documentation/documentation-definition.embedded.js.map +1 -0
- package/dist/core/field/field-constants.d.ts +7 -4
- package/dist/core/field/field-constants.d.ts.map +1 -1
- package/dist/core/field/field-constants.js +7 -7
- package/dist/core/field/field-constants.js.map +1 -1
- package/dist/core/field/field-definition.embedded.d.ts +3 -0
- package/dist/core/field/field-definition.embedded.d.ts.map +1 -0
- package/dist/core/field/field-definition.embedded.js +236 -0
- package/dist/core/field/field-definition.embedded.js.map +1 -0
- package/dist/core/field/meta-field.d.ts.map +1 -1
- package/dist/core/field/meta-field.js +7 -5
- package/dist/core/field/meta-field.js.map +1 -1
- package/dist/core/identity/identity-constants.d.ts.map +1 -1
- package/dist/core/identity/identity-constants.js +3 -0
- package/dist/core/identity/identity-constants.js.map +1 -1
- package/dist/core/identity/identity-definition.embedded.d.ts +3 -0
- package/dist/core/identity/identity-definition.embedded.d.ts.map +1 -0
- package/dist/core/identity/identity-definition.embedded.js +93 -0
- package/dist/core/identity/identity-definition.embedded.js.map +1 -0
- package/dist/core/identity/meta-identity.d.ts.map +1 -1
- package/dist/core/identity/meta-identity.js +8 -1
- package/dist/core/identity/meta-identity.js.map +1 -1
- package/dist/core/identity/validate-identity-passthrough.d.ts +42 -0
- package/dist/core/identity/validate-identity-passthrough.d.ts.map +1 -0
- package/dist/core/identity/validate-identity-passthrough.js +158 -0
- package/dist/core/identity/validate-identity-passthrough.js.map +1 -0
- package/dist/core/object/object-constants.d.ts +2 -1
- package/dist/core/object/object-constants.d.ts.map +1 -1
- package/dist/core/object/object-constants.js +3 -0
- package/dist/core/object/object-constants.js.map +1 -1
- package/dist/core/object/object-definition.embedded.d.ts +3 -0
- package/dist/core/object/object-definition.embedded.d.ts.map +1 -0
- package/dist/core/object/object-definition.embedded.js +110 -0
- package/dist/core/object/object-definition.embedded.js.map +1 -0
- package/dist/core/object/validate-discriminator.d.ts.map +1 -1
- package/dist/core/object/validate-discriminator.js +1 -3
- package/dist/core/object/validate-discriminator.js.map +1 -1
- package/dist/core/query/query-constants.d.ts.map +1 -1
- package/dist/core/query/query-constants.js +5 -3
- package/dist/core/query/query-constants.js.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.d.ts +26 -0
- package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -0
- package/dist/core/relationship/derive-m2m-fields.js +102 -0
- package/dist/core/relationship/derive-m2m-fields.js.map +1 -0
- package/dist/core/relationship/meta-relationship.d.ts +6 -4
- package/dist/core/relationship/meta-relationship.d.ts.map +1 -1
- package/dist/core/relationship/meta-relationship.js +12 -8
- package/dist/core/relationship/meta-relationship.js.map +1 -1
- package/dist/core/relationship/relationship-constants.d.ts +6 -2
- package/dist/core/relationship/relationship-constants.d.ts.map +1 -1
- package/dist/core/relationship/relationship-constants.js +6 -2
- package/dist/core/relationship/relationship-constants.js.map +1 -1
- package/dist/core/relationship/relationship-definition.embedded.d.ts +3 -0
- package/dist/core/relationship/relationship-definition.embedded.d.ts.map +1 -0
- package/dist/core/relationship/relationship-definition.embedded.js +310 -0
- package/dist/core/relationship/relationship-definition.embedded.js.map +1 -0
- package/dist/core/validator/validator-constants.d.ts +14 -1
- package/dist/core/validator/validator-constants.d.ts.map +1 -1
- package/dist/core/validator/validator-constants.js +20 -1
- package/dist/core/validator/validator-constants.js.map +1 -1
- package/dist/core/validator/validator-definition.embedded.d.ts +3 -0
- package/dist/core/validator/validator-definition.embedded.d.ts.map +1 -0
- package/dist/core/validator/validator-definition.embedded.js +255 -0
- package/dist/core/validator/validator-definition.embedded.js.map +1 -0
- package/dist/core/yaml-desugar.d.ts.map +1 -1
- package/dist/core/yaml-desugar.js +88 -10
- package/dist/core/yaml-desugar.js.map +1 -1
- package/dist/core-types.d.ts +5 -2
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +285 -116
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +3 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +87 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +17 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/library/embedded-library.generated.d.ts +2 -0
- package/dist/library/embedded-library.generated.d.ts.map +1 -0
- package/dist/library/embedded-library.generated.js +11 -0
- package/dist/library/embedded-library.generated.js.map +1 -0
- package/dist/library/library-sources.d.ts +12 -0
- package/dist/library/library-sources.d.ts.map +1 -0
- package/dist/library/library-sources.js +88 -0
- package/dist/library/library-sources.js.map +1 -0
- package/dist/loader/meta-data-loader.d.ts +10 -2
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +57 -6
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/shortcuts.d.ts +2 -5
- package/dist/loader/shortcuts.d.ts.map +1 -1
- package/dist/loader/shortcuts.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +3 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +519 -34
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/loader/validation-registry.d.ts +10 -0
- package/dist/loader/validation-registry.d.ts.map +1 -0
- package/dist/loader/validation-registry.js +84 -0
- package/dist/loader/validation-registry.js.map +1 -0
- package/dist/metamodel-docs/index.d.ts +19 -0
- package/dist/metamodel-docs/index.d.ts.map +1 -0
- package/dist/metamodel-docs/index.js +37 -0
- package/dist/metamodel-docs/index.js.map +1 -0
- package/dist/metamodel-docs/provenance.d.ts +42 -0
- package/dist/metamodel-docs/provenance.d.ts.map +1 -0
- package/dist/metamodel-docs/provenance.js +148 -0
- package/dist/metamodel-docs/provenance.js.map +1 -0
- package/dist/metamodel-docs/provider-definitions.d.ts +8 -0
- package/dist/metamodel-docs/provider-definitions.d.ts.map +1 -0
- package/dist/metamodel-docs/provider-definitions.js +48 -0
- package/dist/metamodel-docs/provider-definitions.js.map +1 -0
- package/dist/metamodel-docs/render.d.ts +12 -0
- package/dist/metamodel-docs/render.d.ts.map +1 -0
- package/dist/metamodel-docs/render.js +252 -0
- package/dist/metamodel-docs/render.js.map +1 -0
- package/dist/naming-refs.d.ts +41 -0
- package/dist/naming-refs.d.ts.map +1 -0
- package/dist/naming-refs.js +144 -0
- package/dist/naming-refs.js.map +1 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +10 -2
- package/dist/naming.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +84 -9
- package/dist/parser-core.js.map +1 -1
- package/dist/persistence/db/db-constants.d.ts +10 -0
- package/dist/persistence/db/db-constants.d.ts.map +1 -1
- package/dist/persistence/db/db-constants.js +14 -0
- package/dist/persistence/db/db-constants.js.map +1 -1
- package/dist/persistence/db/db-definition.embedded.d.ts +3 -0
- package/dist/persistence/db/db-definition.embedded.d.ts.map +1 -0
- package/dist/persistence/db/db-definition.embedded.js +227 -0
- package/dist/persistence/db/db-definition.embedded.js.map +1 -0
- package/dist/persistence/db/db-provider.d.ts.map +1 -1
- package/dist/persistence/db/db-provider.js +13 -18
- package/dist/persistence/db/db-provider.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.d.ts +3 -0
- package/dist/persistence/origin/origin-definition.embedded.d.ts.map +1 -0
- package/dist/persistence/origin/origin-definition.embedded.js +88 -0
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -0
- package/dist/persistence/source/source-definition.embedded.d.ts +3 -0
- package/dist/persistence/source/source-definition.embedded.d.ts.map +1 -0
- package/dist/persistence/source/source-definition.embedded.js +17 -0
- package/dist/persistence/source/source-definition.embedded.js.map +1 -0
- package/dist/persistence/source/validate-source-parameter-ref.d.ts.map +1 -1
- package/dist/persistence/source/validate-source-parameter-ref.js +7 -1
- package/dist/persistence/source/validate-source-parameter-ref.js.map +1 -1
- package/dist/persistence/source/validate-source-roles.d.ts.map +1 -1
- package/dist/persistence/source/validate-source-roles.js +16 -1
- package/dist/persistence/source/validate-source-roles.js.map +1 -1
- package/dist/presentation/layout/layout-definition.embedded.d.ts +3 -0
- package/dist/presentation/layout/layout-definition.embedded.d.ts.map +1 -0
- package/dist/presentation/layout/layout-definition.embedded.js +16 -0
- package/dist/presentation/layout/layout-definition.embedded.js.map +1 -0
- package/dist/presentation/ui/ui-definition.embedded.d.ts +3 -0
- package/dist/presentation/ui/ui-definition.embedded.d.ts.map +1 -0
- package/dist/presentation/ui/ui-definition.embedded.js +114 -0
- package/dist/presentation/ui/ui-definition.embedded.js.map +1 -0
- package/dist/presentation/ui/ui-provider.d.ts +3 -0
- package/dist/presentation/ui/ui-provider.d.ts.map +1 -0
- package/dist/presentation/ui/ui-provider.js +21 -0
- package/dist/presentation/ui/ui-provider.js.map +1 -0
- package/dist/presentation/view/view-definition.embedded.d.ts +3 -0
- package/dist/presentation/view/view-definition.embedded.d.ts.map +1 -0
- package/dist/presentation/view/view-definition.embedded.js +76 -0
- package/dist/presentation/view/view-definition.embedded.js.map +1 -0
- package/dist/provider-data.d.ts +184 -0
- package/dist/provider-data.d.ts.map +1 -0
- package/dist/provider-data.js +271 -0
- package/dist/provider-data.js.map +1 -0
- package/dist/provider.d.ts +3 -1
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +15 -1
- package/dist/provider.js.map +1 -1
- package/dist/registry-coverage.d.ts +99 -0
- package/dist/registry-coverage.d.ts.map +1 -0
- package/dist/registry-coverage.js +294 -0
- package/dist/registry-coverage.js.map +1 -0
- package/dist/registry-manifest-exclusions.d.ts +62 -0
- package/dist/registry-manifest-exclusions.d.ts.map +1 -0
- package/dist/registry-manifest-exclusions.js +163 -0
- package/dist/registry-manifest-exclusions.js.map +1 -0
- package/dist/registry-manifest.d.ts +117 -0
- package/dist/registry-manifest.d.ts.map +1 -0
- package/dist/registry-manifest.js +242 -0
- package/dist/registry-manifest.js.map +1 -0
- package/dist/registry.d.ts +76 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +37 -1
- package/dist/registry.js.map +1 -1
- package/dist/shared/structural.d.ts +7 -0
- package/dist/shared/structural.d.ts.map +1 -1
- package/dist/shared/structural.js +7 -0
- package/dist/shared/structural.js.map +1 -1
- package/dist/subtype-rules.d.ts.map +1 -1
- package/dist/subtype-rules.js +97 -13
- package/dist/subtype-rules.js.map +1 -1
- package/dist/super-resolve.d.ts +49 -2
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +128 -43
- package/dist/super-resolve.js.map +1 -1
- package/dist/template/meta-template.d.ts +3 -2
- package/dist/template/meta-template.d.ts.map +1 -1
- package/dist/template/meta-template.js +3 -2
- package/dist/template/meta-template.js.map +1 -1
- package/dist/template/prompt-definition.embedded.d.ts +3 -0
- package/dist/template/prompt-definition.embedded.d.ts.map +1 -0
- package/dist/template/prompt-definition.embedded.js +368 -0
- package/dist/template/prompt-definition.embedded.js.map +1 -0
- package/dist/template/prompt-provider.d.ts +3 -0
- package/dist/template/prompt-provider.d.ts.map +1 -0
- package/dist/template/prompt-provider.js +25 -0
- package/dist/template/prompt-provider.js.map +1 -0
- package/dist/template/template-constants.d.ts +2 -0
- package/dist/template/template-constants.d.ts.map +1 -1
- package/dist/template/template-constants.js +7 -0
- package/dist/template/template-constants.js.map +1 -1
- package/dist/template/template-definition.embedded.d.ts +3 -0
- package/dist/template/template-definition.embedded.d.ts.map +1 -0
- package/dist/template/template-definition.embedded.js +30 -0
- package/dist/template/template-definition.embedded.js.map +1 -0
- package/dist/validate-max-occurs.d.ts +5 -0
- package/dist/validate-max-occurs.d.ts.map +1 -0
- package/dist/validate-max-occurs.js +28 -0
- package/dist/validate-max-occurs.js.map +1 -0
- package/dist/validation-types.d.ts +36 -0
- package/dist/validation-types.d.ts.map +1 -0
- package/dist/validation-types.js +7 -0
- package/dist/validation-types.js.map +1 -0
- package/package.json +33 -22
- package/src/attr-schema-validate.ts +108 -8
- package/src/constraint-merge.ts +0 -0
- package/src/constraint-validate.ts +363 -0
- package/src/core/attr/attr-constants.ts +15 -3
- package/src/core/attr/attr-definition.embedded.ts +67 -0
- package/src/core/documentation/doc-constants.ts +3 -1
- package/src/core/documentation/doc-provider.ts +6 -2
- package/src/core/documentation/documentation-definition.embedded.ts +86 -0
- package/src/core/field/field-constants.ts +8 -7
- package/src/core/field/field-definition.embedded.ts +243 -0
- package/src/core/field/meta-field.ts +6 -7
- package/src/core/identity/identity-constants.ts +4 -0
- package/src/core/identity/identity-definition.embedded.ts +100 -0
- package/src/core/identity/meta-identity.ts +8 -1
- package/src/core/identity/validate-identity-passthrough.ts +194 -0
- package/src/core/object/object-constants.ts +3 -0
- package/src/core/object/object-definition.embedded.ts +117 -0
- package/src/core/object/validate-discriminator.ts +0 -4
- package/src/core/query/query-constants.ts +5 -3
- package/src/core/relationship/derive-m2m-fields.ts +145 -0
- package/src/core/relationship/meta-relationship.ts +15 -9
- package/src/core/relationship/relationship-constants.ts +6 -2
- package/src/core/relationship/relationship-definition.embedded.ts +317 -0
- package/src/core/validator/validator-constants.ts +22 -1
- package/src/core/validator/validator-definition.embedded.ts +262 -0
- package/src/core/yaml-desugar.ts +96 -7
- package/src/core-types.ts +314 -150
- package/src/errors.ts +89 -2
- package/src/index.ts +47 -2
- package/src/library/embedded-library.generated.ts +10 -0
- package/src/library/library-sources.ts +97 -0
- package/src/loader/meta-data-loader.ts +78 -7
- package/src/loader/shortcuts.ts +2 -2
- package/src/loader/validation-passes.ts +690 -34
- package/src/loader/validation-registry.ts +93 -0
- package/src/metamodel-docs/index.ts +41 -0
- package/src/metamodel-docs/provenance.ts +187 -0
- package/src/metamodel-docs/provider-definitions.ts +50 -0
- package/src/metamodel-docs/render.ts +309 -0
- package/src/naming-refs.ts +162 -0
- package/src/naming.ts +10 -2
- package/src/parser-core.ts +96 -9
- package/src/persistence/db/db-constants.ts +16 -0
- package/src/persistence/db/db-definition.embedded.ts +234 -0
- package/src/persistence/db/db-provider.ts +13 -18
- package/src/persistence/origin/origin-definition.embedded.ts +95 -0
- package/src/persistence/source/source-definition.embedded.ts +24 -0
- package/src/persistence/source/validate-source-parameter-ref.ts +7 -1
- package/src/persistence/source/validate-source-roles.ts +22 -1
- package/src/presentation/layout/layout-definition.embedded.ts +23 -0
- package/src/presentation/ui/ui-definition.embedded.ts +121 -0
- package/src/presentation/ui/ui-provider.ts +25 -0
- package/src/presentation/view/view-definition.embedded.ts +83 -0
- package/src/provider-data.ts +463 -0
- package/src/provider.ts +18 -0
- package/src/registry-coverage.ts +430 -0
- package/src/registry-manifest-exclusions.ts +176 -0
- package/src/registry-manifest.ts +334 -0
- package/src/registry.ts +106 -3
- package/src/shared/structural.ts +8 -0
- package/src/subtype-rules.ts +135 -18
- package/src/super-resolve.ts +153 -43
- package/src/template/meta-template.ts +3 -2
- package/src/template/prompt-definition.embedded.ts +375 -0
- package/src/template/prompt-provider.ts +29 -0
- package/src/template/template-constants.ts +8 -0
- package/src/template/template-definition.embedded.ts +37 -0
- package/src/validate-max-occurs.ts +39 -0
- package/src/validation-types.ts +57 -0
- package/dist/core/documentation/doc-schema.d.ts +0 -8
- package/dist/core/documentation/doc-schema.d.ts.map +0 -1
- package/dist/core/documentation/doc-schema.js +0 -53
- package/dist/core/documentation/doc-schema.js.map +0 -1
- package/dist/core/field/field-schema.d.ts +0 -22
- package/dist/core/field/field-schema.d.ts.map +0 -1
- package/dist/core/field/field-schema.js +0 -171
- package/dist/core/field/field-schema.js.map +0 -1
- package/dist/core/identity/identity-schema.d.ts +0 -6
- package/dist/core/identity/identity-schema.d.ts.map +0 -1
- package/dist/core/identity/identity-schema.js +0 -55
- package/dist/core/identity/identity-schema.js.map +0 -1
- package/dist/core/object/object-schema.d.ts +0 -4
- package/dist/core/object/object-schema.d.ts.map +0 -1
- package/dist/core/object/object-schema.js +0 -28
- package/dist/core/object/object-schema.js.map +0 -1
- package/dist/core/relationship/relationship-schema.d.ts +0 -4
- package/dist/core/relationship/relationship-schema.d.ts.map +0 -1
- package/dist/core/relationship/relationship-schema.js +0 -51
- package/dist/core/relationship/relationship-schema.js.map +0 -1
- package/dist/core/validator/validator-schema.d.ts +0 -4
- package/dist/core/validator/validator-schema.d.ts.map +0 -1
- package/dist/core/validator/validator-schema.js +0 -38
- package/dist/core/validator/validator-schema.js.map +0 -1
- package/dist/persistence/db/db-schema.d.ts +0 -14
- package/dist/persistence/db/db-schema.d.ts.map +0 -1
- package/dist/persistence/db/db-schema.js +0 -35
- package/dist/persistence/db/db-schema.js.map +0 -1
- package/dist/persistence/origin/origin-schema.d.ts +0 -4
- package/dist/persistence/origin/origin-schema.d.ts.map +0 -1
- package/dist/persistence/origin/origin-schema.js +0 -59
- package/dist/persistence/origin/origin-schema.js.map +0 -1
- package/dist/persistence/source/source-schema.d.ts +0 -4
- package/dist/persistence/source/source-schema.d.ts.map +0 -1
- package/dist/persistence/source/source-schema.js +0 -98
- package/dist/persistence/source/source-schema.js.map +0 -1
- package/dist/presentation/layout/layout-schema.d.ts +0 -4
- package/dist/presentation/layout/layout-schema.d.ts.map +0 -1
- package/dist/presentation/layout/layout-schema.js +0 -46
- package/dist/presentation/layout/layout-schema.js.map +0 -1
- package/dist/presentation/view/view-schema.d.ts +0 -4
- package/dist/presentation/view/view-schema.d.ts.map +0 -1
- package/dist/presentation/view/view-schema.js +0 -15
- package/dist/presentation/view/view-schema.js.map +0 -1
- package/dist/template/template-schema.d.ts +0 -3
- package/dist/template/template-schema.d.ts.map +0 -1
- package/dist/template/template-schema.js +0 -173
- package/dist/template/template-schema.js.map +0 -1
- package/src/core/documentation/doc-schema.ts +0 -64
- package/src/core/field/field-schema.ts +0 -228
- package/src/core/identity/identity-schema.ts +0 -80
- package/src/core/object/object-schema.ts +0 -35
- package/src/core/relationship/relationship-schema.ts +0 -67
- package/src/core/validator/validator-schema.ts +0 -50
- package/src/persistence/db/db-schema.ts +0 -50
- package/src/persistence/origin/origin-schema.ts +0 -80
- package/src/persistence/source/source-schema.ts +0 -129
- package/src/presentation/layout/layout-schema.ts +0 -62
- package/src/presentation/view/view-schema.ts +0 -21
- package/src/template/template-schema.ts +0 -211
|
@@ -4,13 +4,17 @@
|
|
|
4
4
|
// warnings. No loader state is read or written — these are pure functions.
|
|
5
5
|
//
|
|
6
6
|
// Exported: validateDataGridSortFields, validateFilterableHasIndex,
|
|
7
|
-
// validateOriginPaths,
|
|
7
|
+
// validateOriginPaths, validateDerivedFieldProvidability,
|
|
8
|
+
// validateDataGridFilterValues,
|
|
8
9
|
// validateFieldObjectStorage (called by MetaDataLoader.load() in order).
|
|
9
10
|
// Private: _findObject, _findField, _findRelationship,
|
|
10
11
|
// _validateFromPath, _validateViaPath (helpers, not exported).
|
|
11
12
|
|
|
12
13
|
import type { MetaData } from "../shared/meta-data.js";
|
|
14
|
+
import type { MetaObject } from "../core/object/meta-object.js";
|
|
15
|
+
import type { MetaReferenceIdentity } from "../core/identity/meta-identity.js";
|
|
13
16
|
import { ParseError } from "../errors.js";
|
|
17
|
+
import { refMatchesObject } from "../naming-refs.js";
|
|
14
18
|
import { resolvedSource, type ErrorSource } from "../source.js";
|
|
15
19
|
import {
|
|
16
20
|
TYPE_OBJECT,
|
|
@@ -19,10 +23,12 @@ import {
|
|
|
19
23
|
TYPE_IDENTITY,
|
|
20
24
|
TYPE_ORIGIN,
|
|
21
25
|
TYPE_RELATIONSHIP,
|
|
26
|
+
TYPE_SOURCE,
|
|
22
27
|
TYPE_TEMPLATE,
|
|
23
28
|
} from "../shared/base-types.js";
|
|
24
29
|
import {
|
|
25
30
|
TEMPLATE_ATTR_PAYLOAD_REF,
|
|
31
|
+
TEMPLATE_ATTR_RESPONSE_REF,
|
|
26
32
|
TEMPLATE_ATTR_REQUIRED_SLOTS,
|
|
27
33
|
TEMPLATE_ATTR_TEXT_REF,
|
|
28
34
|
TEMPLATE_ATTR_KIND,
|
|
@@ -32,7 +38,12 @@ import {
|
|
|
32
38
|
TEMPLATE_SUBTYPE_OUTPUT,
|
|
33
39
|
TEMPLATE_SUBTYPE_PROMPT,
|
|
34
40
|
} from "../template/template-constants.js";
|
|
35
|
-
import {
|
|
41
|
+
import {
|
|
42
|
+
OBJECT_SUBTYPE_ENTITY,
|
|
43
|
+
OBJECT_SUBTYPE_VALUE,
|
|
44
|
+
OBJECT_SUBTYPE_PROJECTION,
|
|
45
|
+
} from "../core/object/object-constants.js";
|
|
46
|
+
import { MetaSource } from "../persistence/source/meta-source.js";
|
|
36
47
|
import {
|
|
37
48
|
LAYOUT_SUBTYPE_DATA_GRID,
|
|
38
49
|
LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD,
|
|
@@ -52,9 +63,13 @@ import {
|
|
|
52
63
|
FIELD_SUBTYPE_DECIMAL,
|
|
53
64
|
FIELD_SUBTYPE_BOOLEAN,
|
|
54
65
|
FIELD_SUBTYPE_ENUM,
|
|
66
|
+
FIELD_SUBTYPE_OBJECT,
|
|
55
67
|
} from "../core/field/field-constants.js";
|
|
56
68
|
import { FIELD_ATTR_DB_INDEXED } from "../persistence/db/db-constants.js";
|
|
57
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
IDENTITY_ATTR_FIELDS,
|
|
71
|
+
IDENTITY_SUBTYPE_REFERENCE,
|
|
72
|
+
} from "../core/identity/identity-constants.js";
|
|
58
73
|
import {
|
|
59
74
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
60
75
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
@@ -63,7 +78,16 @@ import {
|
|
|
63
78
|
ORIGIN_AGGREGATE_ATTR_OF,
|
|
64
79
|
ORIGIN_AGGREGATE_ATTR_VIA,
|
|
65
80
|
} from "../persistence/origin/origin-constants.js";
|
|
66
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
83
|
+
RELATIONSHIP_ATTR_CARDINALITY,
|
|
84
|
+
RELATIONSHIP_ATTR_THROUGH,
|
|
85
|
+
RELATIONSHIP_ATTR_SOURCE_REF_FIELD,
|
|
86
|
+
RELATIONSHIP_ATTR_SYMMETRIC,
|
|
87
|
+
CARDINALITY_ONE,
|
|
88
|
+
CARDINALITY_MANY,
|
|
89
|
+
} from "../core/relationship/relationship-constants.js";
|
|
90
|
+
import { stripPackage } from "../naming.js";
|
|
67
91
|
import {
|
|
68
92
|
FILTER_COMPOSE_OR,
|
|
69
93
|
FILTER_COMPOSE_AND,
|
|
@@ -110,9 +134,19 @@ export function validateDataGridSortFields(root: MetaData): ParseError[] {
|
|
|
110
134
|
// `meta verify` step, not here.
|
|
111
135
|
// ---------------------------------------------------------------------------
|
|
112
136
|
|
|
137
|
+
/** Recursively collect all template.* nodes anywhere in the metadata tree. */
|
|
138
|
+
function allTemplates(node: MetaData): MetaData[] {
|
|
139
|
+
const out: MetaData[] = [];
|
|
140
|
+
for (const c of node.ownChildren()) {
|
|
141
|
+
if (c.type === TYPE_TEMPLATE) out.push(c);
|
|
142
|
+
out.push(...allTemplates(c));
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
113
147
|
export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
|
|
114
148
|
const errors: ParseError[] = [];
|
|
115
|
-
for (const tmpl of root
|
|
149
|
+
for (const tmpl of allTemplates(root)) {
|
|
116
150
|
// --- @kind / textRef / email part-ref cross-field rules ---
|
|
117
151
|
// template.output is either a document (@kind absent/"document" → @textRef
|
|
118
152
|
// required) or an email (@kind="email" → @subjectRef + @htmlBodyRef required,
|
|
@@ -165,7 +199,7 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
|
|
|
165
199
|
|
|
166
200
|
const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
|
|
167
201
|
if (typeof payloadRef !== "string") continue; // absence handled by the required-attr schema check
|
|
168
|
-
const payload = root.ownChildren().find((c) => c.type === TYPE_OBJECT && c
|
|
202
|
+
const payload = root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
|
|
169
203
|
if (!payload || payload.subType !== OBJECT_SUBTYPE_VALUE) {
|
|
170
204
|
// FR5d — @payloadRef is a reference; emit format=resolved with
|
|
171
205
|
// referrer=template FQN, target=the unresolved payloadRef string.
|
|
@@ -180,6 +214,18 @@ export function validateTemplatePayloadRefs(root: MetaData): ParseError[] {
|
|
|
180
214
|
);
|
|
181
215
|
continue;
|
|
182
216
|
}
|
|
217
|
+
const responseRef = tmpl.ownAttr(TEMPLATE_ATTR_RESPONSE_REF);
|
|
218
|
+
if (typeof responseRef === "string") {
|
|
219
|
+
const resVo = root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, responseRef));
|
|
220
|
+
if (!resVo || resVo.subType !== OBJECT_SUBTYPE_VALUE) {
|
|
221
|
+
errors.push(
|
|
222
|
+
new ParseError(
|
|
223
|
+
`template "${tmpl.name}" @responseRef "${responseRef}" does not resolve to an object.value at root`,
|
|
224
|
+
{ code: "ERR_INVALID_TEMPLATE", source: resolvedSource(tmpl.source, tmpl.fqn(), responseRef) },
|
|
225
|
+
),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
183
229
|
const fieldNames = new Set(
|
|
184
230
|
payload.children().filter((c) => c.type === TYPE_FIELD).map((f) => f.name),
|
|
185
231
|
);
|
|
@@ -242,6 +288,35 @@ export function validateFilterableHasIndex(root: MetaData): string[] {
|
|
|
242
288
|
return warnings;
|
|
243
289
|
}
|
|
244
290
|
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// @filterable on a subtype with no operator band (SP-H Unit9)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// A field marked @filterable: true whose subtype has NO entry in OPS_BY_SUBTYPE
|
|
295
|
+
// (e.g. field.object, or any extension subtype without a declared op band)
|
|
296
|
+
// would silently generate a filter type/allowlist with an empty op set — a
|
|
297
|
+
// filter route that rejects every request. Error early instead of shipping
|
|
298
|
+
// broken codegen. → ERR_FILTERABLE_UNSUPPORTED_SUBTYPE.
|
|
299
|
+
|
|
300
|
+
export function validateFilterableHasSupportedOps(root: MetaData): ParseError[] {
|
|
301
|
+
const errors: ParseError[] = [];
|
|
302
|
+
for (const obj of root.ownChildren().filter((c) => c.type === TYPE_OBJECT)) {
|
|
303
|
+
// children() — inherited @filterable fields (via extends:/super:) are visible.
|
|
304
|
+
for (const field of obj.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
305
|
+
if (field.ownAttr(FIELD_ATTR_FILTERABLE) !== true) continue;
|
|
306
|
+
if (opsForSubType(field.subType).length > 0) continue;
|
|
307
|
+
errors.push(
|
|
308
|
+
new ParseError(
|
|
309
|
+
`Field "${obj.name}.${field.name}" has @filterable: true but its subtype ` +
|
|
310
|
+
`"${field.subType}" has no filter-operator band. Remove @filterable, or use a ` +
|
|
311
|
+
`field subtype that supports filtering (string/enum/uuid/number/currency/date/boolean).`,
|
|
312
|
+
{ code: "ERR_FILTERABLE_UNSUPPORTED_SUBTYPE", source: field.source },
|
|
313
|
+
),
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return errors;
|
|
318
|
+
}
|
|
319
|
+
|
|
245
320
|
// ---------------------------------------------------------------------------
|
|
246
321
|
// Origin path validation
|
|
247
322
|
//
|
|
@@ -257,7 +332,9 @@ export function validateFilterableHasIndex(root: MetaData): string[] {
|
|
|
257
332
|
// ---------------------------------------------------------------------------
|
|
258
333
|
|
|
259
334
|
function _findObject(root: MetaData, name: string): MetaData | undefined {
|
|
260
|
-
|
|
335
|
+
// FR-032 — origin heads are FQN-qualified after the desugar/sweep; match on
|
|
336
|
+
// the effective FQN resolution key (with bare back-compat).
|
|
337
|
+
return root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, name));
|
|
261
338
|
}
|
|
262
339
|
|
|
263
340
|
function _findField(obj: MetaData, name: string): MetaData | undefined {
|
|
@@ -270,6 +347,21 @@ function _findRelationship(obj: MetaData, name: string): MetaData | undefined {
|
|
|
270
347
|
return obj.children().find((c) => c.type === TYPE_RELATIONSHIP && c.name === name);
|
|
271
348
|
}
|
|
272
349
|
|
|
350
|
+
/** Resolved `Entity.field` reference target: the entity AND the field node.
|
|
351
|
+
* FR-024 B5 inference needs the entity; the B6 extends/origin agreement
|
|
352
|
+
* check compares against the field node identity. */
|
|
353
|
+
interface ResolvedFromTarget {
|
|
354
|
+
readonly entity: MetaData;
|
|
355
|
+
readonly field: MetaData;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Validate a passthrough `@from` / aggregate `@of` "Entity.field" reference.
|
|
360
|
+
* Returns the resolved target entity + field on full success (FR-024 B5 —
|
|
361
|
+
* the inference/cardinality stage needs the entity; B6 agreement needs the
|
|
362
|
+
* field), or undefined when any error was pushed (malformed shape / unknown
|
|
363
|
+
* entity / unknown field).
|
|
364
|
+
*/
|
|
273
365
|
function _validateFromPath(
|
|
274
366
|
fromAttr: string,
|
|
275
367
|
root: MetaData,
|
|
@@ -278,7 +370,7 @@ function _validateFromPath(
|
|
|
278
370
|
originSource: ErrorSource,
|
|
279
371
|
errors: ParseError[],
|
|
280
372
|
label: string = "origin.passthrough.@from",
|
|
281
|
-
):
|
|
373
|
+
): ResolvedFromTarget | undefined {
|
|
282
374
|
const projectionName = projection.name;
|
|
283
375
|
// FR5d — referrer is `<projection-FQN>::<fieldName>` (the canonical
|
|
284
376
|
// "where the broken reference lives" identifier).
|
|
@@ -297,7 +389,7 @@ function _validateFromPath(
|
|
|
297
389
|
},
|
|
298
390
|
),
|
|
299
391
|
);
|
|
300
|
-
return;
|
|
392
|
+
return undefined;
|
|
301
393
|
}
|
|
302
394
|
const entityName = fromAttr.slice(0, dotIdx);
|
|
303
395
|
const targetFieldName = fromAttr.slice(dotIdx + 1);
|
|
@@ -313,7 +405,7 @@ function _validateFromPath(
|
|
|
313
405
|
},
|
|
314
406
|
),
|
|
315
407
|
);
|
|
316
|
-
return;
|
|
408
|
+
return undefined;
|
|
317
409
|
}
|
|
318
410
|
const sourceField = _findField(sourceObj, targetFieldName);
|
|
319
411
|
if (!sourceField) {
|
|
@@ -327,9 +419,16 @@ function _validateFromPath(
|
|
|
327
419
|
},
|
|
328
420
|
),
|
|
329
421
|
);
|
|
422
|
+
return undefined;
|
|
330
423
|
}
|
|
424
|
+
return { entity: sourceObj, field: sourceField };
|
|
331
425
|
}
|
|
332
426
|
|
|
427
|
+
/**
|
|
428
|
+
* Validate an explicit `@via` "Entity.rel[.rel...]" path. Returns the walked
|
|
429
|
+
* relationship hop nodes (in path order) on full success — FR-024 B5 runs the
|
|
430
|
+
* cardinality checks over them — or undefined when any error was pushed.
|
|
431
|
+
*/
|
|
333
432
|
function _validateViaPath(
|
|
334
433
|
viaAttr: string,
|
|
335
434
|
root: MetaData,
|
|
@@ -337,7 +436,7 @@ function _validateViaPath(
|
|
|
337
436
|
fieldName: string,
|
|
338
437
|
originSource: ErrorSource,
|
|
339
438
|
errors: ParseError[],
|
|
340
|
-
):
|
|
439
|
+
): MetaData[] | undefined {
|
|
341
440
|
const projectionName = projection.name;
|
|
342
441
|
// FR5d — referrer is `<projection-FQN>::<fieldName>`.
|
|
343
442
|
const referrer = `${projection.fqn()}::${fieldName}`;
|
|
@@ -352,7 +451,7 @@ function _validateViaPath(
|
|
|
352
451
|
},
|
|
353
452
|
),
|
|
354
453
|
);
|
|
355
|
-
return;
|
|
454
|
+
return undefined;
|
|
356
455
|
}
|
|
357
456
|
const [entityName, ...relSegments] = segments as [string, ...string[]];
|
|
358
457
|
let currentObj = _findObject(root, entityName);
|
|
@@ -366,7 +465,7 @@ function _validateViaPath(
|
|
|
366
465
|
},
|
|
367
466
|
),
|
|
368
467
|
);
|
|
369
|
-
return;
|
|
468
|
+
return undefined;
|
|
370
469
|
}
|
|
371
470
|
// FR5d — track the deepest-valid-prefix as we walk. The prefix grows
|
|
372
471
|
// segment-by-segment; on a hop failure the error message names the prefix
|
|
@@ -374,6 +473,7 @@ function _validateViaPath(
|
|
|
374
473
|
// After the entity lookup above, the deepest valid prefix is just the
|
|
375
474
|
// entity name; each successful relationship hop appends a segment.
|
|
376
475
|
const validSegments: string[] = [entityName];
|
|
476
|
+
const hops: MetaData[] = [];
|
|
377
477
|
for (const relName of relSegments) {
|
|
378
478
|
const rel = _findRelationship(currentObj, relName);
|
|
379
479
|
if (!rel) {
|
|
@@ -388,7 +488,7 @@ function _validateViaPath(
|
|
|
388
488
|
},
|
|
389
489
|
),
|
|
390
490
|
);
|
|
391
|
-
return;
|
|
491
|
+
return undefined;
|
|
392
492
|
}
|
|
393
493
|
const refTarget = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF);
|
|
394
494
|
if (typeof refTarget !== "string" || refTarget === "") {
|
|
@@ -401,7 +501,7 @@ function _validateViaPath(
|
|
|
401
501
|
},
|
|
402
502
|
),
|
|
403
503
|
);
|
|
404
|
-
return;
|
|
504
|
+
return undefined;
|
|
405
505
|
}
|
|
406
506
|
const nextObj = _findObject(root, refTarget);
|
|
407
507
|
if (!nextObj) {
|
|
@@ -418,16 +518,302 @@ function _validateViaPath(
|
|
|
418
518
|
},
|
|
419
519
|
),
|
|
420
520
|
);
|
|
421
|
-
return;
|
|
521
|
+
return undefined;
|
|
422
522
|
}
|
|
423
523
|
validSegments.push(relName);
|
|
524
|
+
hops.push(rel);
|
|
424
525
|
currentObj = nextObj;
|
|
425
526
|
}
|
|
527
|
+
return hops;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// FR-024 B5 — base-entity derivation, single-hop-unique @via inference, and
|
|
532
|
+
// origin cardinality checks (spec §5–§6; ADR-0029 decisions 5–6).
|
|
533
|
+
// ---------------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
/** A hop relationship's effective @cardinality, or undefined when not declared. */
|
|
536
|
+
function _hopCardinality(rel: MetaData): string | undefined {
|
|
537
|
+
const v = rel.attr(RELATIONSHIP_ATTR_CARDINALITY);
|
|
538
|
+
return typeof v === "string" ? v : undefined;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Derive the BASE entity a no-`@via` origin path anchors at (spec §5):
|
|
543
|
+
* - an entity (or any non-projection host) is its own base — derived fields
|
|
544
|
+
* on multi-source entities anchor at the entity itself;
|
|
545
|
+
* - a projection's base is the owner entity of its EXTENDED identity
|
|
546
|
+
* (`identity.primary { extends: "Customer.id" }` — declared structurally);
|
|
547
|
+
* - fallback (no identity): the single distinct entity targeted by the
|
|
548
|
+
* projection's plain field-`extends` refs; >1 distinct entity →
|
|
549
|
+
* ERR_AMBIGUOUS_PATH instructing the author to declare an extended
|
|
550
|
+
* identity; 0 → ERR_INVALID_ORIGIN (no base derivable, cannot infer).
|
|
551
|
+
*
|
|
552
|
+
* Returns undefined when no base is derivable (an error has been pushed).
|
|
553
|
+
*/
|
|
554
|
+
/**
|
|
555
|
+
* FR-024: the entity NAMED by a node's dotted extends ref — the OWNER part of
|
|
556
|
+
* `<owner>.<child>...` resolved as an object. This differs from
|
|
557
|
+
* `superResolved.parent` when the resolved child is INHERITED: `Product.id`
|
|
558
|
+
* selecting BaseEntity's identity through Product's effective children must
|
|
559
|
+
* anchor `Product` (what the author wrote), never `BaseEntity` (where the
|
|
560
|
+
* child physically lives).
|
|
561
|
+
*/
|
|
562
|
+
function _refNamedOwner(node: MetaData, root: MetaData): MetaData | undefined {
|
|
563
|
+
const ref = node.superRef;
|
|
564
|
+
if (ref === undefined) return undefined;
|
|
565
|
+
const lastSep = ref.lastIndexOf("::");
|
|
566
|
+
const tail = lastSep === -1 ? ref : ref.slice(lastSep + 2);
|
|
567
|
+
const dot = tail.indexOf(".");
|
|
568
|
+
if (dot <= 0) return undefined;
|
|
569
|
+
return _findObject(root, tail.slice(0, dot));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function _deriveBaseEntity(
|
|
573
|
+
obj: MetaData,
|
|
574
|
+
root: MetaData,
|
|
575
|
+
fieldName: string,
|
|
576
|
+
originSource: ErrorSource,
|
|
577
|
+
errors: ParseError[],
|
|
578
|
+
): MetaData | undefined {
|
|
579
|
+
if (obj.subType !== OBJECT_SUBTYPE_PROJECTION) return obj;
|
|
580
|
+
|
|
581
|
+
// 1) The extended identity anchors the base entity (declared, not inferred).
|
|
582
|
+
// The anchor is the entity NAMED in the ref's owner part — see _refNamedOwner.
|
|
583
|
+
for (const identity of obj.ownChildren().filter((c) => c.type === TYPE_IDENTITY)) {
|
|
584
|
+
const extended = identity.superResolved;
|
|
585
|
+
if (extended !== undefined && extended.type === TYPE_IDENTITY) {
|
|
586
|
+
const named = _refNamedOwner(identity, root);
|
|
587
|
+
if (named !== undefined) return named;
|
|
588
|
+
const owner = extended.parent;
|
|
589
|
+
if (owner !== undefined && owner.type === TYPE_OBJECT) return owner;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 2) Fallback: the single distinct entity targeted by plain field-extends —
|
|
594
|
+
// again preferring the ref-named owner over the physical declaring ancestor.
|
|
595
|
+
const targets = new Set<MetaData>();
|
|
596
|
+
for (const f of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
597
|
+
const sup = f.superResolved;
|
|
598
|
+
if (sup === undefined) continue;
|
|
599
|
+
const named = _refNamedOwner(f, root);
|
|
600
|
+
const owner = named ?? sup.parent;
|
|
601
|
+
if (
|
|
602
|
+
owner !== undefined &&
|
|
603
|
+
owner.type === TYPE_OBJECT &&
|
|
604
|
+
owner.subType !== OBJECT_SUBTYPE_VALUE &&
|
|
605
|
+
owner !== obj
|
|
606
|
+
) {
|
|
607
|
+
targets.add(owner);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (targets.size === 1) return [...targets][0];
|
|
611
|
+
if (targets.size > 1) {
|
|
612
|
+
const names = [...targets].map((t) => `"${t.name}"`).join(", ");
|
|
613
|
+
errors.push(
|
|
614
|
+
new ParseError(
|
|
615
|
+
`origin on ${obj.name}.${fieldName}: cannot derive the base entity — the projection's fields extend ` +
|
|
616
|
+
`multiple entities (${names}) and no identity extends an entity identity. Declare an extended identity ` +
|
|
617
|
+
`(e.g. identity.primary { name: "id", extends: "<Entity>.<identity>" }) to anchor the base entity (FR-024).`,
|
|
618
|
+
{ code: "ERR_AMBIGUOUS_PATH", source: originSource },
|
|
619
|
+
),
|
|
620
|
+
);
|
|
621
|
+
} else {
|
|
622
|
+
errors.push(
|
|
623
|
+
new ParseError(
|
|
624
|
+
`origin on ${obj.name}.${fieldName}: cannot derive the base entity for @via inference — the projection ` +
|
|
625
|
+
`has no extended identity and no entity-targeted field extends. Declare an extended identity or an explicit @via (FR-024).`,
|
|
626
|
+
{ code: "ERR_INVALID_ORIGIN", source: originSource },
|
|
627
|
+
),
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return undefined;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* True when the `@from`/`@of` target entity IS the host's base relation: the
|
|
635
|
+
* derived base entity itself, or an ancestor on the base's (or the host's)
|
|
636
|
+
* whole-object extends chain — the legacy `Summary extends Program` projection
|
|
637
|
+
* style inherits the base relation from its super, so `Program.title` on it is
|
|
638
|
+
* a base-relation column, not a join.
|
|
639
|
+
*/
|
|
640
|
+
function _isBaseRelationTarget(target: MetaData, base: MetaData, host: MetaData): boolean {
|
|
641
|
+
for (let cur: MetaData | undefined = base; cur !== undefined; cur = cur.superResolved) {
|
|
642
|
+
if (cur === target) return true;
|
|
643
|
+
}
|
|
644
|
+
for (let cur: MetaData | undefined = host; cur !== undefined; cur = cur.superResolved) {
|
|
645
|
+
if (cur === target) return true;
|
|
646
|
+
}
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Single-hop-unique `@via` inference (ADR-0029 decision 5): scan the base
|
|
652
|
+
* entity's EFFECTIVE relationship children for those whose @objectRef resolves
|
|
653
|
+
* to the `@from`/`@of` target entity. Exactly one → the inferred path (the
|
|
654
|
+
* caller proceeds exactly as if `@via` were declared with that relationship).
|
|
655
|
+
* Zero → ERR_INVALID_ORIGIN (cannot infer; multi-hop is always explicit).
|
|
656
|
+
* More than one → ERR_AMBIGUOUS_PATH naming the candidate relationships.
|
|
657
|
+
*
|
|
658
|
+
* Inference stops at single-hop-unique deliberately: the algorithm is part of
|
|
659
|
+
* the cross-port conformance contract; graph search is not trivially portable.
|
|
660
|
+
*/
|
|
661
|
+
function _inferViaSingleHop(
|
|
662
|
+
base: MetaData,
|
|
663
|
+
targetEntity: MetaData,
|
|
664
|
+
obj: MetaData,
|
|
665
|
+
fieldName: string,
|
|
666
|
+
fromAttr: string,
|
|
667
|
+
label: string,
|
|
668
|
+
originSource: ErrorSource,
|
|
669
|
+
errors: ParseError[],
|
|
670
|
+
): MetaData[] | undefined {
|
|
671
|
+
const candidates = base
|
|
672
|
+
.children()
|
|
673
|
+
.filter((c) => c.type === TYPE_RELATIONSHIP)
|
|
674
|
+
.filter((rel) => {
|
|
675
|
+
const ref = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF);
|
|
676
|
+
return typeof ref === "string" && stripPackage(ref) === targetEntity.name;
|
|
677
|
+
});
|
|
678
|
+
// FR5d — referrer is `<host-FQN>::<fieldName>`, target is the from/of ref
|
|
679
|
+
// whose implicit path could not be resolved.
|
|
680
|
+
const referrer = `${obj.fqn()}::${fieldName}`;
|
|
681
|
+
if (candidates.length === 1) return [candidates[0] as MetaData];
|
|
682
|
+
if (candidates.length === 0) {
|
|
683
|
+
errors.push(
|
|
684
|
+
new ParseError(
|
|
685
|
+
`${label} "${fromAttr}" on ${obj.name}.${fieldName}: no @via and no single-hop relationship from base ` +
|
|
686
|
+
`entity "${base.name}" to "${targetEntity.name}" — cannot infer the path. Declare @via explicitly ` +
|
|
687
|
+
`(multi-hop paths are always explicit; ADR-0029).`,
|
|
688
|
+
{
|
|
689
|
+
code: "ERR_INVALID_ORIGIN",
|
|
690
|
+
source: resolvedSource(originSource, referrer, fromAttr),
|
|
691
|
+
},
|
|
692
|
+
),
|
|
693
|
+
);
|
|
694
|
+
return undefined;
|
|
695
|
+
}
|
|
696
|
+
const names = candidates.map((r) => `"${r.name}"`).join(", ");
|
|
697
|
+
errors.push(
|
|
698
|
+
new ParseError(
|
|
699
|
+
`${label} "${fromAttr}" on ${obj.name}.${fieldName}: no @via and ${candidates.length} relationships from ` +
|
|
700
|
+
`base entity "${base.name}" to "${targetEntity.name}" (${names}) — ambiguous. Declare @via naming one of them (ADR-0029).`,
|
|
701
|
+
{
|
|
702
|
+
code: "ERR_AMBIGUOUS_PATH",
|
|
703
|
+
source: resolvedSource(originSource, referrer, fromAttr),
|
|
704
|
+
},
|
|
705
|
+
),
|
|
706
|
+
);
|
|
707
|
+
return undefined;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* ADR-0029 decision 6 — a passthrough via-path must be effectively to-one at
|
|
712
|
+
* EVERY hop. A hop is judged to-many only when it DECLARES `@cardinality:
|
|
713
|
+
* "many"`: @cardinality is an open string at the metamodel level (Java-
|
|
714
|
+
* canonical composite forms exist, and legacy fixtures omit it), so an
|
|
715
|
+
* absent/unknown cardinality is never misjudged.
|
|
716
|
+
*/
|
|
717
|
+
function _checkPassthroughCardinality(
|
|
718
|
+
hops: readonly MetaData[],
|
|
719
|
+
obj: MetaData,
|
|
720
|
+
fieldName: string,
|
|
721
|
+
originSource: ErrorSource,
|
|
722
|
+
errors: ParseError[],
|
|
723
|
+
): void {
|
|
724
|
+
for (const rel of hops) {
|
|
725
|
+
if (_hopCardinality(rel) === CARDINALITY_MANY) {
|
|
726
|
+
errors.push(
|
|
727
|
+
new ParseError(
|
|
728
|
+
`origin.passthrough on ${obj.name}.${fieldName}: @via hop "${rel.name}" is to-many ` +
|
|
729
|
+
`(@cardinality "${CARDINALITY_MANY}") — a row-multiplying passthrough — you meant aggregate (ADR-0029).`,
|
|
730
|
+
{ code: "ERR_ORIGIN_CARDINALITY", source: originSource },
|
|
731
|
+
),
|
|
732
|
+
);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* ADR-0029 decision 6 — an aggregate via-path must contain at least one
|
|
740
|
+
* to-many hop. Conservative on the open @cardinality vocabulary: the error
|
|
741
|
+
* fires only when the path is PROVABLY to-one (every hop declares
|
|
742
|
+
* `@cardinality: "one"`); absent/composite cardinalities are not judged.
|
|
743
|
+
*/
|
|
744
|
+
function _checkAggregateCardinality(
|
|
745
|
+
hops: readonly MetaData[],
|
|
746
|
+
obj: MetaData,
|
|
747
|
+
fieldName: string,
|
|
748
|
+
originSource: ErrorSource,
|
|
749
|
+
errors: ParseError[],
|
|
750
|
+
): void {
|
|
751
|
+
if (hops.length === 0) return;
|
|
752
|
+
const provablyToOne = hops.every((rel) => _hopCardinality(rel) === CARDINALITY_ONE);
|
|
753
|
+
if (provablyToOne) {
|
|
754
|
+
errors.push(
|
|
755
|
+
new ParseError(
|
|
756
|
+
`origin.aggregate on ${obj.name}.${fieldName}: every @via hop is to-one (@cardinality "${CARDINALITY_ONE}") — ` +
|
|
757
|
+
`aggregating over a to-one path — you meant passthrough (ADR-0029).`,
|
|
758
|
+
{ code: "ERR_ORIGIN_CARDINALITY", source: originSource },
|
|
759
|
+
),
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* FR-024 B6 (spec §4; ADR-0029 decision 7) — extends/origin agreement.
|
|
766
|
+
*
|
|
767
|
+
* When a field declares BOTH an entity-nested `extends` (shape lineage) and
|
|
768
|
+
* an `origin.passthrough` @from (data lineage), the two are independent
|
|
769
|
+
* statements that must coincide: the resolved @from target must be THE SAME
|
|
770
|
+
* NODE as the field's resolved extends target — or appear on its extends
|
|
771
|
+
* chain (a projection field extending another projection's field that
|
|
772
|
+
* ultimately extends the entity field still agrees). Host-agnostic: applies
|
|
773
|
+
* on projections, entities, and values (FR-004 payloads may carry both).
|
|
774
|
+
*
|
|
775
|
+
* NOT judged:
|
|
776
|
+
* - `origin.aggregate` (it computes something new — no passthrough claim);
|
|
777
|
+
* - an extends whose target is a TOP-LEVEL abstract field (its parent is
|
|
778
|
+
* not an object) — shape-only reuse makes no lineage claim.
|
|
779
|
+
*/
|
|
780
|
+
function _checkExtendsOriginAgreement(
|
|
781
|
+
field: MetaData,
|
|
782
|
+
fromField: MetaData,
|
|
783
|
+
fromAttr: string,
|
|
784
|
+
obj: MetaData,
|
|
785
|
+
originSource: ErrorSource,
|
|
786
|
+
errors: ParseError[],
|
|
787
|
+
): void {
|
|
788
|
+
const sup = field.superResolved;
|
|
789
|
+
if (sup === undefined || sup.type !== TYPE_FIELD) return;
|
|
790
|
+
const supOwner = sup.parent;
|
|
791
|
+
if (supOwner === undefined || supOwner.type !== TYPE_OBJECT) return;
|
|
792
|
+
for (let cur: MetaData | undefined = sup; cur !== undefined; cur = cur.superResolved) {
|
|
793
|
+
if (cur === fromField) return; // shape lineage and data lineage agree
|
|
794
|
+
}
|
|
795
|
+
// FR5d resolved envelope: referrer = host::field, target = the @from ref.
|
|
796
|
+
errors.push(
|
|
797
|
+
new ParseError(
|
|
798
|
+
`origin.passthrough on ${obj.name}.${field.name}: @from "${fromAttr}" disagrees with the field's extends ` +
|
|
799
|
+
`target "${supOwner.name}.${sup.name}" — extends (shape lineage) and origin.passthrough (data lineage) ` +
|
|
800
|
+
`must point at the same entity field (FR-024).`,
|
|
801
|
+
{
|
|
802
|
+
code: "ERR_EXTENDS_ORIGIN_MISMATCH",
|
|
803
|
+
source: resolvedSource(originSource, `${obj.fqn()}::${field.name}`, fromAttr),
|
|
804
|
+
},
|
|
805
|
+
),
|
|
806
|
+
);
|
|
426
807
|
}
|
|
427
808
|
|
|
428
809
|
export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
429
810
|
const errors: ParseError[] = [];
|
|
430
811
|
for (const obj of root.ownChildren().filter((c) => c.type === TYPE_OBJECT)) {
|
|
812
|
+
// FR-024 B5: object.value hosts are EXEMPT from @via inference and
|
|
813
|
+
// cardinality checks — a value's origin.passthrough is FR-015 parameter
|
|
814
|
+
// lineage (values are constructed, never assembled; spec §7), not an
|
|
815
|
+
// assembly path. Their @from refs are still resolution-validated.
|
|
816
|
+
const isValueHost = obj.subType === OBJECT_SUBTYPE_VALUE;
|
|
431
817
|
for (const field of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
432
818
|
for (const origin of field.ownChildren().filter((c) => c.type === TYPE_ORIGIN)) {
|
|
433
819
|
if (origin.subType === ORIGIN_SUBTYPE_PASSTHROUGH) {
|
|
@@ -443,10 +829,32 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
443
829
|
);
|
|
444
830
|
continue;
|
|
445
831
|
}
|
|
446
|
-
_validateFromPath(from, root, obj, field.name, origin.source, errors);
|
|
832
|
+
const fromTarget = _validateFromPath(from, root, obj, field.name, origin.source, errors);
|
|
833
|
+
// FR-024 B6 — extends/origin agreement (host-agnostic; runs whether
|
|
834
|
+
// @via is explicit, inferred, or a base-relation column).
|
|
835
|
+
if (fromTarget !== undefined) {
|
|
836
|
+
_checkExtendsOriginAgreement(field, fromTarget.field, from, obj, origin.source, errors);
|
|
837
|
+
}
|
|
447
838
|
const via = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_VIA);
|
|
448
839
|
if (typeof via === "string" && via !== "") {
|
|
449
|
-
_validateViaPath(via, root, obj, field.name, origin.source, errors);
|
|
840
|
+
const hops = _validateViaPath(via, root, obj, field.name, origin.source, errors);
|
|
841
|
+
if (hops !== undefined) {
|
|
842
|
+
_checkPassthroughCardinality(hops, obj, field.name, origin.source, errors);
|
|
843
|
+
}
|
|
844
|
+
} else if (fromTarget !== undefined && !isValueHost) {
|
|
845
|
+
// FR-024 §6 — no @via: derive the base entity; a @from targeting
|
|
846
|
+
// the base relation itself is a plain base column (no checks);
|
|
847
|
+
// otherwise infer the single-hop-unique path and gate cardinality.
|
|
848
|
+
const base = _deriveBaseEntity(obj, root, field.name, origin.source, errors);
|
|
849
|
+
if (base !== undefined && !_isBaseRelationTarget(fromTarget.entity, base, obj)) {
|
|
850
|
+
const hops = _inferViaSingleHop(
|
|
851
|
+
base, fromTarget.entity, obj, field.name, from,
|
|
852
|
+
"origin.passthrough.@from", origin.source, errors,
|
|
853
|
+
);
|
|
854
|
+
if (hops !== undefined) {
|
|
855
|
+
_checkPassthroughCardinality(hops, obj, field.name, origin.source, errors);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
450
858
|
}
|
|
451
859
|
} else if (origin.subType === ORIGIN_SUBTYPE_AGGREGATE) {
|
|
452
860
|
const of_ = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_OF);
|
|
@@ -459,9 +867,34 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
459
867
|
);
|
|
460
868
|
continue;
|
|
461
869
|
}
|
|
462
|
-
|
|
870
|
+
// NOTE (FR-024 B6): NO extends/origin agreement check on aggregates —
|
|
871
|
+
// an aggregate computes something new (count/sum/…); spec §4 defines
|
|
872
|
+
// agreement for passthrough only.
|
|
873
|
+
const ofTarget = _validateFromPath(of_, root, obj, field.name, origin.source, errors, "origin.aggregate.@of");
|
|
463
874
|
const via = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_VIA);
|
|
464
|
-
if (typeof via
|
|
875
|
+
if (typeof via === "string" && via !== "") {
|
|
876
|
+
const hops = _validateViaPath(via, root, obj, field.name, origin.source, errors);
|
|
877
|
+
if (hops !== undefined) {
|
|
878
|
+
_checkAggregateCardinality(hops, obj, field.name, origin.source, errors);
|
|
879
|
+
}
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
// FR-024 §6 — no @via on an aggregate: inference applies only when
|
|
883
|
+
// @of targets a non-base entity from a non-value host; an aggregate
|
|
884
|
+
// over the base relation itself still requires an explicit path.
|
|
885
|
+
if (ofTarget === undefined) continue; // @of did not resolve — no inference to attempt
|
|
886
|
+
if (isValueHost) {
|
|
887
|
+
errors.push(
|
|
888
|
+
new ParseError(
|
|
889
|
+
`origin.aggregate on ${obj.name}.${field.name}: missing @via (aggregates require a relationship path).`,
|
|
890
|
+
{ code: "ERR_INVALID_ORIGIN", source: origin.source },
|
|
891
|
+
),
|
|
892
|
+
);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
const base = _deriveBaseEntity(obj, root, field.name, origin.source, errors);
|
|
896
|
+
if (base === undefined) continue; // base underivable — error already pushed
|
|
897
|
+
if (_isBaseRelationTarget(ofTarget.entity, base, obj)) {
|
|
465
898
|
errors.push(
|
|
466
899
|
new ParseError(
|
|
467
900
|
`origin.aggregate on ${obj.name}.${field.name}: missing @via (aggregates require a relationship path).`,
|
|
@@ -470,7 +903,13 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
470
903
|
);
|
|
471
904
|
continue;
|
|
472
905
|
}
|
|
473
|
-
|
|
906
|
+
const hops = _inferViaSingleHop(
|
|
907
|
+
base, ofTarget.entity, obj, field.name, of_,
|
|
908
|
+
"origin.aggregate.@of", origin.source, errors,
|
|
909
|
+
);
|
|
910
|
+
if (hops !== undefined) {
|
|
911
|
+
_checkAggregateCardinality(hops, obj, field.name, origin.source, errors);
|
|
912
|
+
}
|
|
474
913
|
}
|
|
475
914
|
}
|
|
476
915
|
}
|
|
@@ -479,34 +918,94 @@ export function validateOriginPaths(root: MetaData): ParseError[] {
|
|
|
479
918
|
}
|
|
480
919
|
|
|
481
920
|
// ---------------------------------------------------------------------------
|
|
482
|
-
//
|
|
921
|
+
// FR-024 B6 — derived-field providability (spec §7 population doctrine).
|
|
922
|
+
//
|
|
923
|
+
// An object.ENTITY field carrying any origin.* child is derived (read-only):
|
|
924
|
+
// it does not exist on the writable table — something must PROVIDE it on
|
|
925
|
+
// read. The spec §7 multi-source pattern is a writable table-primary source
|
|
926
|
+
// plus a read-only-kind source (view / materializedView / storedProc /
|
|
927
|
+
// tableFunction, e.g. @role "replica") that carries the derived fields.
|
|
928
|
+
// An entity whose only sources are writable kinds — or that has no source at
|
|
929
|
+
// all — cannot provide an origin-bearing field → ERR_DERIVED_FIELD_NO_READ_SOURCE
|
|
930
|
+
// on that field (plain node-source envelope).
|
|
931
|
+
//
|
|
932
|
+
// Exemptions:
|
|
933
|
+
// - object.projection — the projection's own source/wire IS the provider;
|
|
934
|
+
// - object.value — FR-015 lineage; values are constructed, never populated.
|
|
935
|
+
//
|
|
936
|
+
// The rule reads "at least one read-only-kind source, any role". Since the
|
|
937
|
+
// B4b hard cutover (ERR_ENTITY_PRIMARY_SOURCE_READONLY makes a read-only-kind
|
|
938
|
+
// PRIMARY illegal on entities), the only loadable satisfying shape is the
|
|
939
|
+
// strict one — a non-primary-role read-only source (the §7 multi-source
|
|
940
|
+
// pattern: table primary + view replica).
|
|
483
941
|
//
|
|
484
|
-
//
|
|
485
|
-
//
|
|
486
|
-
//
|
|
942
|
+
// Sources are scanned on the EFFECTIVE child view (children()) so an entity
|
|
943
|
+
// inheriting its sources from an abstract base is judged by what it actually
|
|
944
|
+
// has; fields + origins use the own view, mirroring validateOriginPaths.
|
|
945
|
+
// ---------------------------------------------------------------------------
|
|
946
|
+
|
|
947
|
+
export function validateDerivedFieldProvidability(root: MetaData): ParseError[] {
|
|
948
|
+
const errors: ParseError[] = [];
|
|
949
|
+
for (const obj of root
|
|
950
|
+
.ownChildren()
|
|
951
|
+
.filter((c) => c.type === TYPE_OBJECT && c.subType === OBJECT_SUBTYPE_ENTITY)) {
|
|
952
|
+
const hasReadCapableSource = obj
|
|
953
|
+
.children()
|
|
954
|
+
.filter((c) => c.type === TYPE_SOURCE)
|
|
955
|
+
.some((s) => (s as MetaSource).isReadOnly());
|
|
956
|
+
if (hasReadCapableSource) continue;
|
|
957
|
+
for (const field of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
958
|
+
if (!field.ownChildren().some((c) => c.type === TYPE_ORIGIN)) continue;
|
|
959
|
+
errors.push(
|
|
960
|
+
new ParseError(
|
|
961
|
+
`derived field "${obj.name}.${field.name}" carries an origin.* but entity "${obj.name}" declares no ` +
|
|
962
|
+
`read-capable source — derived fields do not exist on the writable table. Declare a read-only source ` +
|
|
963
|
+
`(e.g. source.rdb @kind "view" @role "replica") to provide it, or move the field to an object.projection (FR-024 §7).`,
|
|
964
|
+
{ code: "ERR_DERIVED_FIELD_NO_READ_SOURCE", source: field.source },
|
|
965
|
+
),
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return errors;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// ---------------------------------------------------------------------------
|
|
973
|
+
// field.object + @storage cross-attribute validation
|
|
974
|
+
//
|
|
975
|
+
// Rules (ADR-0013):
|
|
976
|
+
// 1. A field.object ALWAYS requires @objectRef. A field.object models a typed
|
|
977
|
+
// nested value; without @objectRef it is "an oxymoron at the logical layer".
|
|
978
|
+
// Genuinely open/untyped JSON uses the physical @dbColumnType: jsonb escape
|
|
979
|
+
// hatch on field.string, NOT a bare object. → ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF.
|
|
980
|
+
// (This rule subsumes the legacy @storage-without-@objectRef check —
|
|
981
|
+
// @storage is only meaningful on a field.object, so the missing-@objectRef
|
|
982
|
+
// situation now always reports this single, clearer error. One error per
|
|
983
|
+
// node: when @objectRef is absent we skip the flattened/array check below.)
|
|
487
984
|
// 2. @storage "flattened" requires isArray to be absent or false (cannot
|
|
488
985
|
// flatten a variable-length array into a fixed column set).
|
|
489
|
-
//
|
|
490
|
-
// Only field.object nodes carry @storage in practice, but the check is applied
|
|
491
|
-
// to every field node that has @storage set — matching the permissive "check
|
|
492
|
-
// what's there" model used by the other validation passes.
|
|
493
986
|
// ---------------------------------------------------------------------------
|
|
494
987
|
|
|
495
988
|
export function validateFieldObjectStorage(root: MetaData): ParseError[] {
|
|
496
989
|
const errors: ParseError[] = [];
|
|
497
990
|
for (const obj of root.ownChildren().filter((c) => c.type === TYPE_OBJECT)) {
|
|
498
991
|
for (const field of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
499
|
-
const storage = field.ownAttr(FIELD_ATTR_STORAGE);
|
|
500
|
-
if (storage === undefined || storage === null) continue;
|
|
501
992
|
const objectRef = field.ownAttr(FIELD_ATTR_OBJECT_REF);
|
|
502
|
-
|
|
993
|
+
const hasObjectRef = typeof objectRef === "string" && objectRef.length > 0;
|
|
994
|
+
|
|
995
|
+
if (field.subType === FIELD_SUBTYPE_OBJECT && !hasObjectRef) {
|
|
996
|
+
// A field.object with no @objectRef is rejected outright; reporting any
|
|
997
|
+
// further @storage error on the same node would be redundant.
|
|
503
998
|
errors.push(
|
|
504
999
|
new ParseError(
|
|
505
|
-
`field "${obj.name}.${field.name}"
|
|
506
|
-
{ code: "
|
|
1000
|
+
`field.object "${obj.name}.${field.name}" has no @objectRef; a field.object requires @objectRef. For an open/untyped JSON map use @dbColumnType: jsonb on a field.string instead of a bare object.`,
|
|
1001
|
+
{ code: "ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF", source: field.source },
|
|
507
1002
|
),
|
|
508
1003
|
);
|
|
1004
|
+
continue;
|
|
509
1005
|
}
|
|
1006
|
+
|
|
1007
|
+
const storage = field.ownAttr(FIELD_ATTR_STORAGE);
|
|
1008
|
+
if (storage === undefined || storage === null) continue;
|
|
510
1009
|
if (storage === STORAGE_FLATTENED && field.isArray === true) {
|
|
511
1010
|
errors.push(
|
|
512
1011
|
new ParseError(
|
|
@@ -646,6 +1145,163 @@ export function validateDataGridFilterValues(root: MetaData): ParseError[] {
|
|
|
646
1145
|
return errors;
|
|
647
1146
|
}
|
|
648
1147
|
|
|
1148
|
+
// ---------------------------------------------------------------------------
|
|
1149
|
+
// FR-017 — M:N relationship validation (slim vocabulary)
|
|
1150
|
+
//
|
|
1151
|
+
// Deferred-resolution validation (runs after all files load + extends:
|
|
1152
|
+
// resolution, like origin paths), enforcing the cross-port M:N contract:
|
|
1153
|
+
//
|
|
1154
|
+
// (a) @symmetric:true is valid only on a self-join (@objectRef == declaring
|
|
1155
|
+
// entity). Otherwise ERR_BAD_ATTR_VALUE.
|
|
1156
|
+
// (b) @symmetric and @sourceRefField are mutually exclusive → ERR_BAD_ATTR_VALUE.
|
|
1157
|
+
// (c) When @through is present: the named entity must exist and declare exactly
|
|
1158
|
+
// two identity.reference children; @sourceRefField (if present) must match
|
|
1159
|
+
// one of those references' FK fields → ERR_INVALID_RELATIONSHIP.
|
|
1160
|
+
// (d) @through / @sourceRefField / @symmetric are invalid on a non-M:N
|
|
1161
|
+
// relationship (@cardinality != "many", or no @through) → ERR_INVALID_RELATIONSHIP.
|
|
1162
|
+
//
|
|
1163
|
+
// Own-relationships only: a relationship is validated on the entity that declares
|
|
1164
|
+
// it (matching the own-attrs policy of the other passes).
|
|
1165
|
+
// ---------------------------------------------------------------------------
|
|
1166
|
+
|
|
1167
|
+
// The junction's reference view: the validator and the runtime/codegen FK
|
|
1168
|
+
// derivation (deriveM2MFields) MUST agree on which references count. Both use
|
|
1169
|
+
// the EFFECTIVE view (own + inherited via extends) via referenceIdentities(),
|
|
1170
|
+
// so a junction defined through `extends` is treated identically here and at
|
|
1171
|
+
// resolution time. (For a junction with no extends, effective == own.)
|
|
1172
|
+
function _junctionReferences(junction: MetaData): MetaReferenceIdentity[] {
|
|
1173
|
+
return (junction as MetaObject).referenceIdentities();
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/** FK field names declared by a junction's effective identity.reference children. */
|
|
1177
|
+
function _junctionReferenceFkFields(junction: MetaData): string[] {
|
|
1178
|
+
const out: string[] = [];
|
|
1179
|
+
for (const ref of _junctionReferences(junction)) {
|
|
1180
|
+
const first = ref.fields[0];
|
|
1181
|
+
if (first) out.push(first);
|
|
1182
|
+
}
|
|
1183
|
+
return out;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function _countJunctionReferences(junction: MetaData): number {
|
|
1187
|
+
return _junctionReferences(junction).length;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
export function validateRelationships(root: MetaData): ParseError[] {
|
|
1191
|
+
const errors: ParseError[] = [];
|
|
1192
|
+
for (const obj of root.ownChildren().filter((c) => c.type === TYPE_OBJECT)) {
|
|
1193
|
+
for (const rel of obj.ownChildren().filter((c) => c.type === TYPE_RELATIONSHIP)) {
|
|
1194
|
+
const through = rel.ownAttr(RELATIONSHIP_ATTR_THROUGH);
|
|
1195
|
+
const sourceRefField = rel.ownAttr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
1196
|
+
const symmetric = rel.ownAttr(RELATIONSHIP_ATTR_SYMMETRIC) === true;
|
|
1197
|
+
const cardinality = rel.ownAttr(RELATIONSHIP_ATTR_CARDINALITY);
|
|
1198
|
+
const objectRef = rel.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF);
|
|
1199
|
+
|
|
1200
|
+
const hasThrough = typeof through === "string" && through !== "";
|
|
1201
|
+
const hasSourceRefField = typeof sourceRefField === "string" && sourceRefField !== "";
|
|
1202
|
+
const isMany = cardinality === CARDINALITY_MANY;
|
|
1203
|
+
const isM2M = hasThrough && isMany;
|
|
1204
|
+
|
|
1205
|
+
// NOTE: @objectRef existence resolution moved to the validation registry
|
|
1206
|
+
// (defaultValidationRegistry → a declarative reference descriptor). The M:N
|
|
1207
|
+
// slim-vocabulary rules below stay here for now (Phase 3 migrates them too).
|
|
1208
|
+
|
|
1209
|
+
// Rule (d): M:N-only attrs on a non-M:N relationship.
|
|
1210
|
+
if (!isM2M) {
|
|
1211
|
+
if (hasThrough) {
|
|
1212
|
+
errors.push(
|
|
1213
|
+
new ParseError(
|
|
1214
|
+
`relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_THROUGH} but is not a M:N ` +
|
|
1215
|
+
`relationship (requires @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_MANY}").`,
|
|
1216
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
1217
|
+
),
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
if (hasSourceRefField) {
|
|
1221
|
+
errors.push(
|
|
1222
|
+
new ParseError(
|
|
1223
|
+
`relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is not a M:N relationship.`,
|
|
1224
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
1225
|
+
),
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
if (symmetric) {
|
|
1229
|
+
errors.push(
|
|
1230
|
+
new ParseError(
|
|
1231
|
+
`relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but is not a M:N relationship.`,
|
|
1232
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
1233
|
+
),
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Rule (b): @symmetric and @sourceRefField are mutually exclusive.
|
|
1240
|
+
if (symmetric && hasSourceRefField) {
|
|
1241
|
+
errors.push(
|
|
1242
|
+
new ParseError(
|
|
1243
|
+
`relationship "${obj.name}.${rel.name}" sets both @${RELATIONSHIP_ATTR_SYMMETRIC} and ` +
|
|
1244
|
+
`@${RELATIONSHIP_ATTR_SOURCE_REF_FIELD}; they are mutually exclusive.`,
|
|
1245
|
+
{ code: "ERR_BAD_ATTR_VALUE", source: rel.source },
|
|
1246
|
+
),
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Rule (a): @symmetric is valid only on a self-join (@objectRef == declaring entity).
|
|
1251
|
+
const isSelfJoin = typeof objectRef === "string" && stripPackage(objectRef) === obj.name;
|
|
1252
|
+
if (symmetric && !isSelfJoin) {
|
|
1253
|
+
errors.push(
|
|
1254
|
+
new ParseError(
|
|
1255
|
+
`relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but @${RELATIONSHIP_ATTR_OBJECT_REF} ` +
|
|
1256
|
+
`"${String(objectRef)}" is not the declaring entity "${obj.name}"; @${RELATIONSHIP_ATTR_SYMMETRIC} is self-join-only.`,
|
|
1257
|
+
{ code: "ERR_BAD_ATTR_VALUE", source: rel.source },
|
|
1258
|
+
),
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
// Rule (c): @through must name an entity declaring exactly two identity.reference children.
|
|
1263
|
+
const junction = _findObject(root, through as string);
|
|
1264
|
+
if (!junction) {
|
|
1265
|
+
errors.push(
|
|
1266
|
+
new ParseError(
|
|
1267
|
+
`relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" does not resolve to an entity.`,
|
|
1268
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${obj.fqn()}::${rel.name}`, String(through)) },
|
|
1269
|
+
),
|
|
1270
|
+
);
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const refCount = _countJunctionReferences(junction);
|
|
1274
|
+
if (refCount !== 2) {
|
|
1275
|
+
errors.push(
|
|
1276
|
+
new ParseError(
|
|
1277
|
+
`relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" must declare exactly two ` +
|
|
1278
|
+
`identity.reference children (one per FK side); found ${refCount}.`,
|
|
1279
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
1280
|
+
),
|
|
1281
|
+
);
|
|
1282
|
+
continue;
|
|
1283
|
+
}
|
|
1284
|
+
// @sourceRefField (if present) must match one of the junction's reference FK fields.
|
|
1285
|
+
if (hasSourceRefField) {
|
|
1286
|
+
const fkFields = _junctionReferenceFkFields(junction);
|
|
1287
|
+
if (!fkFields.includes(sourceRefField as string)) {
|
|
1288
|
+
errors.push(
|
|
1289
|
+
new ParseError(
|
|
1290
|
+
`relationship "${obj.name}.${rel.name}" @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} "${sourceRefField}" does not match ` +
|
|
1291
|
+
`any identity.reference FK field on junction "${through}". Available: ${fkFields.join(", ") || "(none)"}.`,
|
|
1292
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
1293
|
+
),
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
return errors;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// NOTE: identity.reference @references resolution moved to the validation registry
|
|
1303
|
+
// (defaultValidationRegistry → a declarative reference descriptor with dottedFieldPath).
|
|
1304
|
+
|
|
649
1305
|
function checkFilterClauses(
|
|
650
1306
|
filter: Record<string, unknown>,
|
|
651
1307
|
allow: Map<string, readonly string[]>,
|