graphddb 0.4.2 → 0.5.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 +145 -476
- package/dist/{chunk-BROCT574.js → chunk-7NPG5R7O.js} +1 -1
- package/dist/{chunk-CPTV3H2U.js → chunk-FMJIWFIS.js} +156 -30
- package/dist/{chunk-G5RWWBAL.js → chunk-J5A665UW.js} +23 -3
- package/dist/cli.js +781 -6
- package/dist/index.d.ts +65 -3
- package/dist/index.js +20 -4
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-BWOrWcbd.d.ts → types-B-F7jw9f.d.ts} +42 -0
- package/docs/cdc-emulator.md +203 -0
- package/docs/class-hydration.md +409 -0
- package/docs/cloudformation.md +294 -0
- package/docs/cqrs-contract.md +526 -0
- package/docs/design-patterns.md +521 -0
- package/docs/middleware.md +189 -0
- package/docs/mutation-command-derivation.md +356 -0
- package/docs/python-bridge.md +611 -0
- package/docs/spec.md +1626 -0
- package/docs/testing.md +265 -0
- package/package.json +6 -3
|
@@ -412,6 +412,21 @@ var relationDepthRule = {
|
|
|
412
412
|
}
|
|
413
413
|
};
|
|
414
414
|
|
|
415
|
+
// src/client/TableMapping.ts
|
|
416
|
+
var TableMapping = class {
|
|
417
|
+
static mapping = {};
|
|
418
|
+
static set(mapping) {
|
|
419
|
+
this.mapping = { ...mapping };
|
|
420
|
+
}
|
|
421
|
+
static resolve(declaredName) {
|
|
422
|
+
return this.mapping[declaredName] ?? declaredName;
|
|
423
|
+
}
|
|
424
|
+
/** @internal — for testing only */
|
|
425
|
+
static reset() {
|
|
426
|
+
this.mapping = {};
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
415
430
|
// src/linter/rules/same-partition-preset.ts
|
|
416
431
|
var samePartitionPresetRule = {
|
|
417
432
|
id: "same-partition-preset",
|
|
@@ -843,6 +858,112 @@ function destinationRowKey(owner, relation) {
|
|
|
843
858
|
return `${owner}#${parts.join("&")}`;
|
|
844
859
|
}
|
|
845
860
|
|
|
861
|
+
// src/linter/rules/cfn-schema-consistency.ts
|
|
862
|
+
var MAX_GSIS_PER_TABLE = 20;
|
|
863
|
+
function gsiShape(gsi2) {
|
|
864
|
+
const hasSk = gsi2.segmented.skSegments.length > 0;
|
|
865
|
+
return { indexName: gsi2.indexName, hasSk };
|
|
866
|
+
}
|
|
867
|
+
function err3(entity, message) {
|
|
868
|
+
return {
|
|
869
|
+
ruleId: "cfn-schema-consistency",
|
|
870
|
+
severity: "error",
|
|
871
|
+
message,
|
|
872
|
+
entity
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
var cfnSchemaConsistencyRule = {
|
|
876
|
+
id: "cfn-schema-consistency",
|
|
877
|
+
severity: "error",
|
|
878
|
+
check(metadata, registry) {
|
|
879
|
+
return checkAllTables(metadata, registry);
|
|
880
|
+
}
|
|
881
|
+
};
|
|
882
|
+
function checkAllTables(current, registry) {
|
|
883
|
+
const seenMeta = /* @__PURE__ */ new Set();
|
|
884
|
+
const byPhysical = /* @__PURE__ */ new Map();
|
|
885
|
+
const add = (meta) => {
|
|
886
|
+
if (seenMeta.has(meta)) return;
|
|
887
|
+
seenMeta.add(meta);
|
|
888
|
+
const physical = TableMapping.resolve(meta.tableName);
|
|
889
|
+
const list = byPhysical.get(physical) ?? [];
|
|
890
|
+
list.push(meta);
|
|
891
|
+
byPhysical.set(physical, list);
|
|
892
|
+
};
|
|
893
|
+
add(current);
|
|
894
|
+
for (const [, meta] of registry.getFinalized()) add(meta);
|
|
895
|
+
const currentPhysical = TableMapping.resolve(current.tableName);
|
|
896
|
+
const entities = byPhysical.get(currentPhysical);
|
|
897
|
+
if (!entities) return [];
|
|
898
|
+
return checkTable(currentPhysical, entities);
|
|
899
|
+
}
|
|
900
|
+
function checkTable(physical, entities) {
|
|
901
|
+
const ordered = [...entities].sort((a, b) => a.prefix.localeCompare(b.prefix));
|
|
902
|
+
const results = [];
|
|
903
|
+
results.push(...checkBaseKeySchema(physical, ordered));
|
|
904
|
+
results.push(...checkGsiUnion(physical, ordered));
|
|
905
|
+
return results;
|
|
906
|
+
}
|
|
907
|
+
function checkBaseKeySchema(physical, entities) {
|
|
908
|
+
const withSk = [];
|
|
909
|
+
const withoutSk = [];
|
|
910
|
+
for (const meta of entities) {
|
|
911
|
+
if (!meta.primaryKey) continue;
|
|
912
|
+
const hasSk = meta.primaryKey.segmented.skSegments.length > 0;
|
|
913
|
+
(hasSk ? withSk : withoutSk).push(meta.prefix);
|
|
914
|
+
}
|
|
915
|
+
if (withSk.length > 0 && withoutSk.length > 0) {
|
|
916
|
+
const offender = [...withSk, ...withoutSk].sort()[0];
|
|
917
|
+
return [
|
|
918
|
+
err3(
|
|
919
|
+
offender,
|
|
920
|
+
`CloudFormation schema consistency: physical table '${physical}' has an ambiguous base KeySchema \u2014 some entities declare a sort key and some do not (with SK: [${withSk.join(", ")}]; without SK: [${withoutSk.join(", ")}]). A single DynamoDB table has exactly one KeySchema; every entity sharing a table must agree on whether the base key has an SK.`
|
|
921
|
+
)
|
|
922
|
+
];
|
|
923
|
+
}
|
|
924
|
+
return [];
|
|
925
|
+
}
|
|
926
|
+
function checkGsiUnion(physical, entities) {
|
|
927
|
+
const results = [];
|
|
928
|
+
const seen = /* @__PURE__ */ new Map();
|
|
929
|
+
for (const meta of entities) {
|
|
930
|
+
for (const gsi2 of meta.gsiDefinitions) {
|
|
931
|
+
const shape = gsiShape(gsi2);
|
|
932
|
+
const existing = seen.get(shape.indexName);
|
|
933
|
+
if (!existing) {
|
|
934
|
+
seen.set(shape.indexName, { shape, entity: meta.prefix });
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
const conflict = describeConflict(existing.shape, shape);
|
|
938
|
+
if (conflict) {
|
|
939
|
+
results.push(
|
|
940
|
+
err3(
|
|
941
|
+
meta.prefix,
|
|
942
|
+
`CloudFormation schema consistency: GSI '${shape.indexName}' on physical table '${physical}' is declared inconsistently across entities that share it \u2014 ${conflict} (first declared by '${existing.entity}', conflicting declaration on '${meta.prefix}'). A single DynamoDB table has exactly one GSI per index name; entities may share an index name (projecting different logical fields into the shared physical key), but only when its physical key structure (sort-key presence) is identical.`
|
|
943
|
+
)
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (seen.size > MAX_GSIS_PER_TABLE) {
|
|
949
|
+
const names = [...seen.keys()].sort();
|
|
950
|
+
const offender = [...entities].map((e) => e.prefix).sort()[0];
|
|
951
|
+
results.push(
|
|
952
|
+
err3(
|
|
953
|
+
offender,
|
|
954
|
+
`CloudFormation schema consistency: physical table '${physical}' would have ${seen.size} global secondary indexes after unioning across entities, exceeding the DynamoDB limit of ${MAX_GSIS_PER_TABLE}. Distinct index names: [${names.join(", ")}]. Reduce the number of distinct GSIs on this table.`
|
|
955
|
+
)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
return results;
|
|
959
|
+
}
|
|
960
|
+
function describeConflict(a, b) {
|
|
961
|
+
if (a.hasSk !== b.hasSk) {
|
|
962
|
+
return `one declaration has a sort key and the other does not (${a.hasSk ? "with" : "without"} SK vs. ${b.hasSk ? "with" : "without"} SK)`;
|
|
963
|
+
}
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
|
|
846
967
|
// src/linter/default-linter.ts
|
|
847
968
|
function createDefaultLinter() {
|
|
848
969
|
const linter = new Linter();
|
|
@@ -857,6 +978,7 @@ function createDefaultLinter() {
|
|
|
857
978
|
linter.addRule(hotPartitionRule);
|
|
858
979
|
linter.addRule(fanOutRule);
|
|
859
980
|
linter.addRule(multiMaintainerSameRowRule);
|
|
981
|
+
linter.addRule(cfnSchemaConsistencyRule);
|
|
860
982
|
return linter;
|
|
861
983
|
}
|
|
862
984
|
|
|
@@ -909,6 +1031,25 @@ var MetadataRegistry = class {
|
|
|
909
1031
|
}
|
|
910
1032
|
return new Map(this.store);
|
|
911
1033
|
}
|
|
1034
|
+
/**
|
|
1035
|
+
* Snapshot of the entities that are **already finalized**, WITHOUT triggering
|
|
1036
|
+
* finalize on any pending entity (unlike {@link getAll}). This is the safe view
|
|
1037
|
+
* for a cross-entity lint rule that runs *inside* `finalize`: eagerly
|
|
1038
|
+
* finalizing the rest of the registry from within a finalize pass would
|
|
1039
|
+
* re-enter (and duplicate) the in-progress entity's finalize. A whole-registry
|
|
1040
|
+
* rule instead reasons over the already-finalized peers plus the entity
|
|
1041
|
+
* currently being checked; any conflicting pair is caught when the later of the
|
|
1042
|
+
* two finalizes (both are present by then).
|
|
1043
|
+
*
|
|
1044
|
+
* @internal — for cross-entity linter rules.
|
|
1045
|
+
*/
|
|
1046
|
+
static getFinalized() {
|
|
1047
|
+
const out = /* @__PURE__ */ new Map();
|
|
1048
|
+
for (const [target, meta] of this.store) {
|
|
1049
|
+
if (meta._finalized) out.set(target, meta);
|
|
1050
|
+
}
|
|
1051
|
+
return out;
|
|
1052
|
+
}
|
|
912
1053
|
static get linter() {
|
|
913
1054
|
return this._linter;
|
|
914
1055
|
}
|
|
@@ -968,21 +1109,6 @@ ${messages}`);
|
|
|
968
1109
|
}
|
|
969
1110
|
};
|
|
970
1111
|
|
|
971
|
-
// src/client/TableMapping.ts
|
|
972
|
-
var TableMapping = class {
|
|
973
|
-
static mapping = {};
|
|
974
|
-
static set(mapping) {
|
|
975
|
-
this.mapping = { ...mapping };
|
|
976
|
-
}
|
|
977
|
-
static resolve(declaredName) {
|
|
978
|
-
return this.mapping[declaredName] ?? declaredName;
|
|
979
|
-
}
|
|
980
|
-
/** @internal — for testing only */
|
|
981
|
-
static reset() {
|
|
982
|
-
this.mapping = {};
|
|
983
|
-
}
|
|
984
|
-
};
|
|
985
|
-
|
|
986
1112
|
// src/define/param.ts
|
|
987
1113
|
function isParamOptions(value) {
|
|
988
1114
|
return typeof value === "object" && value !== null;
|
|
@@ -2334,14 +2460,14 @@ var MiddlewareRuntime = class {
|
|
|
2334
2460
|
* is the host's responsibility — a hook recovering a `query` should return an
|
|
2335
2461
|
* item / `null`, one recovering a `list` a `{ items, cursor }`.
|
|
2336
2462
|
*/
|
|
2337
|
-
async runRequestError(ctx,
|
|
2463
|
+
async runRequestError(ctx, err4) {
|
|
2338
2464
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2339
2465
|
const onError = this.chain[i].read?.onError;
|
|
2340
2466
|
if (!onError) continue;
|
|
2341
|
-
const recovered = await onError(ctx,
|
|
2467
|
+
const recovered = await onError(ctx, err4);
|
|
2342
2468
|
if (recovered !== void 0) return recovered;
|
|
2343
2469
|
}
|
|
2344
|
-
throw
|
|
2470
|
+
throw err4;
|
|
2345
2471
|
}
|
|
2346
2472
|
/**
|
|
2347
2473
|
* Drive ONE physical op (root read or any fan-out fetch) through R2 → send →
|
|
@@ -2381,14 +2507,14 @@ var MiddlewareRuntime = class {
|
|
|
2381
2507
|
if (afterFetch) items = await afterFetch(ctx, items);
|
|
2382
2508
|
}
|
|
2383
2509
|
return items;
|
|
2384
|
-
} catch (
|
|
2510
|
+
} catch (err4) {
|
|
2385
2511
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2386
2512
|
const onError = this.chain[i].read?.op?.onError;
|
|
2387
2513
|
if (!onError) continue;
|
|
2388
|
-
const recovered = await onError(ctx,
|
|
2514
|
+
const recovered = await onError(ctx, err4);
|
|
2389
2515
|
if (recovered !== void 0) return recovered;
|
|
2390
2516
|
}
|
|
2391
|
-
throw
|
|
2517
|
+
throw err4;
|
|
2392
2518
|
}
|
|
2393
2519
|
}
|
|
2394
2520
|
};
|
|
@@ -2452,14 +2578,14 @@ var WriteRuntime = class {
|
|
|
2452
2578
|
* short-circuits the chain); the value becomes the write's resolved value.
|
|
2453
2579
|
* Otherwise the original error rethrows (symmetric with the read `onError`).
|
|
2454
2580
|
*/
|
|
2455
|
-
async runWriteError(ctx,
|
|
2581
|
+
async runWriteError(ctx, err4) {
|
|
2456
2582
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2457
2583
|
const onError = this.chain[i].write?.onError;
|
|
2458
2584
|
if (!onError) continue;
|
|
2459
|
-
const recovered = await onError(ctx,
|
|
2585
|
+
const recovered = await onError(ctx, err4);
|
|
2460
2586
|
if (recovered !== void 0) return recovered;
|
|
2461
2587
|
}
|
|
2462
|
-
throw
|
|
2588
|
+
throw err4;
|
|
2463
2589
|
}
|
|
2464
2590
|
/** Build a W3/W4/W5 persist context (pure — no hooks run). */
|
|
2465
2591
|
persistCtx(items, origins, transaction) {
|
|
@@ -2488,20 +2614,20 @@ var WriteRuntime = class {
|
|
|
2488
2614
|
if (before) await before(ctx);
|
|
2489
2615
|
}
|
|
2490
2616
|
results = await send(ctx.items);
|
|
2491
|
-
} catch (
|
|
2617
|
+
} catch (err4) {
|
|
2492
2618
|
let recovered;
|
|
2493
2619
|
let didRecover = false;
|
|
2494
2620
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
2495
2621
|
const onError = this.chain[i].write?.persist?.onError;
|
|
2496
2622
|
if (!onError) continue;
|
|
2497
|
-
const r = await onError(ctx,
|
|
2623
|
+
const r = await onError(ctx, err4);
|
|
2498
2624
|
if (r !== void 0) {
|
|
2499
2625
|
recovered = r;
|
|
2500
2626
|
didRecover = true;
|
|
2501
2627
|
break;
|
|
2502
2628
|
}
|
|
2503
2629
|
}
|
|
2504
|
-
if (!didRecover) throw
|
|
2630
|
+
if (!didRecover) throw err4;
|
|
2505
2631
|
results = recovered;
|
|
2506
2632
|
}
|
|
2507
2633
|
for (let i = this.chain.length - 1; i >= 0; i--) {
|
|
@@ -2722,8 +2848,8 @@ async function executeSingleWriteWithHooks(req, mw) {
|
|
|
2722
2848
|
await mw.runWriteBefore(ctx);
|
|
2723
2849
|
const change2 = await persistRewrittenWrite(req.modelClass, ctx, mw);
|
|
2724
2850
|
await mw.runWriteAfter(ctx, change2);
|
|
2725
|
-
} catch (
|
|
2726
|
-
await mw.runWriteError(ctx,
|
|
2851
|
+
} catch (err4) {
|
|
2852
|
+
await mw.runWriteError(ctx, err4);
|
|
2727
2853
|
}
|
|
2728
2854
|
}
|
|
2729
2855
|
async function persistRewrittenWrite(modelClass, ctx, mw) {
|
|
@@ -4287,9 +4413,9 @@ export {
|
|
|
4287
4413
|
resolveKey,
|
|
4288
4414
|
missingGsiRule,
|
|
4289
4415
|
relationDepthRule,
|
|
4416
|
+
TableMapping,
|
|
4290
4417
|
createDefaultLinter,
|
|
4291
4418
|
MetadataRegistry,
|
|
4292
|
-
TableMapping,
|
|
4293
4419
|
pkTemplate,
|
|
4294
4420
|
skTemplate,
|
|
4295
4421
|
resolveSegmentedKey,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
serializeFieldValue,
|
|
43
43
|
serializeRawCondition,
|
|
44
44
|
skTemplate
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-FMJIWFIS.js";
|
|
46
46
|
|
|
47
47
|
// src/spec/types.ts
|
|
48
48
|
var SPEC_VERSION = "1.0";
|
|
@@ -129,7 +129,11 @@ function buildEntity(metadata) {
|
|
|
129
129
|
key: buildKey(metadata.primaryKey),
|
|
130
130
|
gsis: buildGsis(metadata),
|
|
131
131
|
relations: buildRelations(metadata),
|
|
132
|
-
...metadata.description !== void 0 ? { description: metadata.description } : {}
|
|
132
|
+
...metadata.description !== void 0 ? { description: metadata.description } : {},
|
|
133
|
+
// TTL (issue #172, Epic #167 — C4): a physical-schema fact carried on the
|
|
134
|
+
// manifest so the CFn emitter can render `TimeToLiveSpecification`. Absent
|
|
135
|
+
// when no `@ttl` field is declared (byte-identical to the pre-#172 manifest).
|
|
136
|
+
...metadata.ttlAttribute !== void 0 ? { ttlAttribute: metadata.ttlAttribute } : {}
|
|
133
137
|
};
|
|
134
138
|
}
|
|
135
139
|
function buildManifest(registry = MetadataRegistry) {
|
|
@@ -140,12 +144,28 @@ function buildManifest(registry = MetadataRegistry) {
|
|
|
140
144
|
}
|
|
141
145
|
const entities = {};
|
|
142
146
|
const tables = {};
|
|
147
|
+
const ttlByPhysicalTable = /* @__PURE__ */ new Map();
|
|
143
148
|
for (const name of [...entitiesByName.keys()].sort()) {
|
|
144
149
|
const metadata = entitiesByName.get(name);
|
|
145
150
|
entities[name] = buildEntity(metadata);
|
|
151
|
+
const physicalName = TableMapping.resolve(metadata.tableName);
|
|
146
152
|
tables[metadata.tableName] = {
|
|
147
|
-
physicalName
|
|
153
|
+
physicalName
|
|
148
154
|
};
|
|
155
|
+
if (metadata.ttlAttribute !== void 0) {
|
|
156
|
+
const existing = ttlByPhysicalTable.get(physicalName);
|
|
157
|
+
if (existing && existing.attr !== metadata.ttlAttribute) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Manifest build: physical table '${physicalName}' has more than one TTL attribute \u2014 entity '${existing.entity}' declares \`@ttl\` on '${existing.attr}' and entity '${name}' on '${metadata.ttlAttribute}'. DynamoDB allows exactly one TTL attribute per table; align the \`@ttl\` attribute across all entities sharing this table (or drop one).`
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
if (!existing) {
|
|
163
|
+
ttlByPhysicalTable.set(physicalName, {
|
|
164
|
+
entity: name,
|
|
165
|
+
attr: metadata.ttlAttribute
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
149
169
|
}
|
|
150
170
|
return {
|
|
151
171
|
version: SPEC_VERSION,
|