graphddb 0.8.1 → 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,
@@ -4680,8 +4680,16 @@ function buildCommandContract(methods) {
4680
4680
  const resolved = {};
4681
4681
  for (const [name, body] of Object.entries(methods)) {
4682
4682
  if (isPublicWriteDescriptor(body)) {
4683
- const { plan: plan2, select, condition, mode, description } = writeDescriptorToPlan(name, body);
4684
- 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
+ );
4685
4693
  continue;
4686
4694
  }
4687
4695
  if (isPublicCompositeDescriptor(body)) {
@@ -4771,7 +4779,7 @@ function buildPlannedCommandMethod(name, planned) {
4771
4779
  ...returnSelection !== void 0 ? { returnSelection } : {}
4772
4780
  };
4773
4781
  }
4774
- function finalizeDescriptorCommand(name, plan2, select, condition, mode, description) {
4782
+ function finalizeDescriptorCommand(name, plan2, select, condition, declaredMode, description, batchWriteEligible = false) {
4775
4783
  const planned = {
4776
4784
  [PLANNED_COMMAND_BRAND]: true,
4777
4785
  plan: plan2,
@@ -4779,6 +4787,13 @@ function finalizeDescriptorCommand(name, plan2, select, condition, mode, descrip
4779
4787
  select
4780
4788
  };
4781
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");
4782
4797
  const builtOp = built.op;
4783
4798
  const op = condition !== void 0 ? { ...builtOp, condition } : builtOp;
4784
4799
  return {
@@ -5015,7 +5030,8 @@ function writeDescriptorToPlan(name, d) {
5015
5030
  });
5016
5031
  const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
5017
5032
  const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
5018
- 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 };
5019
5035
  }
5020
5036
 
5021
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,