@metaobjectsdev/metadata 1.0.5-rc.1 → 1.0.5-rc.11
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/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.d.ts.map +1 -1
- package/dist/core/relationship/derive-m2m-fields.js +50 -27
- package/dist/core/relationship/derive-m2m-fields.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +5 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +8 -1
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +1 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +115 -0
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +70 -22
- package/dist/parser-core.js.map +1 -1
- package/package.json +1 -1
- package/src/constants.ts +2 -0
- package/src/core/relationship/derive-m2m-fields.ts +56 -27
- package/src/errors.ts +5 -0
- package/src/index.ts +1 -1
- package/src/loader/meta-data-loader.ts +9 -1
- package/src/loader/validation-passes.ts +113 -0
- package/src/parser-core.ts +81 -30
|
@@ -134,6 +134,10 @@ import {
|
|
|
134
134
|
resolveRelationshipReference,
|
|
135
135
|
} from "../core/relationship/resolve-relationship-reference.js";
|
|
136
136
|
import type { MetaRoot } from "../shared/meta-root.js";
|
|
137
|
+
import type { MetaRelationship } from "../core/relationship/meta-relationship.js";
|
|
138
|
+
// Rule (f) runs the REAL M:N derivation so the loader and codegen cannot drift.
|
|
139
|
+
import { deriveM2MFields, M2MDerivationError } from "../core/relationship/derive-m2m-fields.js";
|
|
140
|
+
import { isReadOnlySource, isWritableSource } from "../shared/node-guards.js";
|
|
137
141
|
import { stripPackage } from "../naming.js";
|
|
138
142
|
import {
|
|
139
143
|
FILTER_COMPOSE_OR,
|
|
@@ -2250,6 +2254,115 @@ export function validateOneSideReferenceResolution(root: MetaRoot): ParseError[]
|
|
|
2250
2254
|
return errors;
|
|
2251
2255
|
}
|
|
2252
2256
|
|
|
2257
|
+
// ---------------------------------------------------------------------------
|
|
2258
|
+
// Rule (f) — a M:N junction must PAIR: one identity.reference resolving to the
|
|
2259
|
+
// navigating entity, one to the @objectRef target.
|
|
2260
|
+
//
|
|
2261
|
+
// Rule (d) already checks that the junction declares exactly TWO references. It
|
|
2262
|
+
// never checked WHAT they point at, so a junction whose two references name some
|
|
2263
|
+
// other pair of entities loaded perfectly clean and then failed — differently —
|
|
2264
|
+
// in every port's codegen: TypeScript and C# warned and emitted no traversal
|
|
2265
|
+
// route (the endpoint simply 404s, with nothing in the build output a CI gate
|
|
2266
|
+
// would fail on), while Java, Kotlin and Python threw and failed the build. One
|
|
2267
|
+
// input, five contracts, and the quiet arm is the dangerous one.
|
|
2268
|
+
//
|
|
2269
|
+
// The check belongs HERE because the model is what is wrong, and because a
|
|
2270
|
+
// loader error is the only answer every port already agrees on. Both codegen
|
|
2271
|
+
// branches become unreachable for a model that loaded.
|
|
2272
|
+
//
|
|
2273
|
+
// It runs the REAL derivation (deriveM2MFields) rather than reimplementing the
|
|
2274
|
+
// pairing, so the loader and the FK derivation cannot drift apart — the same
|
|
2275
|
+
// reason _junctionReferences above shares referenceIdentities() with codegen. A
|
|
2276
|
+
// parallel check would be a second opinion, and the defect class being closed
|
|
2277
|
+
// here is precisely "two parts of the pipeline disagree".
|
|
2278
|
+
//
|
|
2279
|
+
// SCOPE mirrors codegen's own iteration exactly (relation-resolver.ts): every
|
|
2280
|
+
// CONCRETE, non-projection object crossed with its EFFECTIVE relationships.
|
|
2281
|
+
// That is not incidental —
|
|
2282
|
+
// * per (entity, relationship), NOT deduped by declaration like rule (d):
|
|
2283
|
+
// pairing is a property of the NAVIGATING entity. deriveM2MFields accepts a
|
|
2284
|
+
// junction reference naming either the declaring entity or the navigating
|
|
2285
|
+
// one, so an inherited M:N can pair from one subtype and not another, and
|
|
2286
|
+
// checking the declaration alone would both miss that and reject models
|
|
2287
|
+
// codegen accepts.
|
|
2288
|
+
// * abstract levels are skipped because their own declarations do not file —
|
|
2289
|
+
// an abstract base's FK reaches a table only through a concrete descendant,
|
|
2290
|
+
// whose own walk reaches this same relationship.
|
|
2291
|
+
// * projections are skipped because they never emit a relations() block.
|
|
2292
|
+
// Widening past that set would fail builds codegen never had a problem with.
|
|
2293
|
+
// ---------------------------------------------------------------------------
|
|
2294
|
+
|
|
2295
|
+
/** codegen-ts's isProjection, reproduced here: that module is not reachable from
|
|
2296
|
+
* the loader, and the two source guards it is built from live in this package.
|
|
2297
|
+
* ADR-0039: own — projection-ness is the entity's OWN declared source @kind. */
|
|
2298
|
+
function isProjectionObject(obj: MetaObject): boolean {
|
|
2299
|
+
const own = obj.ownChildren();
|
|
2300
|
+
return own.some(isReadOnlySource) && !own.some(isWritableSource);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
export function validateM2MJunctionPairing(root: MetaRoot): ParseError[] {
|
|
2304
|
+
const errors: ParseError[] = [];
|
|
2305
|
+
for (const obj of root.objects()) {
|
|
2306
|
+
if (obj.isAbstract === true) continue;
|
|
2307
|
+
if (isProjectionObject(obj)) continue;
|
|
2308
|
+
// ADR-0039: resolving — an M:N relationship may be reached only via extends.
|
|
2309
|
+
for (const rel of obj.relationships()) {
|
|
2310
|
+
if (rel.attr(RELATIONSHIP_ATTR_CARDINALITY) !== CARDINALITY_MANY) continue;
|
|
2311
|
+
if (rel.attr(RELATIONSHIP_ATTR_THROUGH) === undefined) continue;
|
|
2312
|
+
try {
|
|
2313
|
+
deriveM2MFields(rel, obj, root);
|
|
2314
|
+
} catch (err) {
|
|
2315
|
+
if (!(err instanceof M2MDerivationError)) throw err;
|
|
2316
|
+
// Rule (d) reports the malformed-junction cases it owns (missing
|
|
2317
|
+
// @through, unresolvable @through, a reference count other than two, an
|
|
2318
|
+
// @sourceRefField matching no FK) against the DECLARING entity, and the
|
|
2319
|
+
// derivation re-checks them. Reporting them again here — once per
|
|
2320
|
+
// concrete subtype — would bury the pairing finding under duplicates of
|
|
2321
|
+
// a diagnosis the author already has, so those are left to rule (d) and
|
|
2322
|
+
// this rule speaks only where it is the sole witness.
|
|
2323
|
+
if (isJunctionShapeErrorOwnedByRuleD(rel, root)) continue;
|
|
2324
|
+
errors.push(
|
|
2325
|
+
new ParseError(
|
|
2326
|
+
`relationship "${obj.name}.${rel.name}" cannot be resolved through its ` +
|
|
2327
|
+
`@${RELATIONSHIP_ATTR_THROUGH} junction: ${err.message}. Without a reference to ` +
|
|
2328
|
+
`"${obj.name}" the junction rows cannot be filtered to this entity, so no ` +
|
|
2329
|
+
`traversal is derivable.`,
|
|
2330
|
+
{ code: "ERR_INVALID_RELATIONSHIP", source: rel.source },
|
|
2331
|
+
),
|
|
2332
|
+
);
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return errors;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/**
|
|
2340
|
+
* True when the derivation's failure is one rule (d) already reports — a
|
|
2341
|
+
* junction that is missing, unresolvable, or does not declare exactly two
|
|
2342
|
+
* identity.reference children. Keyed on the SHAPE rather than on the message
|
|
2343
|
+
* text: matching strings would make this silently stop de-duplicating the first
|
|
2344
|
+
* time a message is reworded, and a gate keyed to prose breaks on the rewrite.
|
|
2345
|
+
*/
|
|
2346
|
+
function isJunctionShapeErrorOwnedByRuleD(rel: MetaRelationship, root: MetaRoot): boolean {
|
|
2347
|
+
const through = rel.attr(RELATIONSHIP_ATTR_THROUGH);
|
|
2348
|
+
if (typeof through !== "string" || through === "") return true;
|
|
2349
|
+
// Resolve exactly as rule (d) and deriveM2MFields do — ADR-0042 via the single
|
|
2350
|
+
// resolveObjectRef matcher, referred from the DECLARING entity's package. A
|
|
2351
|
+
// second resolution rule here is what made this helper disagree with the
|
|
2352
|
+
// derivation and emit rule (d)'s finding a second time.
|
|
2353
|
+
const declaring = rel.parent;
|
|
2354
|
+
const referrerPkg = declaring?.package ?? declaring?.fileDefaultPackage ?? "";
|
|
2355
|
+
const junction = _findObject(root, through, referrerPkg) as MetaObject | undefined;
|
|
2356
|
+
if (junction === undefined) return true;
|
|
2357
|
+
if (junction.subType !== OBJECT_SUBTYPE_ENTITY) return true;
|
|
2358
|
+
if (_countJunctionReferences(junction) !== 2) return true;
|
|
2359
|
+
const sourceRefField = rel.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
|
|
2360
|
+
if (typeof sourceRefField === "string" && sourceRefField !== "") {
|
|
2361
|
+
if (!_junctionReferenceFkFields(junction).includes(sourceRefField)) return true;
|
|
2362
|
+
}
|
|
2363
|
+
return false;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2253
2366
|
/**
|
|
2254
2367
|
* Render a candidate reference as `name(fkField)`, or `name(fieldA, fieldB)`
|
|
2255
2368
|
* for a composite reference — so two composite references sharing a first
|
package/src/parser-core.ts
CHANGED
|
@@ -302,6 +302,24 @@ function isAbstractAnchorFor(type: string, registry: TypeRegistry): boolean {
|
|
|
302
302
|
return registry.allSubTypesOf(type).some((sub) => sub !== SUBTYPE_BASE);
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
/**
|
|
306
|
+
* How a (type, subType) pair is named back to the author in an error.
|
|
307
|
+
*
|
|
308
|
+
* A bare wrapper key for an UNREGISTERED type has no subType to resolve, so the naive
|
|
309
|
+
* `${type}.${subType}` renders `"madeup."` — a trailing dot that reads like the author
|
|
310
|
+
* wrote one. Print the type alone in that case; it is what they actually wrote.
|
|
311
|
+
*/
|
|
312
|
+
function typeDisplayName(type: string, subType: string): string {
|
|
313
|
+
return subType === "" ? type : `${type}${TYPE_SUBTYPE_SEPARATOR}${subType}`;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** The JSON kind of a value, for an error that has to say what the author wrote instead. */
|
|
317
|
+
function describeJsonKind(value: unknown): string {
|
|
318
|
+
if (value === null) return "null";
|
|
319
|
+
if (Array.isArray(value)) return "an array";
|
|
320
|
+
return `a ${typeof value}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
305
323
|
function abstractSubtypeMessage(type: string): string {
|
|
306
324
|
return (
|
|
307
325
|
`"${type}.${SUBTYPE_BASE}" may not be authored — every "${SUBTYPE_BASE}" subtype is an ` +
|
|
@@ -596,19 +614,6 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
596
614
|
const rootKey = wrapperKeys[0]!;
|
|
597
615
|
const rootData = topLevel[rootKey];
|
|
598
616
|
|
|
599
|
-
if (typeof rootData !== "object" || rootData === null || Array.isArray(rootData)) {
|
|
600
|
-
// The error context references the rootKey wrapper; push it so the
|
|
601
|
-
// envelope's jsonPath includes it (matches the legacy `path` slot).
|
|
602
|
-
_currentPath!.pushKey(rootKey);
|
|
603
|
-
const src = errSource();
|
|
604
|
-
_currentPath!.pop();
|
|
605
|
-
throw new ParseError(
|
|
606
|
-
`Top-level wrapper "${rootKey}" must contain an object`,
|
|
607
|
-
{ code: "ERR_TOP_LEVEL_NOT_OBJECT", source: src },
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
const rootDataObj = rootData as Record<string, unknown>;
|
|
612
617
|
const { type: rootType, subType: rootSubType, explicit: rootExplicit } =
|
|
613
618
|
splitTypeKey(rootKey, opts.registry);
|
|
614
619
|
|
|
@@ -649,7 +654,7 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
649
654
|
// other" is how a rule ends up half-true and passing every probe.
|
|
650
655
|
const retiredRoot = retiredSubType(rootType, rootSubType);
|
|
651
656
|
throw new ParseError(
|
|
652
|
-
`Unknown root type "${rootType
|
|
657
|
+
`Unknown root type "${typeDisplayName(rootType, rootSubType)}" — ` +
|
|
653
658
|
(retiredRoot !== undefined ? retirementHint(retiredRoot) : "not registered"),
|
|
654
659
|
{
|
|
655
660
|
code: rootTypeCode,
|
|
@@ -661,6 +666,26 @@ export function buildTree(parsed: unknown, opts: ParseOptions): ParseResult {
|
|
|
661
666
|
);
|
|
662
667
|
}
|
|
663
668
|
|
|
669
|
+
// --- The root wrapper's BODY must be an object ---
|
|
670
|
+
// AFTER the key resolves — the same order as the CHILD door (see processChildren).
|
|
671
|
+
// One rule, both doors, in the same order at both: until 1.0.5 this door ran the body
|
|
672
|
+
// check first, the child door ran it first too but under a DIFFERENT code, and the JVM
|
|
673
|
+
// root door ran it last with no error code at all.
|
|
674
|
+
if (typeof rootData !== "object" || rootData === null || Array.isArray(rootData)) {
|
|
675
|
+
// The error context references the rootKey wrapper; push it so the
|
|
676
|
+
// envelope's jsonPath includes it (matches the legacy `path` slot).
|
|
677
|
+
_currentPath!.pushKey(rootKey);
|
|
678
|
+
const src = errSource();
|
|
679
|
+
_currentPath!.pop();
|
|
680
|
+
throw new ParseError(
|
|
681
|
+
`Top-level wrapper "${rootKey}" must contain an object, and holds ` +
|
|
682
|
+
`${describeJsonKind(rootData)}`,
|
|
683
|
+
{ code: "ERR_TOP_LEVEL_NOT_OBJECT", source: src },
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const rootDataObj = rootData as Record<string, unknown>;
|
|
688
|
+
|
|
664
689
|
// FR5a — push the wrapper-key segment onto the JSONPath stack so all
|
|
665
690
|
// descendants emit jsonPath strings rooted at "$.<rootKey>". The merge-mode
|
|
666
691
|
// path keeps the existing root's source untouched; only NEW children get
|
|
@@ -762,7 +787,7 @@ function parseNodeFresh(
|
|
|
762
787
|
if (registry.has(type, SUBTYPE_BASE)) {
|
|
763
788
|
subType = SUBTYPE_BASE;
|
|
764
789
|
} else {
|
|
765
|
-
const msg = `Unknown type "${type
|
|
790
|
+
const msg = `Unknown type "${typeDisplayName(type, subType)}" — not registered`;
|
|
766
791
|
errors.push(new ParseError(msg, { code: "ERR_UNKNOWN_TYPE", source: errSource() }));
|
|
767
792
|
const rawName = nodeData[RESERVED_KEY_NAME];
|
|
768
793
|
const name = typeof rawName === "string" ? rawName : "";
|
|
@@ -1666,18 +1691,6 @@ function processChildren(
|
|
|
1666
1691
|
_currentYamlPosition = getYamlPosition(childRecord, childKey);
|
|
1667
1692
|
}
|
|
1668
1693
|
|
|
1669
|
-
if (typeof childData !== "object" || childData === null || Array.isArray(childData)) {
|
|
1670
|
-
reportProblem(
|
|
1671
|
-
`Child wrapper "${childKey}" at ${childNodePath} must contain an object`,
|
|
1672
|
-
strict, warnings, "ERR_TOP_LEVEL_NOT_OBJECT",
|
|
1673
|
-
);
|
|
1674
|
-
_currentPath?.pop(); // pop child wrapper key
|
|
1675
|
-
_currentPath?.pop(); // pop array index
|
|
1676
|
-
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
1677
|
-
continue;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const childDataObj = childData as Record<string, unknown>;
|
|
1681
1694
|
const { type: childType, subType: childSubTypeRaw, explicit } = splitTypeKey(childKey, registry);
|
|
1682
1695
|
|
|
1683
1696
|
let childSubType = childSubTypeRaw;
|
|
@@ -1687,8 +1700,17 @@ function processChildren(
|
|
|
1687
1700
|
// (identity, index, requirement) falls into that check's else-arm first and is reported as
|
|
1688
1701
|
// an UNKNOWN TYPE — a message asserting the type does not exist, about a type that does,
|
|
1689
1702
|
// with its name mangled to `"identity."`. Order is the whole rule here.
|
|
1690
|
-
|
|
1691
|
-
|
|
1703
|
+
//
|
|
1704
|
+
// Registration first, exactly as the ROOT door does it: an UNREGISTERED type has no
|
|
1705
|
+
// declared default either, so without this guard a typo'd child key (`madeup`) lands in
|
|
1706
|
+
// this branch and is told to "write the full `madeup.<subType>`" — advice about a type
|
|
1707
|
+
// that does not exist. The root door has carried the guard since FR5a and Java pins it
|
|
1708
|
+
// with a root-door test; the child door never got it, so one rule was true at one of its
|
|
1709
|
+
// two doors in TypeScript, C# and Java, while Python reported ERR_UNKNOWN_TYPE. The
|
|
1710
|
+
// registration check below owns the unregistered case and phrases it correctly.
|
|
1711
|
+
if (registry.allSubTypesOf(childType).length > 0
|
|
1712
|
+
&& ((childSubType === SUBTYPE_BASE && isAbstractAnchorFor(childType, registry))
|
|
1713
|
+
|| (!explicit && childSubType === ""))) {
|
|
1692
1714
|
errors.push(
|
|
1693
1715
|
explicit
|
|
1694
1716
|
? new ParseError(abstractSubtypeMessage(childType), {
|
|
@@ -1723,7 +1745,7 @@ function processChildren(
|
|
|
1723
1745
|
const retiredChild = retiredSubType(childType, childSubType);
|
|
1724
1746
|
errors.push(
|
|
1725
1747
|
new ParseError(
|
|
1726
|
-
`Unknown type "${childType
|
|
1748
|
+
`Unknown type "${typeDisplayName(childType, childSubType)}" — ` +
|
|
1727
1749
|
(retiredChild !== undefined ? retirementHint(retiredChild) : "not registered"),
|
|
1728
1750
|
{ code: childTypeCode, source: errSource() },
|
|
1729
1751
|
),
|
|
@@ -1735,6 +1757,35 @@ function processChildren(
|
|
|
1735
1757
|
}
|
|
1736
1758
|
}
|
|
1737
1759
|
|
|
1760
|
+
// --- The wrapper's BODY must be an object ---
|
|
1761
|
+
// AFTER the key resolves, deliberately. A child is one `{ "<type>.<subType>": { …body… } }`
|
|
1762
|
+
// pair, so a string/number/boolean/array/null body carries no node to read — but running
|
|
1763
|
+
// this check FIRST answers `{ "$comment": "prose" }` with "must contain an object", which
|
|
1764
|
+
// sends the author to wrap prose that was never a node in the first place. Key first, body
|
|
1765
|
+
// second: each mistake gets the message about itself.
|
|
1766
|
+
//
|
|
1767
|
+
// A hard ERROR, not `reportProblem`'s lax warning (1.0.5). Under a warning the child was
|
|
1768
|
+
// DROPPED from the tree on every port and the load still succeeded — a declared field
|
|
1769
|
+
// simply not there, with `meta gen` emitting a table without its column. Four ports
|
|
1770
|
+
// disagreed about it besides: TypeScript and C# warned, Java logged to slf4j and put
|
|
1771
|
+
// nothing in the envelope at all, and Python dropped a registered type's bad body in
|
|
1772
|
+
// silence. See docs/compatibility-policy.md, "Correcting input we wrongly accepted".
|
|
1773
|
+
if (typeof childData !== "object" || childData === null || Array.isArray(childData)) {
|
|
1774
|
+
errors.push(
|
|
1775
|
+
new ParseError(
|
|
1776
|
+
`Child wrapper "${childKey}" at ${childNodePath} must contain an object — ` +
|
|
1777
|
+
`a child is one { "<type>.<subType>": { …node body… } } pair, and this one holds ` +
|
|
1778
|
+
`${describeJsonKind(childData)}. Give it a node body, or remove the entry.`,
|
|
1779
|
+
{ code: "ERR_CHILD_NOT_OBJECT", source: errSource() },
|
|
1780
|
+
),
|
|
1781
|
+
);
|
|
1782
|
+
_currentPath?.pop(); // pop child wrapper key
|
|
1783
|
+
_currentPath?.pop(); // pop array index
|
|
1784
|
+
_currentYamlPosition = savedYamlPosition; // FR5b — restore parent's pos
|
|
1785
|
+
continue; // skip this child
|
|
1786
|
+
}
|
|
1787
|
+
const childDataObj = childData as Record<string, unknown>;
|
|
1788
|
+
|
|
1738
1789
|
// A `<type>.base` child may not be AUTHORED — the CHILD door (the root door is above).
|
|
1739
1790
|
// Gated on `explicit`, deliberately: `base` is also this parser's fallback for an
|
|
1740
1791
|
// OMITTED subtype whose registry default is unregistered (the branch directly above),
|