@metaobjectsdev/metadata 1.0.3 → 1.0.4
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/core/relationship/derive-m2m-fields.d.ts +3 -1
- package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.js +110 -15
- package/dist/core/relationship/derive-m2m-fields.js.map +1 -1
- package/dist/core/relationship/find-reference.d.ts +21 -5
- package/dist/core/relationship/find-reference.d.ts.map +1 -1
- package/dist/core/relationship/find-reference.js +29 -10
- package/dist/core/relationship/find-reference.js.map +1 -1
- package/dist/core/relationship/relationship-definition.embedded.js +8 -8
- package/dist/core/relationship/relationship-definition.embedded.js.map +1 -1
- package/dist/core/relationship/resolve-relationship-reference.d.ts +27 -0
- package/dist/core/relationship/resolve-relationship-reference.d.ts.map +1 -0
- package/dist/core/relationship/resolve-relationship-reference.js +84 -0
- package/dist/core/relationship/resolve-relationship-reference.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +10 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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 -1
- package/dist/library/embedded-library.generated.js +11 -1
- package/dist/library/embedded-library.generated.js.map +1 -1
- package/dist/library/index.d.ts +2 -0
- package/dist/library/index.d.ts.map +1 -0
- package/dist/library/index.js +24 -0
- package/dist/library/index.js.map +1 -0
- package/dist/library/library-sources.d.ts +99 -6
- package/dist/library/library-sources.d.ts.map +1 -1
- package/dist/library/library-sources.js +165 -43
- 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 +6 -1
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +2 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +162 -22
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +13 -2
- package/dist/parser-core.js.map +1 -1
- package/dist/vocabulary-rewrite-yaml.d.ts +2 -0
- package/dist/vocabulary-rewrite-yaml.d.ts.map +1 -0
- package/dist/vocabulary-rewrite-yaml.js +22 -0
- package/dist/vocabulary-rewrite-yaml.js.map +1 -0
- package/package.json +7 -7
- package/src/core/relationship/derive-m2m-fields.ts +115 -15
- package/src/core/relationship/find-reference.ts +34 -11
- package/src/core/relationship/relationship-definition.embedded.ts +8 -8
- package/src/core/relationship/resolve-relationship-reference.ts +99 -0
- package/src/errors.ts +10 -0
- package/src/index.ts +6 -1
- package/src/library/embedded-library.generated.ts +12 -1
- package/src/library/index.ts +23 -0
- package/src/library/library-sources.ts +197 -47
- package/src/loader/meta-data-loader.ts +7 -1
- package/src/loader/validation-passes.ts +181 -23
- package/src/parser-core.ts +15 -2
- package/src/vocabulary-rewrite-yaml.ts +21 -0
|
@@ -25,7 +25,7 @@ import { ParseError } from "../errors.js";
|
|
|
25
25
|
import type { LoaderWarning } from "../source.js";
|
|
26
26
|
import { codeSource, resolvedSource } from "../source.js";
|
|
27
27
|
import { parseJson } from "../parser-json.js";
|
|
28
|
-
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter, validateRetiredRequirementLinks } from "./validation-passes.js";
|
|
28
|
+
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateOneSideReferenceResolution, validateIndexLookupFields, validateProjectionFilter, validateRetiredRequirementLinks } from "./validation-passes.js";
|
|
29
29
|
import { runRegisteredValidation } from "./validation-registry.js";
|
|
30
30
|
import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
|
|
31
31
|
import { validateSourceEscapes } from "../persistence/source/validate-source-escapes.js";
|
|
@@ -652,6 +652,12 @@ export class MetaDataLoader {
|
|
|
652
652
|
// are invalid on a 1:N relationship.
|
|
653
653
|
errors.push(...validateRelationships(root));
|
|
654
654
|
|
|
655
|
+
// Rule (e) — #368: a `@cardinality: one` relationship must resolve to
|
|
656
|
+
// exactly one identity.reference when its declaring entity holds more
|
|
657
|
+
// than one onto the same target; an unresolvable case is a load error
|
|
658
|
+
// naming the candidates (ADR-0029 §5).
|
|
659
|
+
errors.push(...validateOneSideReferenceResolution(root));
|
|
660
|
+
|
|
655
661
|
// index.lookup @fields resolution — each index.lookup must name ≥1 field,
|
|
656
662
|
// and every field must exist in the entity's effective (resolved) field set
|
|
657
663
|
// (ADR-0039: resolving accessor, so inherited fields via extends are visible).
|
|
@@ -129,6 +129,11 @@ import {
|
|
|
129
129
|
CARDINALITY_ONE,
|
|
130
130
|
CARDINALITY_MANY,
|
|
131
131
|
} from "../core/relationship/relationship-constants.js";
|
|
132
|
+
import {
|
|
133
|
+
referenceCandidatesFor,
|
|
134
|
+
resolveRelationshipReference,
|
|
135
|
+
} from "../core/relationship/resolve-relationship-reference.js";
|
|
136
|
+
import type { MetaRoot } from "../shared/meta-root.js";
|
|
132
137
|
import { stripPackage } from "../naming.js";
|
|
133
138
|
import {
|
|
134
139
|
FILTER_COMPOSE_OR,
|
|
@@ -1938,11 +1943,29 @@ export function validateDataGridFilterValues(root: MetaData): ParseError[] {
|
|
|
1938
1943
|
// (c) When @through is present: the named entity must exist and declare exactly
|
|
1939
1944
|
// two identity.reference children; @sourceRefField (if present) must match
|
|
1940
1945
|
// one of those references' FK fields → ERR_INVALID_RELATIONSHIP.
|
|
1941
|
-
// (d) @through / @
|
|
1942
|
-
//
|
|
1946
|
+
// (d) @through / @symmetric are invalid on a non-M:N relationship
|
|
1947
|
+
// (@cardinality != "many", or no @through) → ERR_INVALID_RELATIONSHIP.
|
|
1948
|
+
// @sourceRefField is also invalid there, EXCEPT on @cardinality: "one"
|
|
1949
|
+
// (#368: it then names which of several identity.reference nodes onto
|
|
1950
|
+
// the same target the relationship navigates).
|
|
1943
1951
|
//
|
|
1944
|
-
//
|
|
1945
|
-
//
|
|
1952
|
+
// ADR-0039: resolving, not own-only (#368 fix round 3) — every rule above
|
|
1953
|
+
// validates a property of the relationship's OWN declaration (its attrs,
|
|
1954
|
+
// plus for rule (c) the @through target resolved against the root), so a
|
|
1955
|
+
// relationship inherited via extends must be validated wherever it's
|
|
1956
|
+
// visible, or a child entity that only SEES the relationship through
|
|
1957
|
+
// inheritance could carry a violation no pass ever examines. That makes an
|
|
1958
|
+
// inherited, UNMODIFIED relationship visited once per inheriting entity —
|
|
1959
|
+
// checked once below (keyed on the relationship node's own identity), not
|
|
1960
|
+
// once per entity, using the DECLARING entity (rel.parent) rather than
|
|
1961
|
+
// whichever entity's effective view got there first for every piece of
|
|
1962
|
+
// context a rule reads (message text, @through's package per ADR-0042, rule
|
|
1963
|
+
// (a)'s self-join comparison). That keeps each check's result independent of
|
|
1964
|
+
// which entity triggered it, which is what makes "checked once" both
|
|
1965
|
+
// sufficient and correct. An override replaces the node in place
|
|
1966
|
+
// (MetaData._effectiveChildren), so it is never the same object as what it
|
|
1967
|
+
// overrides and is never skipped against it — a genuinely different
|
|
1968
|
+
// declaration is always independently checked.
|
|
1946
1969
|
// ---------------------------------------------------------------------------
|
|
1947
1970
|
|
|
1948
1971
|
// The junction's reference view: the validator and the runtime/codegen FK
|
|
@@ -1970,14 +1993,36 @@ function _countJunctionReferences(junction: MetaData): number {
|
|
|
1970
1993
|
|
|
1971
1994
|
export function validateRelationships(root: MetaData): ParseError[] {
|
|
1972
1995
|
const errors: ParseError[] = [];
|
|
1996
|
+
// #368 fix round 3 — the outer loop below is now resolving (obj.relationships()),
|
|
1997
|
+
// so a relationship inherited unmodified by N entities is reached N times. Its
|
|
1998
|
+
// own attrs never change based on who inherits it, so re-validating it more
|
|
1999
|
+
// than once would report the identical finding N times — pure noise. Checked
|
|
2000
|
+
// is keyed on the relationship NODE's own object identity: MetaData._effectiveChildren
|
|
2001
|
+
// reuses the super's child object in place for an unmodified inherited child,
|
|
2002
|
+
// so the same physical declaration IS the same object everywhere it's visible,
|
|
2003
|
+
// while an override replaces it with a genuinely different object (correctly
|
|
2004
|
+
// NOT skipped — a distinct declaration is a distinct finding).
|
|
2005
|
+
const checked = new Set<MetaData>();
|
|
1973
2006
|
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
1974
2007
|
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
1975
|
-
// ADR-
|
|
1976
|
-
|
|
1977
|
-
//
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2008
|
+
// ADR-0039: resolving — see the header comment above: rule (d) (like rule
|
|
2009
|
+
// (e)) must see a relationship inherited via extends, not just this
|
|
2010
|
+
// entity's own declarations. `checked` absorbs the resulting revisits.
|
|
2011
|
+
for (const rel of (obj as MetaObject).relationships()) {
|
|
2012
|
+
if (checked.has(rel)) continue;
|
|
2013
|
+
checked.add(rel);
|
|
2014
|
+
|
|
2015
|
+
// Every rule below validates a property of the relationship's OWN
|
|
2016
|
+
// declaration (its attrs, plus for rule (c) the @through target), so
|
|
2017
|
+
// context — the entity name in messages, the package a bare @through
|
|
2018
|
+
// resolves in (ADR-0042), and rule (a)'s self-join comparison — is
|
|
2019
|
+
// always the entity that DECLARES `rel` (rel.parent), never `obj` (the
|
|
2020
|
+
// entity whose effective view happened to reach it first). This keeps
|
|
2021
|
+
// the check's result independent of iteration order/inheritance depth,
|
|
2022
|
+
// which is what makes checking each node exactly once correct.
|
|
2023
|
+
const declaringEntity = rel.parent ?? obj;
|
|
2024
|
+
const referrerPkg = declaringEntity.package ?? declaringEntity.fileDefaultPackage ?? "";
|
|
2025
|
+
|
|
1981
2026
|
// ADR-0039: resolving — a relationship may inherit its M:N attrs via extends.
|
|
1982
2027
|
const through = rel.attr(RELATIONSHIP_ATTR_THROUGH);
|
|
1983
2028
|
const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
@@ -1999,16 +2044,23 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
1999
2044
|
if (hasThrough) {
|
|
2000
2045
|
errors.push(
|
|
2001
2046
|
new ParseError(
|
|
2002
|
-
`relationship "${
|
|
2047
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_THROUGH} but is not a M:N ` +
|
|
2003
2048
|
`relationship (requires @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_MANY}").`,
|
|
2004
2049
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2005
2050
|
),
|
|
2006
2051
|
);
|
|
2007
2052
|
}
|
|
2008
|
-
|
|
2053
|
+
// #368: @sourceRefField also disambiguates a `@cardinality: one`
|
|
2054
|
+
// relationship when the entity holds more than one identity.reference
|
|
2055
|
+
// onto the same target. Only the M:N *junction* reading is rejected
|
|
2056
|
+
// here; rule (e) — validateOneSideReferenceResolution, below in this
|
|
2057
|
+
// file — checks that it names a real local reference.
|
|
2058
|
+
if (hasSourceRefField && cardinality !== CARDINALITY_ONE) {
|
|
2009
2059
|
errors.push(
|
|
2010
2060
|
new ParseError(
|
|
2011
|
-
`relationship "${
|
|
2061
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} but is neither a M:N ` +
|
|
2062
|
+
`relationship (requires @${RELATIONSHIP_ATTR_THROUGH} with @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_MANY}") ` +
|
|
2063
|
+
`nor a @${RELATIONSHIP_ATTR_CARDINALITY}: "${CARDINALITY_ONE}" relationship.`,
|
|
2012
2064
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2013
2065
|
),
|
|
2014
2066
|
);
|
|
@@ -2016,7 +2068,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2016
2068
|
if (symmetric) {
|
|
2017
2069
|
errors.push(
|
|
2018
2070
|
new ParseError(
|
|
2019
|
-
`relationship "${
|
|
2071
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but is not a M:N relationship.`,
|
|
2020
2072
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2021
2073
|
),
|
|
2022
2074
|
);
|
|
@@ -2028,7 +2080,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2028
2080
|
if (symmetric && hasSourceRefField) {
|
|
2029
2081
|
errors.push(
|
|
2030
2082
|
new ParseError(
|
|
2031
|
-
`relationship "${
|
|
2083
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets both @${RELATIONSHIP_ATTR_SYMMETRIC} and ` +
|
|
2032
2084
|
`@${RELATIONSHIP_ATTR_SOURCE_REF_FIELD}; they are mutually exclusive.`,
|
|
2033
2085
|
{ code: "ERR_BAD_ATTR_VALUE", source: rel.source },
|
|
2034
2086
|
),
|
|
@@ -2040,12 +2092,13 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2040
2092
|
// in this package is self, but an FQN "other::Widget" (a different same-short-
|
|
2041
2093
|
// name entity) is NOT (comparing stripped short names would misclassify it).
|
|
2042
2094
|
const isSelfJoin =
|
|
2043
|
-
typeof objectRef === "string" &&
|
|
2095
|
+
typeof objectRef === "string" &&
|
|
2096
|
+
resolveObjectRef(root, objectRef, referrerPkg).node === declaringEntity;
|
|
2044
2097
|
if (symmetric && !isSelfJoin) {
|
|
2045
2098
|
errors.push(
|
|
2046
2099
|
new ParseError(
|
|
2047
|
-
`relationship "${
|
|
2048
|
-
`"${String(objectRef)}" is not the declaring entity "${
|
|
2100
|
+
`relationship "${declaringEntity.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SYMMETRIC} but @${RELATIONSHIP_ATTR_OBJECT_REF} ` +
|
|
2101
|
+
`"${String(objectRef)}" is not the declaring entity "${declaringEntity.name}"; @${RELATIONSHIP_ATTR_SYMMETRIC} is self-join-only.`,
|
|
2049
2102
|
{ code: "ERR_BAD_ATTR_VALUE", source: rel.source },
|
|
2050
2103
|
),
|
|
2051
2104
|
);
|
|
@@ -2056,8 +2109,8 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2056
2109
|
if (!junction) {
|
|
2057
2110
|
errors.push(
|
|
2058
2111
|
new ParseError(
|
|
2059
|
-
`relationship "${
|
|
2060
|
-
{ code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${
|
|
2112
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" does not resolve to an entity.${didYouMeanHint(root, String(through))}`,
|
|
2113
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: resolvedSource(rel.source, `${declaringEntity.fqn()}::${rel.name}`, String(through)) },
|
|
2061
2114
|
),
|
|
2062
2115
|
);
|
|
2063
2116
|
continue;
|
|
@@ -2069,7 +2122,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2069
2122
|
if (junction.subType !== OBJECT_SUBTYPE_ENTITY) {
|
|
2070
2123
|
errors.push(
|
|
2071
2124
|
new ParseError(
|
|
2072
|
-
`relationship "${
|
|
2125
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" resolves to ` +
|
|
2073
2126
|
`${junction.type}.${junction.subType}, not an entity — a junction is a persisted join table ` +
|
|
2074
2127
|
`and must be object.entity.`,
|
|
2075
2128
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
@@ -2081,7 +2134,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2081
2134
|
if (refCount !== 2) {
|
|
2082
2135
|
errors.push(
|
|
2083
2136
|
new ParseError(
|
|
2084
|
-
`relationship "${
|
|
2137
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_THROUGH} "${through}" must declare exactly two ` +
|
|
2085
2138
|
`identity.reference children (one per FK side); found ${refCount}.`,
|
|
2086
2139
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2087
2140
|
),
|
|
@@ -2094,7 +2147,7 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2094
2147
|
if (!fkFields.includes(sourceRefField as string)) {
|
|
2095
2148
|
errors.push(
|
|
2096
2149
|
new ParseError(
|
|
2097
|
-
`relationship "${
|
|
2150
|
+
`relationship "${declaringEntity.name}.${rel.name}" @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} "${sourceRefField}" does not match ` +
|
|
2098
2151
|
`any identity.reference FK field on junction "${through}". Available: ${fkFields.join(", ") || "(none)"}.`,
|
|
2099
2152
|
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2100
2153
|
),
|
|
@@ -2106,6 +2159,111 @@ export function validateRelationships(root: MetaData): ParseError[] {
|
|
|
2106
2159
|
return errors;
|
|
2107
2160
|
}
|
|
2108
2161
|
|
|
2162
|
+
// ---------------------------------------------------------------------------
|
|
2163
|
+
// Rule (e) — #368: a `@cardinality: one` relationship must resolve to exactly
|
|
2164
|
+
// one identity.reference. Two references onto the same target are
|
|
2165
|
+
// indistinguishable from the relationship's @objectRef alone, so the resolver
|
|
2166
|
+
// would silently emit the first one's FK column. ADR-0029 §5: a second path
|
|
2167
|
+
// is a load error naming the candidates.
|
|
2168
|
+
//
|
|
2169
|
+
// Registered alongside validateRelationships (the M:N slim-vocabulary pass,
|
|
2170
|
+
// above) — same deferred-resolution timing (after all files load + extends
|
|
2171
|
+
// resolution).
|
|
2172
|
+
//
|
|
2173
|
+
// Scope differs deliberately from rule (d) — in SUBJECT, not in which
|
|
2174
|
+
// relationships each pass walks (both walk the EFFECTIVE set; rule (d) is
|
|
2175
|
+
// no longer own-scoped, or an M:N declaration reached only via extends
|
|
2176
|
+
// would go unchecked). Rule (d) validates attrs that travel with the
|
|
2177
|
+
// relationship's OWN declaration (@through/@symmetric/@sourceRefField), so
|
|
2178
|
+
// it checks each declaration EXACTLY ONCE — deduped by node identity, and
|
|
2179
|
+
// reported against the entity that declares it — because those attrs don't
|
|
2180
|
+
// change meaning depending on who inherits the relationship. Rule (e)
|
|
2181
|
+
// instead validates whether THIS entity's reference set resolves the
|
|
2182
|
+
// relationship uniquely, which is a property of the EFFECTIVE entity, not of
|
|
2183
|
+
// wherever the relationship happens to be declared. A child entity that
|
|
2184
|
+
// extends a clean parent and adds a second identity.reference onto the same
|
|
2185
|
+
// target makes an INHERITED relationship ambiguous on the child even though
|
|
2186
|
+
// the parent (and the relationship's own declaration) are untouched — own-
|
|
2187
|
+
// scoping this pass would leave that case unchecked, and codegen/runtime
|
|
2188
|
+
// (which resolve against the effective entity) would silently drop the
|
|
2189
|
+
// relation (#368 fix round 2). If a parent and a child are both genuinely
|
|
2190
|
+
// ambiguous, both are reported — two entities are broken, not one error
|
|
2191
|
+
// duplicated.
|
|
2192
|
+
// ---------------------------------------------------------------------------
|
|
2193
|
+
|
|
2194
|
+
export function validateOneSideReferenceResolution(root: MetaRoot): ParseError[] {
|
|
2195
|
+
const errors: ParseError[] = [];
|
|
2196
|
+
for (const obj of root.objects()) {
|
|
2197
|
+
// ADR-0039: resolving — see the scope note above: rule (e) checks THIS
|
|
2198
|
+
// entity's effective reference set against every relationship it can see,
|
|
2199
|
+
// including one only inherited via extends.
|
|
2200
|
+
for (const rel of obj.relationships()) {
|
|
2201
|
+
// ADR-0039: resolving — @cardinality/@objectRef may be inherited via extends.
|
|
2202
|
+
if (rel.attr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_ONE) continue;
|
|
2203
|
+
const objectRef = rel.attr(RELATIONSHIP_ATTR_OBJECT_REF);
|
|
2204
|
+
if (typeof objectRef !== "string" || objectRef === "") continue;
|
|
2205
|
+
|
|
2206
|
+
const candidates = referenceCandidatesFor(obj, objectRef);
|
|
2207
|
+
|
|
2208
|
+
const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
2209
|
+
const declared = typeof sourceRefField === "string" && sourceRefField !== ""
|
|
2210
|
+
? sourceRefField
|
|
2211
|
+
: undefined;
|
|
2212
|
+
|
|
2213
|
+
if (declared !== undefined) {
|
|
2214
|
+
// A declared @sourceRefField short-circuits the ladder at ANY
|
|
2215
|
+
// candidate count — checked independently of resolveRelationshipReference,
|
|
2216
|
+
// whose step 1 ("exactly one candidate -> that one") would otherwise
|
|
2217
|
+
// silently return the lone candidate even when it disagrees with the
|
|
2218
|
+
// declared field. The author named a specific FK; it must exist,
|
|
2219
|
+
// whether there are zero, one, or many candidates.
|
|
2220
|
+
const matchesDeclared = candidates.some((c) => c.fields[0] === declared);
|
|
2221
|
+
if (matchesDeclared) continue;
|
|
2222
|
+
errors.push(
|
|
2223
|
+
new ParseError(
|
|
2224
|
+
`relationship "${obj.name}.${rel.name}" sets @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} ` +
|
|
2225
|
+
`"${declared}", which names no identity.reference targeting "${objectRef}". ` +
|
|
2226
|
+
`Candidates: ${formatReferenceCandidates(candidates)}.`,
|
|
2227
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2228
|
+
),
|
|
2229
|
+
);
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// No @sourceRefField declared: ambiguity only exists with 2+ candidates —
|
|
2234
|
+
// resolveRelationshipReference's name-pairing step (ladder step 3) decides.
|
|
2235
|
+
if (candidates.length <= 1) continue;
|
|
2236
|
+
const resolved = resolveRelationshipReference(obj, rel.name, objectRef);
|
|
2237
|
+
if (resolved) continue;
|
|
2238
|
+
|
|
2239
|
+
errors.push(
|
|
2240
|
+
new ParseError(
|
|
2241
|
+
`relationship "${obj.name}.${rel.name}" is ambiguous: "${obj.name}" declares ` +
|
|
2242
|
+
`${candidates.length} identity.reference nodes targeting "${objectRef}" and the ` +
|
|
2243
|
+
`relationship name does not pair with exactly one. Candidates: ${formatReferenceCandidates(candidates)}. ` +
|
|
2244
|
+
`Set @${RELATIONSHIP_ATTR_SOURCE_REF_FIELD} to the FK field this relationship navigates.`,
|
|
2245
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2246
|
+
),
|
|
2247
|
+
);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
return errors;
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
/**
|
|
2254
|
+
* Render a candidate reference as `name(fkField)`, or `name(fieldA, fieldB)`
|
|
2255
|
+
* for a composite reference — so two composite references sharing a first
|
|
2256
|
+
* column (e.g. both starting `tenantId`) still print distinguishably.
|
|
2257
|
+
*
|
|
2258
|
+
* NOTE: this is display only. Matching (both here and in
|
|
2259
|
+
* resolveRelationshipReference) still keys on `fields[0]` alone — a
|
|
2260
|
+
* composite reference cannot actually be disambiguated by @sourceRefField.
|
|
2261
|
+
* That's a documented limitation, not fixed by this rendering change.
|
|
2262
|
+
*/
|
|
2263
|
+
function formatReferenceCandidates(candidates: readonly MetaReferenceIdentity[]): string {
|
|
2264
|
+
return candidates.map((c) => `${c.name}(${c.fields.join(", ")})`).join(", ");
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2109
2267
|
// NOTE: identity.reference @references resolution moved to the validation registry
|
|
2110
2268
|
// (defaultValidationRegistry → a declarative reference descriptor with dottedFieldPath).
|
|
2111
2269
|
|
package/src/parser-core.ts
CHANGED
|
@@ -945,7 +945,18 @@ function parseNodeInto(
|
|
|
945
945
|
// "merged"` envelope. Last-writer-wins is preserved for non-conflicting
|
|
946
946
|
// cases (one side unset, same value, etc.) — those carry through to the
|
|
947
947
|
// existing applyInlineAttrsAndUnknownKeys logic below.
|
|
948
|
-
|
|
948
|
+
//
|
|
949
|
+
// FR-043 Amendment 2 — `overlay: true` LICENSES the override. The conflict error
|
|
950
|
+
// exists to catch two files that collided without knowing about each other; the flag
|
|
951
|
+
// is the author saying "I know about the other declaration and I mean to change it".
|
|
952
|
+
// The loader already treats the flag specially (find-or-throw versus create-or-find),
|
|
953
|
+
// so honouring it here makes it mean ONE thing rather than two. An unmarked
|
|
954
|
+
// redeclaration still merges and still errors, which is the case FR5c was written for.
|
|
955
|
+
if (
|
|
956
|
+
fr5cActive &&
|
|
957
|
+
preMergeAttrSnapshot !== undefined &&
|
|
958
|
+
nodeData[RESERVED_KEY_OVERLAY] !== true
|
|
959
|
+
) {
|
|
949
960
|
detectAttrMergeConflicts(
|
|
950
961
|
target,
|
|
951
962
|
nodeData,
|
|
@@ -1037,7 +1048,9 @@ function parseNodeInto(
|
|
|
1037
1048
|
* non-empty value. If so, emit ERR_MERGE_CONFLICT with a `format: "merged"`
|
|
1038
1049
|
* envelope naming both contributors. The merge itself proceeds (existing
|
|
1039
1050
|
* last-writer-wins) so the loader sees one canonical tree; the error
|
|
1040
|
-
* surfaces the conflict so a consumer can fix the metadata.
|
|
1051
|
+
* surfaces the conflict so a consumer can fix the metadata.
|
|
1052
|
+
*
|
|
1053
|
+
* NOT called for an `overlay: true` declaration — see the call site. */
|
|
1041
1054
|
function detectAttrMergeConflicts(
|
|
1042
1055
|
target: MetaData,
|
|
1043
1056
|
nodeData: Record<string, unknown>,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// @metaobjectsdev/metadata/vocabulary-rewrite-yaml — the YAML arm of `meta upgrade`.
|
|
2
|
+
//
|
|
3
|
+
// The implementation stays under `core/`, beside the canonical-JSON rewriter it mirrors.
|
|
4
|
+
// This file exists so the SUBPATH NAME mirrors the layout under `src/`, which is what
|
|
5
|
+
// `tsconfig.scripts.json`'s `@metaobjectsdev/metadata/*` → `src/*` mapping requires.
|
|
6
|
+
//
|
|
7
|
+
// Nesting is NOT the problem — the `*` matches across `/`, which is why
|
|
8
|
+
// `@metaobjectsdev/codegen-ts/templates/entity-file` resolves to
|
|
9
|
+
// `src/templates/entity-file.ts` perfectly well. The problem was a subpath NAMED
|
|
10
|
+
// `vocabulary-rewrite-yaml` whose source sat at `src/core/vocabulary-rewrite-yaml.ts`:
|
|
11
|
+
// the substitution yields `src/vocabulary-rewrite-yaml`, and that is not where the module
|
|
12
|
+
// is. Renaming the export to `./core/vocabulary-rewrite-yaml` would also have satisfied
|
|
13
|
+
// the rule and was the cheaper edit, but this package is published and that subpath is
|
|
14
|
+
// public API, so the name stays and the layout moves to meet it.
|
|
15
|
+
//
|
|
16
|
+
// When the mapping misses, tsc falls through to `node_modules` and reads `dist/` — present
|
|
17
|
+
// for anyone who has built, absent on a fresh CI checkout where the `gates` lane runs
|
|
18
|
+
// `bun install` and never builds. `./library` shipped that way and took the lane down;
|
|
19
|
+
// this one had not been imported from anywhere `scripts/` typechecks, so it was latent.
|
|
20
|
+
// `cli/test/subpath-resolves-under-scripts-paths.test.ts` now gates the class.
|
|
21
|
+
export { rewriteYamlDocument, type YamlRewriteResult } from "./core/vocabulary-rewrite-yaml.js";
|