graphddb 0.8.0 → 0.9.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.
@@ -1,8 +1,9 @@
1
1
  // src/spec/types.ts
2
2
  var SPEC_VERSION = "1.1";
3
3
  var SPEC_VERSION_SCP = "1.2";
4
- var SPEC_VERSION_SUPPORTED = SPEC_VERSION_SCP;
5
- var EXPR_VERSION = 1;
4
+ var SPEC_VERSION_KEY_EXPR = "2.0";
5
+ var SPEC_VERSION_SUPPORTED = SPEC_VERSION_KEY_EXPR;
6
+ var EXPR_VERSION = 2;
6
7
  var MARKER_ROW_ENTITY = "__marker__";
7
8
 
8
9
  // src/define/transaction.ts
@@ -236,33 +237,14 @@ function canonicalizeExpressionSpec(input, context) {
236
237
  function isScpExprConditionInput(condition) {
237
238
  return "scpExpr" in condition;
238
239
  }
239
- function conditionHasScp(condition) {
240
- return condition?.kind === "scpExpr";
241
- }
242
- function operationsContainScpNodes(doc) {
243
- for (const command of Object.values(doc.commands)) {
244
- if (conditionHasScp(command.condition)) return true;
245
- }
246
- for (const transaction of Object.values(doc.transactions ?? {})) {
247
- for (const item of transaction.items) {
248
- if (item.guard !== void 0 || conditionHasScp(item.condition)) return true;
249
- }
250
- }
251
- for (const contract of Object.values(doc.contracts ?? {})) {
252
- if (contract.kind !== "command") continue;
253
- for (const method of Object.values(contract.methods)) {
254
- if (method.guard !== void 0) return true;
255
- }
256
- }
257
- return false;
258
- }
259
- function operationsSpecVersion(doc) {
260
- return operationsContainScpNodes(doc) ? SPEC_VERSION_SCP : SPEC_VERSION;
240
+ function operationsSpecVersion(_doc) {
241
+ return SPEC_VERSION_KEY_EXPR;
261
242
  }
262
243
 
263
244
  export {
264
245
  SPEC_VERSION,
265
246
  SPEC_VERSION_SCP,
247
+ SPEC_VERSION_KEY_EXPR,
266
248
  SPEC_VERSION_SUPPORTED,
267
249
  EXPR_VERSION,
268
250
  MARKER_ROW_ENTITY,
@@ -273,6 +255,5 @@ export {
273
255
  SCP_LOWERED_MARKER,
274
256
  canonicalizeExpressionSpec,
275
257
  isScpExprConditionInput,
276
- operationsContainScpNodes,
277
258
  operationsSpecVersion
278
259
  };
@@ -17,7 +17,7 @@ import {
17
17
  normalizeTopLevelSelect,
18
18
  parseChange,
19
19
  validateInlineSnapshotSelect
20
- } from "./chunk-HNY2EJPV.js";
20
+ } from "./chunk-KOIJ4SNO.js";
21
21
  import {
22
22
  EXPR_VERSION,
23
23
  MARKER_ROW_ENTITY,
@@ -26,7 +26,7 @@ import {
26
26
  isScpExprConditionInput,
27
27
  isTransactionDefinition,
28
28
  isTransactionRef
29
- } from "./chunk-L4QRCHRQ.js";
29
+ } from "./chunk-WOFRHRXY.js";
30
30
  import {
31
31
  BATCH_GET_MAX_KEYS,
32
32
  CONDITION_OPERATOR_KEYS,
@@ -61,7 +61,7 @@ import {
61
61
  serializeFieldValue,
62
62
  serializeRawCondition,
63
63
  skTemplate
64
- } from "./chunk-L2NEDS7U.js";
64
+ } from "./chunk-ZNU7OI5I.js";
65
65
  import {
66
66
  TableMapping,
67
67
  createColumnMap,
@@ -4669,16 +4669,40 @@ function mutate(body, options) {
4669
4669
  if (isCommandPlan(body)) {
4670
4670
  return mode !== void 0 ? { command: body, mode } : { command: body };
4671
4671
  }
4672
+ if (isPublicCompositeDescriptor(body)) {
4673
+ return mode !== void 0 ? { ...body, mode } : body;
4674
+ }
4672
4675
  throw new Error(
4673
- "mutate: the body must be a write body \u2014 a `defineScpTransaction(...)` / `defineTransaction(...)` transaction definition or a `mutation(...)` plan. `mutate` is the authoring twin of the runtime `mutate`; its only option is `{ mode }` (the sole home of atomicity)."
4676
+ "mutate: the body must be a write body \u2014 a PUBLIC composite descriptor (`{ writes: $ => ({ alias: descriptor, \u2026 }), input?, result? }`, the import-free multi-entity atomic form, #293), a `defineScpTransaction(...)` / `defineTransaction(...)` transaction definition, or a `mutation(...)` plan. `mutate` is the authoring twin of the runtime `mutate`; its only option is `{ mode }` (the sole home of atomicity)."
4674
4677
  );
4675
4678
  }
4676
4679
  function buildCommandContract(methods) {
4677
4680
  const resolved = {};
4678
4681
  for (const [name, body] of Object.entries(methods)) {
4679
4682
  if (isPublicWriteDescriptor(body)) {
4680
- const { plan: plan2, select, condition, mode, description } = writeDescriptorToPlan(name, body);
4681
- resolved[name] = finalizeDescriptorCommand(name, plan2, select, condition, mode, description);
4683
+ const { plan: plan2, select, condition, mode, batchWriteEligible, description } = writeDescriptorToPlan(name, body);
4684
+ resolved[name] = finalizeDescriptorCommand(
4685
+ name,
4686
+ plan2,
4687
+ select,
4688
+ condition,
4689
+ mode,
4690
+ description,
4691
+ batchWriteEligible
4692
+ );
4693
+ continue;
4694
+ }
4695
+ if (isPublicCompositeDescriptor(body)) {
4696
+ const plan2 = mutation(name, body.writes);
4697
+ const select = body.result !== void 0 && body.result.select !== void 0 ? body.result.select : void 0;
4698
+ resolved[name] = finalizeDescriptorCommand(
4699
+ name,
4700
+ plan2,
4701
+ select,
4702
+ void 0,
4703
+ body.mode ?? "transaction",
4704
+ body.description
4705
+ );
4682
4706
  continue;
4683
4707
  }
4684
4708
  if (isPublicComposeDescriptor(body)) {
@@ -4708,7 +4732,7 @@ function buildCommandContract(methods) {
4708
4732
  continue;
4709
4733
  }
4710
4734
  throw new Error(
4711
- `publishCommand: method '${name}' must be a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result?, mode? }\`), a composite \`{ command: mutation(...), input?, result?, mode? }\` descriptor, a bare \`mutation($ => ({ \u2026 }))\` plan, or an SCP native-syntax \`defineScpTransaction(...)\` multi-write body. The legacy closure form (\`(keys, params) => Model.putItem(...)\`) and the \`command(...).plan(...)\` builder have been removed (#101).`
4735
+ `publishCommand: method '${name}' must be a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result?, mode? }\`) for a single-entity write, or \u2014 for a multi-entity atomic (composite) write \u2014 a \`{ writes: $ => ({ alias: { create | update | remove: Model, key, input?, condition? }, \u2026 }), input?, result?, mode? }\` descriptor (the PUBLIC composite form; a multi-alias \`writes\` body is compiled internally into one atomic \`TransactWriteItems\`, needing no \`mutation(...)\` / \`defineScpTransaction(...)\` import). The legacy closure form (\`(keys, params) => Model.putItem(...)\`) and the \`command(...).plan(...)\` builder have been removed (#101).`
4712
4736
  );
4713
4737
  }
4714
4738
  return { __isContract: true, kind: "command", methods: resolved };
@@ -4755,7 +4779,7 @@ function buildPlannedCommandMethod(name, planned) {
4755
4779
  ...returnSelection !== void 0 ? { returnSelection } : {}
4756
4780
  };
4757
4781
  }
4758
- function finalizeDescriptorCommand(name, plan2, select, condition, mode, description) {
4782
+ function finalizeDescriptorCommand(name, plan2, select, condition, declaredMode, description, batchWriteEligible = false) {
4759
4783
  const planned = {
4760
4784
  [PLANNED_COMMAND_BRAND]: true,
4761
4785
  plan: plan2,
@@ -4763,6 +4787,13 @@ function finalizeDescriptorCommand(name, plan2, select, condition, mode, descrip
4763
4787
  select
4764
4788
  };
4765
4789
  const built = buildPlannedCommandMethod(name, planned);
4790
+ const promoted = built.ops !== void 0 && built.ops.length > 1 || built.conditionChecks !== void 0 || built.edgeWrites !== void 0 || built.derivedUpdates !== void 0 || built.uniqueGuards !== void 0 || built.outboxEvents !== void 0 || built.idempotencyGuard !== void 0 || built.maintainWrites !== void 0 || built.maintainOutbox !== void 0;
4791
+ if (declaredMode === "batchWrite" && (!batchWriteEligible || promoted)) {
4792
+ throw new Error(
4793
+ `publishCommand: method '${name}' declares mode: 'batchWrite', but DynamoDB's BatchWriteItem carries no ConditionExpression, no Update and no composed derived effects \u2014 only an UNCONDITIONAL put/delete qualifies (an \`upsert\` or a condition-free \`remove\` with no derived effects). This method is ${!batchWriteEligible ? "conditional / not an unconditional put/delete" : "promoted to an atomic TransactWriteItems by its derived effects"}. Use mode: 'transaction' (atomic TransactWriteItems, condition-capable, \u226425) \u2014 or the transaction forEach form for atomic per-item values.`
4794
+ );
4795
+ }
4796
+ const mode = declaredMode ?? (batchWriteEligible && !promoted ? "batchWrite" : "transaction");
4766
4797
  const builtOp = built.op;
4767
4798
  const op = condition !== void 0 ? { ...builtOp, condition } : builtOp;
4768
4799
  return {
@@ -4801,6 +4832,9 @@ var WRITE_INTENT_KEYS = ["create", "update", "remove", "upsert"];
4801
4832
  function isPublicComposeDescriptor(value) {
4802
4833
  return typeof value === "object" && value !== null && isCommandPlan(value.command);
4803
4834
  }
4835
+ function isPublicCompositeDescriptor(value) {
4836
+ return typeof value === "object" && value !== null && typeof value.writes === "function";
4837
+ }
4804
4838
  function isPublicReadDescriptor(value) {
4805
4839
  return typeof value === "object" && value !== null && READ_INTENT_KEYS2.some((k) => value[k] !== void 0);
4806
4840
  }
@@ -4996,7 +5030,8 @@ function writeDescriptorToPlan(name, d) {
4996
5030
  });
4997
5031
  const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
4998
5032
  const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
4999
- return { plan: plan2, select, condition, mode: d.mode ?? "transaction", description: d.description };
5033
+ const batchWriteEligible = (intent === "upsert" || intent === "remove") && d.condition === void 0;
5034
+ return { plan: plan2, select, condition, mode: d.mode, batchWriteEligible, description: d.description };
5000
5035
  }
5001
5036
 
5002
5037
  // src/spec/mutation-command.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  buildMaintenanceGraph
3
- } from "./chunk-HNY2EJPV.js";
3
+ } from "./chunk-KOIJ4SNO.js";
4
4
  import {
5
5
  ChangeCaptureRegistry,
6
6
  ClientManager,
@@ -9,7 +9,7 @@ import {
9
9
  buildDeleteInput,
10
10
  buildPutInput,
11
11
  resolveModelClass
12
- } from "./chunk-L2NEDS7U.js";
12
+ } from "./chunk-ZNU7OI5I.js";
13
13
 
14
14
  // src/cdc/prng.ts
15
15
  var SeededRandom = class {
@@ -227,10 +227,35 @@ var param = {
227
227
  const param2 = { kind: "array", element };
228
228
  if (options?.description !== void 0) param2.description = options.description;
229
229
  return param2;
230
+ },
231
+ /**
232
+ * A placeholder for a **map / embedded object** value written to a single
233
+ * DynamoDB `M` attribute (issue #295). Free-form by design: the model-side
234
+ * vocabulary (`@map` / `@embedded`) records no nested schema, so the param is
235
+ * typed as the language-native map in generated bindings (`dict` /
236
+ * `map[string]any` / `serde_json::Value` / `array`) while the optional type
237
+ * parameter `T` keeps the TS authoring surface precise.
238
+ *
239
+ * Legal at **value positions only** (a write descriptor's `input`, a
240
+ * transaction fragment value, a batch-write per-item payload field) — a map
241
+ * cannot compose a key, so a `param.map()` in a key position is rejected
242
+ * loudly at build time.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * putLeaf: {
247
+ * upsert: TreeNode,
248
+ * key: { part: param.string(), child: param.string() },
249
+ * input: { value: param.map<AvgValue>(), computedAt: param.number() },
250
+ * }
251
+ * ```
252
+ */
253
+ map(options) {
254
+ return makeParam("map", void 0, options?.description);
230
255
  }
231
256
  };
232
257
  function isParam(value) {
233
- return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array");
258
+ return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array" || value.kind === "map");
234
259
  }
235
260
 
236
261
  // src/client/lib-dynamodb.ts
@@ -337,17 +362,220 @@ async function executeBatchGet(docClient, operation) {
337
362
  return { items: allItems };
338
363
  }
339
364
 
365
+ // src/spec/catalog.ts
366
+ import { classifyBehaviorEffect } from "behavior-contracts";
367
+ var P = (type, required) => required ? { type, required } : { type };
368
+ var READ_PORTS = {
369
+ table: P("string", true),
370
+ indexName: P("string"),
371
+ rangeKey: P("string"),
372
+ rangeOperator: P("string"),
373
+ rangeValue: P("expr"),
374
+ limit: P("number"),
375
+ filter: P("string"),
376
+ projection: P("string[]", true),
377
+ implicitSourceField: P("string")
378
+ };
379
+ var WRITE_PORTS = {
380
+ table: P("string", true),
381
+ entity: P("string", true),
382
+ literalKey: P("boolean"),
383
+ maintain: P("string"),
384
+ "condition.kind": P("string"),
385
+ "condition.field": P("string"),
386
+ "condition.declarative": P("string"),
387
+ "condition.expression": P("string"),
388
+ "condition.names": P("string")
389
+ };
390
+ var entry = (name, inputPorts, shape, portableToIR) => ({
391
+ name,
392
+ inputPorts,
393
+ output: { shape },
394
+ portableToIR,
395
+ // Dynamic port families (key-attribute / write record ports) are validated by
396
+ // assertComponentsInCatalog, not statically enumerable — see the module doc
397
+ // ("Dynamic-port opt-in"). TransactWrite overrides this back to fail-closed.
398
+ additionalPorts: true
399
+ });
400
+ var GRAPHDDB_CATALOG = {
401
+ GetItem: entry("GetItem", READ_PORTS, "item", true),
402
+ Query: entry(
403
+ "Query",
404
+ // `cardinality: 'one'` is the root-collapse port only the Query root emits.
405
+ { ...READ_PORTS, cardinality: P("string") },
406
+ "items",
407
+ true
408
+ ),
409
+ BatchGetItem: entry("BatchGetItem", READ_PORTS, "items", true),
410
+ PutItem: entry("PutItem", WRITE_PORTS, "item", true),
411
+ UpdateItem: entry("UpdateItem", WRITE_PORTS, "item", true),
412
+ DeleteItem: entry("DeleteItem", WRITE_PORTS, "item", true),
413
+ ConditionCheck: entry("ConditionCheck", WRITE_PORTS, "item", true),
414
+ // The executor-side atomic envelope: never referenced by the portable IR
415
+ // (a transaction IS a component over the item primitives) — TS-runtime-only.
416
+ // Its single port is static, so it keeps bc's fail-closed default (no
417
+ // additionalPorts opt-in).
418
+ TransactWrite: {
419
+ ...entry("TransactWrite", { TransactItems: P("map[]", true) }, "item", false),
420
+ additionalPorts: false
421
+ }
422
+ };
423
+ function catalogEntry(name) {
424
+ return GRAPHDDB_CATALOG[name];
425
+ }
426
+ var CATALOG_NAMES = [
427
+ "GetItem",
428
+ "Query",
429
+ "BatchGetItem",
430
+ "PutItem",
431
+ "UpdateItem",
432
+ "DeleteItem",
433
+ "ConditionCheck",
434
+ "TransactWrite"
435
+ ];
436
+ var WRITE_CATALOG_NAMES = [
437
+ "PutItem",
438
+ "UpdateItem",
439
+ "DeleteItem",
440
+ "ConditionCheck",
441
+ "TransactWrite"
442
+ ];
443
+ var WRITE_SET = new Set(WRITE_CATALOG_NAMES);
444
+ var KEY_ATTR_PORT = /^[A-Za-z0-9_]*(?:PK|SK)$/;
445
+ var WRITE_PORT_FAMILIES = [
446
+ "item.",
447
+ "key.",
448
+ "changes.",
449
+ "add.",
450
+ "condition.equals.",
451
+ "condition.params.",
452
+ "condition.values."
453
+ ];
454
+ function assertComponentsInCatalog(components) {
455
+ const errs = [];
456
+ for (const c of components) {
457
+ for (const n of c.body) {
458
+ if ("cond" in n) continue;
459
+ const ref = "map" in n ? n.map : n;
460
+ const entryDef = GRAPHDDB_CATALOG[ref.component];
461
+ if (entryDef === void 0) {
462
+ errs.push(`${c.name}/${n.id}: references '${ref.component}', not a GRAPHDDB_CATALOG entry (known: ${CATALOG_NAMES.join(", ")})`);
463
+ continue;
464
+ }
465
+ const ports = ref.ports;
466
+ for (const [p, s] of Object.entries(entryDef.inputPorts)) {
467
+ if (s.required === true && !(p in ports)) {
468
+ errs.push(`${c.name}/${n.id} (${ref.component}): required port '${p}' is not wired`);
469
+ }
470
+ }
471
+ const isWriteItem = WRITE_SET.has(ref.component);
472
+ for (const p of Object.keys(ports)) {
473
+ if (p in entryDef.inputPorts) continue;
474
+ if (isWriteItem ? WRITE_PORT_FAMILIES.some((f) => p.startsWith(f)) : KEY_ATTR_PORT.test(p)) continue;
475
+ errs.push(`${c.name}/${n.id} (${ref.component}): port '${p}' is not declared by the catalog entry (nor a ${isWriteItem ? "write record-family" : "key-attribute"} port)`);
476
+ }
477
+ }
478
+ }
479
+ if (errs.length > 0) {
480
+ throw new Error(`graphddb catalog validation failed (${errs.length} violation${errs.length === 1 ? "" : "s"}):
481
+ - ${errs.join("\n - ")}`);
482
+ }
483
+ }
484
+ function deriveContractEffect(component) {
485
+ return classifyBehaviorEffect(component, WRITE_CATALOG_NAMES) === "writing" ? "command" : "query";
486
+ }
487
+ function deriveComponentEffects(components) {
488
+ const out = /* @__PURE__ */ new Map();
489
+ for (const c of components) out.set(c.name, deriveContractEffect(c));
490
+ return out;
491
+ }
492
+ function assertPublishSplitMatchesDerived(derived, split, contracts) {
493
+ const errs = [];
494
+ const check = (name, declared, at) => {
495
+ const truth = derived.get(name);
496
+ if (truth === void 0) {
497
+ errs.push(`${at}: no emitted component '${name}'`);
498
+ return;
499
+ }
500
+ if (truth !== declared) {
501
+ const verb = declared === "query" ? "publishQuery" : "publishCommand";
502
+ const fix = truth === "command" ? "publishCommand" : "publishQuery";
503
+ errs.push(
504
+ `${at}: published under ${verb} (declares '${declared}') but component '${name}' derives '${truth}' from its graph \u2014 the component graph is the source of truth; publish it with ${fix}`
505
+ );
506
+ }
507
+ };
508
+ for (const name of derived.keys()) {
509
+ if (split.behaviors.has(name)) continue;
510
+ if (split.queries.has(name)) check(name, "query", "components");
511
+ else if (split.transactions.has(name)) check(name, "command", "components");
512
+ else errs.push(`components: '${name}' has no declared publish origin (queries / transactions / behaviors) to verify against`);
513
+ }
514
+ for (const [cname, contract] of Object.entries(contracts)) {
515
+ for (const [mname, m] of Object.entries(contract.methods)) {
516
+ const at = `contract ${cname}.${mname}`;
517
+ if (contract.kind === "query") {
518
+ const q = m;
519
+ if (!split.queries.has(q.operation)) errs.push(`${at}: operation '${q.operation}' is not in queries`);
520
+ else check(q.operation, "query", at);
521
+ } else {
522
+ const cm = m;
523
+ for (const target of [cm.single, cm.batch]) {
524
+ if (target === void 0) continue;
525
+ if (target.mode === "transaction" || target.mode === "batchWrite") {
526
+ if (!split.transactions.has(target.transaction)) errs.push(`${at}: transaction '${target.transaction}' is not in transactions`);
527
+ else check(target.transaction, "command", at);
528
+ } else if (!split.commands.has(target.operation)) {
529
+ errs.push(`${at}: operation '${target.operation}' is not in commands`);
530
+ }
531
+ }
532
+ if (cm.returnSelection !== void 0) check(`${cname}__${mname}__readback`, "query", `${at} readback`);
533
+ }
534
+ }
535
+ }
536
+ if (errs.length > 0) {
537
+ throw new Error(
538
+ `graphddb CQRS classification: the declared publish split contradicts the graph-derived truth (${errs.length}):
539
+ - ${errs.join("\n - ")}`
540
+ );
541
+ }
542
+ }
543
+
544
+ // src/executor/handler-registry.ts
545
+ function resolveReadHandler(registry, name) {
546
+ const handler = registry[name];
547
+ if (handler === void 0) {
548
+ throw new Error(
549
+ `graphddb handler registry: no handler bound for catalog component '${name}' (known: ${Object.keys(registry).join(", ")})`
550
+ );
551
+ }
552
+ return handler;
553
+ }
554
+ function assertHandlersInCatalog(registry) {
555
+ for (const name of Object.keys(registry)) {
556
+ if (catalogEntry(name) === void 0) {
557
+ throw new Error(
558
+ `graphddb handler registry: handler '${name}' has no catalog entry (every bound handler must declare a Port schema in GRAPHDDB_CATALOG)`
559
+ );
560
+ }
561
+ }
562
+ }
563
+
340
564
  // src/executor/dynamo-executor.ts
565
+ var READ_HANDLERS = {
566
+ GetItem: (docClient, operation) => executeGetItem(docClient, operation),
567
+ Query: (docClient, operation) => executeQuery(docClient, operation),
568
+ BatchGetItem: (docClient, operation) => {
569
+ const op = operation;
570
+ return executeBatchGet(docClient, op);
571
+ }
572
+ };
573
+ assertHandlersInCatalog(READ_HANDLERS);
341
574
  var DynamoExecutor = class {
342
575
  async execute(operation, _options) {
343
576
  const docClient = await ClientManager.getDocumentClient();
344
- if (operation.type === "GetItem") {
345
- return executeGetItem(docClient, operation);
346
- }
347
- if (operation.type === "BatchGetItem") {
348
- return this.batchGet(operation);
349
- }
350
- return executeQuery(docClient, operation);
577
+ const handler = resolveReadHandler(READ_HANDLERS, operation.type);
578
+ return handler(docClient, operation);
351
579
  }
352
580
  async batchGet(input, _options) {
353
581
  const docClient = await ClientManager.getDocumentClient();
@@ -2448,6 +2676,11 @@ export {
2448
2676
  isParam,
2449
2677
  BATCH_GET_MAX_KEYS,
2450
2678
  chunkArray,
2679
+ GRAPHDDB_CATALOG,
2680
+ assertComponentsInCatalog,
2681
+ deriveContractEffect,
2682
+ deriveComponentEffects,
2683
+ assertPublishSplitMatchesDerived,
2451
2684
  ClientManager,
2452
2685
  isRawCondition,
2453
2686
  cond,