graphddb 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-YIXXTGZ6.js → chunk-72VZYOSG.js} +191 -239
- package/dist/{chunk-PNIZS37E.js → chunk-GWFZVNAV.js} +8 -118
- package/dist/{chunk-VST3WOK3.js → chunk-PC54CW5P.js} +3 -5
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +298 -104
- package/dist/index.js +279 -37
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-DuJ08bgc.d.ts → types-CpCo8yHP.d.ts} +140 -419
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -680,7 +680,7 @@ Relation traversal は、Entity 上に定義されたアクセスパスのみを
|
|
|
680
680
|
| ドキュメント | 内容 |
|
|
681
681
|
|----------|-------------|
|
|
682
682
|
| [Specification](./docs/spec.md) | コア API: entity、structured keys/GSIs、query/filter、relation、batch/transaction、design rule、runtime の挙動。 |
|
|
683
|
-
| [Design patterns](./docs/design-patterns.md) | DynamoDB の代表的な 10 設計パターン(RFC #118 §1)を graphddb の機能にマッピング: same-partition / embedded snapshot / edge / materialized view / sparse view / aggregate counter / versioned / reverse lookup / external projection。各パターンの用途・宣言方法(`pattern` / `
|
|
683
|
+
| [Design patterns](./docs/design-patterns.md) | DynamoDB の代表的な 10 設計パターン(RFC #118 §1)を graphddb の機能にマッピング: same-partition / embedded snapshot / edge / materialized view / sparse view / aggregate counter / versioned / reverse lookup / external projection。各パターンの用途・宣言方法(`pattern` / `@model({ kind })` + `@maintainedFrom` / `@hasOne`・`@hasMany` の versioned pattern / `@aggregate`)・read & write 維持の挙動・Phase 別の対応状況。 |
|
|
684
684
|
| [CQRS contract layer](./docs/cqrs-contract.md) | public な Query/Command contract、cardinality matrix、N+1 安全性、contract をまたぐ合成、context 境界。 |
|
|
685
685
|
| [Mutation → command derivation](./docs/mutation-command-derivation.md) | Command IF の背後にある内部的な write-plan 合成 DSL: モデルの write セマンティクス(`entityWrites`)、fragment 合成、atomic な `TransactWriteItems` の導出。 |
|
|
686
686
|
| [Class hydration](./docs/class-hydration.md) | read 結果をホスト言語のオブジェクトにロードする opt-in な `options.hydrate` ファクトリ(Phase 1: `query` トップレベル; Phase 2–3 は今後)。 |
|
|
@@ -167,6 +167,7 @@ var requireLimitRule = {
|
|
|
167
167
|
const results = [];
|
|
168
168
|
for (const relation of metadata.relations) {
|
|
169
169
|
if (relation.type !== "hasMany") continue;
|
|
170
|
+
if (relation.options?.versioned) continue;
|
|
170
171
|
const limit = relation.options?.limit;
|
|
171
172
|
if (!limit || limit.default === void 0 || limit.max === void 0) {
|
|
172
173
|
const kb = Object.keys(relation.keyBinding).join(", ");
|
|
@@ -3453,11 +3454,14 @@ function makeProjectionTransform(path, op, args) {
|
|
|
3453
3454
|
`transform: unknown projection op ${JSON.stringify(op)} (expected \`identity\` or \`preview\`).`
|
|
3454
3455
|
);
|
|
3455
3456
|
}
|
|
3457
|
+
function pathOf(value) {
|
|
3458
|
+
return typeof value === "string" ? value : value.path;
|
|
3459
|
+
}
|
|
3456
3460
|
function preview(path, n) {
|
|
3457
|
-
return makeProjectionTransform(path, "preview", [n]);
|
|
3461
|
+
return makeProjectionTransform(pathOf(path), "preview", [n]);
|
|
3458
3462
|
}
|
|
3459
3463
|
function identity(path) {
|
|
3460
|
-
return makeProjectionTransform(path, "identity", []);
|
|
3464
|
+
return makeProjectionTransform(pathOf(path), "identity", []);
|
|
3461
3465
|
}
|
|
3462
3466
|
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
3463
3467
|
"graphddb:lifecycleContract"
|
|
@@ -3549,251 +3553,208 @@ function lifecyclePhaseForIntent(intent) {
|
|
|
3549
3553
|
return intent;
|
|
3550
3554
|
}
|
|
3551
3555
|
|
|
3552
|
-
// src/
|
|
3553
|
-
function
|
|
3554
|
-
|
|
3555
|
-
const needsValue = op === "eq" || op === "ne";
|
|
3556
|
-
if (needsValue && value === void 0) {
|
|
3557
|
-
throw new Error(
|
|
3558
|
-
`whenMember('${field}', '${op}', \u2026): the '${op}' op requires a comparand value (e.g. \`whenMember('${field}', '${op}', 'active')\`).`
|
|
3559
|
-
);
|
|
3560
|
-
}
|
|
3561
|
-
return needsValue ? { path, op, value } : { path, op };
|
|
3556
|
+
// src/relation/maintenance-view-adapter.ts
|
|
3557
|
+
function logicalNameOf(meta) {
|
|
3558
|
+
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
3562
3559
|
}
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
static views = /* @__PURE__ */ new Map();
|
|
3566
|
-
static gen = 0;
|
|
3567
|
-
/** Register (or replace, by view class) a declared view. Bumps the generation. */
|
|
3568
|
-
static register(def) {
|
|
3569
|
-
this.views.set(def.viewClass, def);
|
|
3570
|
-
this.gen += 1;
|
|
3571
|
-
}
|
|
3572
|
-
/** Every registered view, in declaration order. */
|
|
3573
|
-
static getAll() {
|
|
3574
|
-
return [...this.views.values()];
|
|
3575
|
-
}
|
|
3576
|
-
/** The monotonic generation counter (changes whenever the set of views changes). */
|
|
3577
|
-
static get generation() {
|
|
3578
|
-
return this.gen;
|
|
3579
|
-
}
|
|
3580
|
-
/** Forget every registered view (test reset). Bumps the generation. */
|
|
3581
|
-
static clear() {
|
|
3582
|
-
this.views.clear();
|
|
3583
|
-
this.gen += 1;
|
|
3584
|
-
}
|
|
3585
|
-
};
|
|
3586
|
-
function isViewDefinition(value) {
|
|
3587
|
-
return typeof value === "object" && value !== null && value[VIEW_DEFINITION_MARKER] === true;
|
|
3560
|
+
function resolveClass(factory) {
|
|
3561
|
+
return factory();
|
|
3588
3562
|
}
|
|
3589
|
-
function
|
|
3590
|
-
const
|
|
3591
|
-
|
|
3592
|
-
return resolveModelClass(resolved);
|
|
3593
|
-
} catch {
|
|
3594
|
-
return resolved;
|
|
3595
|
-
}
|
|
3563
|
+
function sourceLogicalName(sourceClass) {
|
|
3564
|
+
const meta = MetadataRegistry.get(sourceClass);
|
|
3565
|
+
return logicalNameOf(meta);
|
|
3596
3566
|
}
|
|
3597
|
-
function
|
|
3598
|
-
return
|
|
3567
|
+
function triggersFor(sourceLogical, on) {
|
|
3568
|
+
return on.map((ev) => `${sourceLogical}.${ev}`);
|
|
3599
3569
|
}
|
|
3600
|
-
function
|
|
3601
|
-
const
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
)
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
out[attr] = value;
|
|
3570
|
+
function assertNoCrossSliceCollision(viewName, decls) {
|
|
3571
|
+
const projectionOwner = /* @__PURE__ */ new Map();
|
|
3572
|
+
const collectionOwner = /* @__PURE__ */ new Map();
|
|
3573
|
+
let keyFieldShape;
|
|
3574
|
+
decls.forEach((d, idx) => {
|
|
3575
|
+
for (const attr of Object.keys(d.project)) {
|
|
3576
|
+
const prev = projectionOwner.get(attr);
|
|
3577
|
+
if (prev !== void 0 && prev !== idx) {
|
|
3578
|
+
throw new Error(
|
|
3579
|
+
`View '${viewName}': projection target '${attr}' is maintained by more than one \`@maintainedFrom\` declaration. A view field must be filled by exactly one source \u2014 the declaration order carries no meaning, so this is ambiguous. Remove '${attr}' from one of the declarations (or split it onto a distinct view field).`
|
|
3580
|
+
);
|
|
3581
|
+
}
|
|
3582
|
+
projectionOwner.set(attr, idx);
|
|
3614
3583
|
}
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
}
|
|
3624
|
-
return raw;
|
|
3625
|
-
}
|
|
3626
|
-
function defineView(view, input) {
|
|
3627
|
-
const viewClass = resolveSourceClass((() => view));
|
|
3628
|
-
const name = viewClass.name;
|
|
3629
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
3630
|
-
throw new Error("defineView(view, input): the view model must be a named class.");
|
|
3631
|
-
}
|
|
3632
|
-
if (input.pattern !== void 0 && input.pattern !== "materializedView" && input.pattern !== "sparseView") {
|
|
3633
|
-
throw new Error(
|
|
3634
|
-
`defineView('${name}'): \`pattern\` must be 'materializedView' or 'sparseView' (got ${JSON.stringify(input.pattern)}).`
|
|
3635
|
-
);
|
|
3636
|
-
}
|
|
3637
|
-
const pattern = input.pattern ?? "materializedView";
|
|
3638
|
-
const sources = Array.isArray(input.source) ? input.source : [input.source];
|
|
3639
|
-
if (sources.length === 0) {
|
|
3640
|
-
throw new Error(
|
|
3641
|
-
`defineView('${name}'): at least one \`source\` is required (a view with no source is maintained by nothing).`
|
|
3642
|
-
);
|
|
3643
|
-
}
|
|
3644
|
-
const slices = [];
|
|
3645
|
-
for (const src of sources) {
|
|
3646
|
-
const keys = src.keys ?? input.keys;
|
|
3647
|
-
if (keys === void 0 || Object.keys(keys).length === 0) {
|
|
3648
|
-
throw new Error(
|
|
3649
|
-
`defineView('${name}'): a source declares no \`keys\` and the view has no default \`keys\`. The view-row key binding (view-row key field \u2192 \`$.entity.<sourceField>\`) is required to resolve which view row a source event maintains.`
|
|
3650
|
-
);
|
|
3584
|
+
if (d.collectionField !== void 0) {
|
|
3585
|
+
const prev = collectionOwner.get(d.collectionField);
|
|
3586
|
+
if (prev !== void 0 && prev !== idx) {
|
|
3587
|
+
throw new Error(
|
|
3588
|
+
`View '${viewName}': collection field '${d.collectionField}' is maintained by more than one \`@maintainedFrom\` declaration. A maintained collection has exactly one source \u2014 split the second source onto a distinct collection field.`
|
|
3589
|
+
);
|
|
3590
|
+
}
|
|
3591
|
+
collectionOwner.set(d.collectionField, idx);
|
|
3651
3592
|
}
|
|
3652
|
-
|
|
3593
|
+
const shape = Object.keys(d.keyBind).slice().sort().join(",");
|
|
3594
|
+
if (keyFieldShape === void 0) {
|
|
3595
|
+
keyFieldShape = shape;
|
|
3596
|
+
} else if (keyFieldShape !== shape) {
|
|
3653
3597
|
throw new Error(
|
|
3654
|
-
`
|
|
3598
|
+
`View '${viewName}': \`@maintainedFrom\` declarations bind different view-key field SETS ([${keyFieldShape}] vs [${shape}]). All sources must resolve the SAME view row identity \u2014 they must bind the same set of view-key fields (the source field a given key reads from may differ per source, but the key SHAPE may not). A divergent key shape would split the row.`
|
|
3655
3599
|
);
|
|
3656
3600
|
}
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
} : {}
|
|
3666
|
-
} : void 0;
|
|
3667
|
-
const predicate = src.when;
|
|
3668
|
-
if (predicate && collection) {
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
function viewFromMaintainedFrom(viewClass, meta, kind) {
|
|
3604
|
+
const viewName = viewClass.name ?? logicalNameOf(meta);
|
|
3605
|
+
const decls = meta.maintainedFrom ?? [];
|
|
3606
|
+
const pattern = kind === "sparseView" ? "sparseView" : "materializedView";
|
|
3607
|
+
for (const d of decls) {
|
|
3608
|
+
if (kind === "sparseView" && !d.when) {
|
|
3669
3609
|
throw new Error(
|
|
3670
|
-
`
|
|
3610
|
+
`View '${viewName}': \`kind: 'sparseView'\` requires every \`@maintainedFrom\` to declare a \`when\` membership predicate (e.g. \`whenMember(source.status, 'eq', 'active')\`) \u2014 the predicate decides PUT (holds) vs DELETE (flips false).`
|
|
3671
3611
|
);
|
|
3672
3612
|
}
|
|
3673
|
-
if (
|
|
3613
|
+
if (kind !== "sparseView" && d.when) {
|
|
3674
3614
|
throw new Error(
|
|
3675
|
-
`
|
|
3615
|
+
`View '${viewName}': a \`@maintainedFrom\` declares a \`when\` membership predicate but the view \`kind\` is '${kind}'. Declare \`kind: 'sparseView'\` to maintain a predicate-gated put/delete row.`
|
|
3676
3616
|
);
|
|
3677
3617
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3618
|
+
}
|
|
3619
|
+
assertNoCrossSliceCollision(
|
|
3620
|
+
viewName,
|
|
3621
|
+
decls.map((d) => ({
|
|
3622
|
+
keyBind: d.keyBind,
|
|
3623
|
+
project: d.project,
|
|
3624
|
+
collectionField: d.collection?.field
|
|
3625
|
+
}))
|
|
3626
|
+
);
|
|
3627
|
+
let updateMode;
|
|
3628
|
+
let consistency;
|
|
3629
|
+
for (const d of decls) {
|
|
3630
|
+
if (d.updateMode !== void 0) {
|
|
3631
|
+
if (updateMode !== void 0 && updateMode !== d.updateMode) {
|
|
3632
|
+
throw new Error(
|
|
3633
|
+
`View '${viewName}': conflicting \`updateMode\` across \`@maintainedFrom\` declarations ('${updateMode}' vs '${d.updateMode}'). A view row has a single write path \u2014 declare the same \`updateMode\` on every source.`
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
updateMode = d.updateMode;
|
|
3637
|
+
}
|
|
3638
|
+
if (d.consistency !== void 0) {
|
|
3639
|
+
if (consistency !== void 0 && consistency !== d.consistency) {
|
|
3640
|
+
throw new Error(
|
|
3641
|
+
`View '${viewName}': conflicting \`consistency\` across \`@maintainedFrom\` declarations ('${consistency}' vs '${d.consistency}').`
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
consistency = d.consistency;
|
|
3682
3645
|
}
|
|
3683
|
-
slices.push({
|
|
3684
|
-
sourceClass: resolveSourceClass(src.entity),
|
|
3685
|
-
maintainedOn,
|
|
3686
|
-
keys,
|
|
3687
|
-
project,
|
|
3688
|
-
...collection ? { collection } : {},
|
|
3689
|
-
...predicate ? { predicate } : {}
|
|
3690
|
-
});
|
|
3691
3646
|
}
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3694
|
-
name,
|
|
3647
|
+
const slices = decls.map((d) => sliceFromDeclaration(d));
|
|
3648
|
+
return {
|
|
3649
|
+
name: viewName,
|
|
3695
3650
|
viewClass,
|
|
3696
3651
|
pattern,
|
|
3697
3652
|
slices,
|
|
3698
|
-
...
|
|
3699
|
-
...
|
|
3653
|
+
...updateMode !== void 0 ? { updateMode } : {},
|
|
3654
|
+
...consistency !== void 0 ? { consistency } : {}
|
|
3700
3655
|
};
|
|
3701
|
-
ViewRegistry.register(def);
|
|
3702
|
-
return def;
|
|
3703
3656
|
}
|
|
3704
|
-
function
|
|
3705
|
-
|
|
3706
|
-
|
|
3657
|
+
function sliceFromDeclaration(d) {
|
|
3658
|
+
const sourceClass = resolveClass(d.sourceFactory);
|
|
3659
|
+
const sourceLogical = sourceLogicalName(sourceClass);
|
|
3660
|
+
const collection = d.collection ? {
|
|
3661
|
+
field: d.collection.field,
|
|
3662
|
+
...d.collection.maxItems !== void 0 ? { maxItems: d.collection.maxItems } : {},
|
|
3663
|
+
...d.collection.orderBy !== void 0 ? { orderBy: d.collection.orderBy, orderDir: d.collection.order ?? "DESC" } : {}
|
|
3664
|
+
} : void 0;
|
|
3665
|
+
return {
|
|
3666
|
+
sourceClass,
|
|
3667
|
+
maintainedOn: triggersFor(sourceLogical, d.on),
|
|
3668
|
+
keys: d.keyBind,
|
|
3669
|
+
project: d.project,
|
|
3670
|
+
...collection ? { collection } : {},
|
|
3671
|
+
...d.when ? { predicate: d.when } : {}
|
|
3672
|
+
};
|
|
3673
|
+
}
|
|
3674
|
+
function versionedViews(ownerClass, ownerMeta) {
|
|
3675
|
+
const ownerLogical = logicalNameOf(ownerMeta);
|
|
3676
|
+
const rels = [];
|
|
3677
|
+
for (const rel of ownerMeta.relations) {
|
|
3678
|
+
const v = rel.options?.versioned;
|
|
3679
|
+
if (!v) continue;
|
|
3680
|
+
const targetClass = resolveClass(rel.targetFactory);
|
|
3681
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
3682
|
+
if (!targetMeta.primaryKey) {
|
|
3707
3683
|
throw new Error(
|
|
3708
|
-
`
|
|
3684
|
+
`Versioned relation '${rel.propertyName}' on '${ownerLogical}': the target model '${targetClass.name}' has no \`key\` \u2014 a versioned latest/history row needs a key to resolve which row to overwrite / append.`
|
|
3709
3685
|
);
|
|
3710
3686
|
}
|
|
3687
|
+
const keyFields = [
|
|
3688
|
+
...segmentFieldNames(targetMeta.primaryKey.segmented.pkSegments),
|
|
3689
|
+
...segmentFieldNames(targetMeta.primaryKey.segmented.skSegments)
|
|
3690
|
+
];
|
|
3691
|
+
rels.push({
|
|
3692
|
+
propertyName: rel.propertyName,
|
|
3693
|
+
mode: v.mode,
|
|
3694
|
+
on: v.on,
|
|
3695
|
+
project: v.project,
|
|
3696
|
+
...v.updateMode !== void 0 ? { updateMode: v.updateMode } : {},
|
|
3697
|
+
...v.consistency !== void 0 ? { consistency: v.consistency } : {},
|
|
3698
|
+
targetClass,
|
|
3699
|
+
targetMeta,
|
|
3700
|
+
keyFields
|
|
3701
|
+
});
|
|
3711
3702
|
}
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
);
|
|
3703
|
+
assertVersionedDiscriminator(ownerLogical, rels);
|
|
3704
|
+
const out = [];
|
|
3705
|
+
for (const rel of rels) {
|
|
3706
|
+
const keys = {};
|
|
3707
|
+
for (const f of rel.keyFields) keys[f] = `$.entity.${f}`;
|
|
3708
|
+
const targetName = rel.targetClass.name ?? logicalNameOf(rel.targetMeta);
|
|
3709
|
+
out.push({
|
|
3710
|
+
name: targetName,
|
|
3711
|
+
viewClass: rel.targetClass,
|
|
3712
|
+
pattern: "materializedView",
|
|
3713
|
+
slices: [
|
|
3714
|
+
{
|
|
3715
|
+
sourceClass: ownerClass,
|
|
3716
|
+
maintainedOn: triggersFor(ownerLogical, rel.on),
|
|
3717
|
+
keys,
|
|
3718
|
+
project: rel.project
|
|
3719
|
+
}
|
|
3720
|
+
],
|
|
3721
|
+
...rel.updateMode !== void 0 ? { updateMode: rel.updateMode } : {},
|
|
3722
|
+
...rel.consistency !== void 0 ? { consistency: rel.consistency } : {}
|
|
3723
|
+
});
|
|
3734
3724
|
}
|
|
3735
|
-
|
|
3736
|
-
pattern: "materializedView",
|
|
3737
|
-
source: { entity: input.source, maintainedOn: input.maintainedOn, keys: input.latestKeys },
|
|
3738
|
-
fields: input.fields,
|
|
3739
|
-
...input.write ? { write: input.write } : {}
|
|
3740
|
-
});
|
|
3741
|
-
const historyView = defineView(history, {
|
|
3742
|
-
pattern: "materializedView",
|
|
3743
|
-
source: { entity: input.source, maintainedOn: input.maintainedOn, keys: input.historyKeys },
|
|
3744
|
-
fields: input.fields,
|
|
3745
|
-
...input.write ? { write: input.write } : {}
|
|
3746
|
-
});
|
|
3747
|
-
return { latest: latestView, history: historyView };
|
|
3748
|
-
}
|
|
3749
|
-
var PROJECTION_DEFINITION_MARKER = /* @__PURE__ */ Symbol(
|
|
3750
|
-
"graphddb:projectionDefinition"
|
|
3751
|
-
);
|
|
3752
|
-
function isProjectionDefinition(value) {
|
|
3753
|
-
return typeof value === "object" && value !== null && value[PROJECTION_DEFINITION_MARKER] === true;
|
|
3725
|
+
return out;
|
|
3754
3726
|
}
|
|
3755
|
-
function
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
throw new Error(
|
|
3772
|
-
`defineProjection('${name}'): \`idempotencyKey\` must be a non-empty source attribute name (e.g. 'resultId'). It folds at-least-once delivery to one sink upsert per genuine source event.`
|
|
3773
|
-
);
|
|
3727
|
+
function assertVersionedDiscriminator(ownerLogical, rels) {
|
|
3728
|
+
const latests = rels.filter((r) => r.mode === "latest");
|
|
3729
|
+
const histories = rels.filter((r) => r.mode === "history");
|
|
3730
|
+
if (latests.length === 0 || histories.length === 0) return;
|
|
3731
|
+
for (const latest of latests) {
|
|
3732
|
+
const latestKeyFields = new Set(latest.keyFields);
|
|
3733
|
+
for (const history of histories) {
|
|
3734
|
+
const discriminators = history.keyFields.filter((f) => !latestKeyFields.has(f));
|
|
3735
|
+
if (discriminators.length === 0) {
|
|
3736
|
+
const latestName = latest.targetClass.name ?? "<latest>";
|
|
3737
|
+
const historyName = history.targetClass.name ?? "<history>";
|
|
3738
|
+
throw new Error(
|
|
3739
|
+
`Versioned relation pair on '${ownerLogical}': the \`versionedHistory\` target '${historyName}' (\`${history.propertyName}\`) is keyed by [${history.keyFields.map((f) => `'${f}'`).join(", ")}], which carries NO version discriminator beyond the \`versionedLatest\` target '${latestName}' (\`${latest.propertyName}\`) keyed by [${[...latestKeyFields].map((f) => `'${f}'`).join(", ")}]. The history row must be keyed by a per-version field (e.g. add a \`version\` segment to '${historyName}'s \`@key\`) so each revision is a NEW row; otherwise every revision overwrites one row (a latest pointer, not an append-only history).`
|
|
3740
|
+
);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3774
3743
|
}
|
|
3775
|
-
const on = input.on ?? ["created", "updated", "removed"];
|
|
3776
|
-
const project = input.fields ? buildProjection(name, input.fields) : {};
|
|
3777
|
-
return {
|
|
3778
|
-
[PROJECTION_DEFINITION_MARKER]: true,
|
|
3779
|
-
name,
|
|
3780
|
-
pattern: "externalProjection",
|
|
3781
|
-
sourceClass: resolveSourceClass(input.source),
|
|
3782
|
-
via,
|
|
3783
|
-
idempotencyKey: input.idempotencyKey,
|
|
3784
|
-
on: [...on],
|
|
3785
|
-
project
|
|
3786
|
-
};
|
|
3787
3744
|
}
|
|
3788
|
-
function
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3745
|
+
function collectViewDefinitions(registry) {
|
|
3746
|
+
const resolved = registry ?? MetadataRegistry.getAll();
|
|
3747
|
+
const out = [];
|
|
3748
|
+
for (const [klass, meta] of resolved) {
|
|
3749
|
+
const kind = meta.kind ?? "entity";
|
|
3750
|
+
if (kind === "materializedView" || kind === "sparseView") {
|
|
3751
|
+
out.push(viewFromMaintainedFrom(klass, meta, kind));
|
|
3752
|
+
}
|
|
3753
|
+
if (meta.relations?.some((r) => r.options?.versioned)) {
|
|
3754
|
+
out.push(...versionedViews(klass, meta));
|
|
3794
3755
|
}
|
|
3795
3756
|
}
|
|
3796
|
-
return
|
|
3757
|
+
return out;
|
|
3797
3758
|
}
|
|
3798
3759
|
|
|
3799
3760
|
// src/relation/maintenance-graph.ts
|
|
@@ -3803,7 +3764,7 @@ var COUNT_DELTA_FOR_EVENT = {
|
|
|
3803
3764
|
updated: 0
|
|
3804
3765
|
};
|
|
3805
3766
|
var MAINTAIN_EVENTS = ["created", "updated", "removed"];
|
|
3806
|
-
function
|
|
3767
|
+
function isPathRooted(value) {
|
|
3807
3768
|
return value.startsWith("$.input.") || value.startsWith("$.entity.");
|
|
3808
3769
|
}
|
|
3809
3770
|
function sourceAttributeOf(path) {
|
|
@@ -3812,7 +3773,7 @@ function sourceAttributeOf(path) {
|
|
|
3812
3773
|
}
|
|
3813
3774
|
function normalizeProjectionEntry(value) {
|
|
3814
3775
|
if (typeof value === "string") {
|
|
3815
|
-
const path =
|
|
3776
|
+
const path = isPathRooted(value) ? value : `$.entity.${value}`;
|
|
3816
3777
|
return identity(path);
|
|
3817
3778
|
}
|
|
3818
3779
|
return value;
|
|
@@ -3879,7 +3840,7 @@ function assertMaintenanceRoundTrips(args) {
|
|
|
3879
3840
|
}
|
|
3880
3841
|
}
|
|
3881
3842
|
for (const [attr, transform] of Object.entries(projection)) {
|
|
3882
|
-
if (!
|
|
3843
|
+
if (!isPathRooted(transform.path)) {
|
|
3883
3844
|
throw new Error(
|
|
3884
3845
|
`Maintenance projection for relation '${relationProperty}' on '${ownerEntity}' captures attribute '${attr}' from '${transform.path}', which is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). The maintenance graph projects only from the source payload (payload \u540C\u68B1) \u2014 there is no fetch / re-projection.`
|
|
3885
3846
|
);
|
|
@@ -3926,12 +3887,12 @@ function assertCounterRoundTrips(args) {
|
|
|
3926
3887
|
}
|
|
3927
3888
|
}
|
|
3928
3889
|
}
|
|
3929
|
-
function
|
|
3890
|
+
function logicalNameOf2(meta) {
|
|
3930
3891
|
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
3931
3892
|
}
|
|
3932
3893
|
function resolveEntityByName(name, registry) {
|
|
3933
3894
|
for (const [klass, meta] of registry) {
|
|
3934
|
-
if (
|
|
3895
|
+
if (logicalNameOf2(meta) === name) return klass;
|
|
3935
3896
|
}
|
|
3936
3897
|
return void 0;
|
|
3937
3898
|
}
|
|
@@ -3999,12 +3960,12 @@ function resolveCollectionOrder(relation, sourceMeta, ownerEntity, project) {
|
|
|
3999
3960
|
const orderField = skFields[skFields.length - 1];
|
|
4000
3961
|
if (orderField === void 0) {
|
|
4001
3962
|
throw new Error(
|
|
4002
|
-
`Relation '${relation.propertyName}' on '${ownerEntity}' declares \`read.order: '${order}'\` for its maintained collection, but its source entity '${
|
|
3963
|
+
`Relation '${relation.propertyName}' on '${ownerEntity}' declares \`read.order: '${order}'\` for its maintained collection, but its source entity '${logicalNameOf2(sourceMeta)}' has no sort key to order by. A maintained collection orders by the source's sort-key field (the field the live read sorts on); declare a sort key on '${logicalNameOf2(sourceMeta)}' or drop \`read.order\`.`
|
|
4003
3964
|
);
|
|
4004
3965
|
}
|
|
4005
3966
|
if (!Object.prototype.hasOwnProperty.call(project, orderField)) {
|
|
4006
3967
|
throw new Error(
|
|
4007
|
-
`Relation '${relation.propertyName}' on '${ownerEntity}' declares \`read.order: '${order}'\` for its maintained collection, which orders by the source entity '${
|
|
3968
|
+
`Relation '${relation.propertyName}' on '${ownerEntity}' declares \`read.order: '${order}'\` for its maintained collection, which orders by the source entity '${logicalNameOf2(sourceMeta)}'s sort-key field '${orderField}', but that field is not in the relation's \`projection\` (projected: ${Object.keys(project).map((a) => `'${a}'`).join(", ")}). The maintained collection sorts its projected items, so the order field must be projected \u2014 add '${orderField}' to the projection (e.g. \`{ ${orderField}: '${orderField}', \u2026 }\`) or drop \`read.order\`.`
|
|
4008
3969
|
);
|
|
4009
3970
|
}
|
|
4010
3971
|
return { orderBy: `$.entity.${orderField}`, orderDir: order };
|
|
@@ -4075,16 +4036,15 @@ function assertNoCycle(items) {
|
|
|
4075
4036
|
}
|
|
4076
4037
|
}
|
|
4077
4038
|
function buildMaintenanceGraph(registry, views) {
|
|
4078
|
-
const useGlobal = registry === void 0;
|
|
4079
4039
|
const resolvedRegistry = registry ?? MetadataRegistry.getAll();
|
|
4080
|
-
const resolvedViews = views ?? (
|
|
4040
|
+
const resolvedViews = views ?? collectViewDefinitions(resolvedRegistry);
|
|
4081
4041
|
return buildMaintenanceGraphImpl(resolvedRegistry, resolvedViews);
|
|
4082
4042
|
}
|
|
4083
4043
|
function buildMaintenanceGraphImpl(registry, views) {
|
|
4084
4044
|
const items = [];
|
|
4085
4045
|
const owners = [...registry.entries()].map(([klass, meta]) => ({ klass, meta })).sort((a, b) => a.klass.name < b.klass.name ? -1 : a.klass.name > b.klass.name ? 1 : 0);
|
|
4086
4046
|
for (const { klass: ownerClass, meta: ownerMeta } of owners) {
|
|
4087
|
-
const ownerEntity =
|
|
4047
|
+
const ownerEntity = logicalNameOf2(ownerMeta);
|
|
4088
4048
|
const relations = [...ownerMeta.relations].sort(
|
|
4089
4049
|
(a, b) => a.propertyName < b.propertyName ? -1 : a.propertyName > b.propertyName ? 1 : 0
|
|
4090
4050
|
);
|
|
@@ -4093,7 +4053,7 @@ function buildMaintenanceGraphImpl(registry, views) {
|
|
|
4093
4053
|
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
4094
4054
|
const sourceClass = relation.targetFactory();
|
|
4095
4055
|
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
4096
|
-
const relationTargetEntity =
|
|
4056
|
+
const relationTargetEntity = logicalNameOf2(sourceMeta);
|
|
4097
4057
|
const project = buildProjectionMap(
|
|
4098
4058
|
relation.options?.projection,
|
|
4099
4059
|
ownerEntity,
|
|
@@ -4159,7 +4119,7 @@ function buildMaintenanceGraphImpl(registry, views) {
|
|
|
4159
4119
|
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
4160
4120
|
const sourceClass = agg.targetFactory();
|
|
4161
4121
|
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
4162
|
-
const aggSourceEntity =
|
|
4122
|
+
const aggSourceEntity = logicalNameOf2(sourceMeta);
|
|
4163
4123
|
const { keys, sourceFields } = buildDestinationKeys(agg.keyBinding);
|
|
4164
4124
|
const destinationKeyFields = Object.keys(keys);
|
|
4165
4125
|
const valueSourceFields = agg.options.value.op === "max" ? [agg.options.value.field] : [];
|
|
@@ -4216,11 +4176,11 @@ function buildMaintenanceGraphImpl(registry, views) {
|
|
|
4216
4176
|
for (const view of [...views].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
4217
4177
|
const ownerClass = view.viewClass;
|
|
4218
4178
|
const ownerMeta = MetadataRegistry.get(ownerClass);
|
|
4219
|
-
const ownerEntity =
|
|
4179
|
+
const ownerEntity = logicalNameOf2(ownerMeta);
|
|
4220
4180
|
view.slices.forEach((slice, sliceIndex) => {
|
|
4221
4181
|
const sourceClass = slice.sourceClass;
|
|
4222
4182
|
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
4223
|
-
const sourceEntity =
|
|
4183
|
+
const sourceEntity = logicalNameOf2(sourceMeta);
|
|
4224
4184
|
const keys = slice.keys;
|
|
4225
4185
|
const destinationKeyFields = Object.keys(keys);
|
|
4226
4186
|
const keyBindingSourceFields = Object.values(keys).map((p) => sourceAttributeOf(p));
|
|
@@ -4242,7 +4202,7 @@ function buildMaintenanceGraphImpl(registry, views) {
|
|
|
4242
4202
|
projection: slice.project
|
|
4243
4203
|
});
|
|
4244
4204
|
if (slice.predicate) {
|
|
4245
|
-
if (!
|
|
4205
|
+
if (!isPathRooted(slice.predicate.path)) {
|
|
4246
4206
|
throw new Error(
|
|
4247
4207
|
`defineView '${view.name}': sparse-view predicate path ${JSON.stringify(slice.predicate.path)} is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). The drain evaluates the predicate against the source image \u2014 use a source attribute path.`
|
|
4248
4208
|
);
|
|
@@ -4368,14 +4328,6 @@ export {
|
|
|
4368
4328
|
entityWrites,
|
|
4369
4329
|
getEntityWrites,
|
|
4370
4330
|
lifecyclePhaseForIntent,
|
|
4371
|
-
|
|
4372
|
-
ViewRegistry,
|
|
4373
|
-
isViewDefinition,
|
|
4374
|
-
defineView,
|
|
4375
|
-
defineViews,
|
|
4376
|
-
defineVersioned,
|
|
4377
|
-
isProjectionDefinition,
|
|
4378
|
-
defineProjection,
|
|
4379
|
-
defineProjections,
|
|
4331
|
+
collectViewDefinitions,
|
|
4380
4332
|
buildMaintenanceGraph
|
|
4381
4333
|
};
|