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/dist/index.js CHANGED
@@ -79,23 +79,19 @@ import {
79
79
  validateDepth,
80
80
  when,
81
81
  wholeKeysSentinel
82
- } from "./chunk-VST3WOK3.js";
82
+ } from "./chunk-PC54CW5P.js";
83
83
  import {
84
84
  CdcEmulator,
85
- InMemoryProjectionSink,
86
85
  MAINT_OUTBOX_PK_PREFIX,
87
86
  MaintenanceDrain,
88
- ProjectionSinkDrain,
89
87
  compareDesc,
90
88
  createCdcEmulator,
91
89
  createMaintenanceDrain,
92
90
  createMaintenanceDrainHandler,
93
- createProjectionSinkDrain,
94
- createProjectionSinkHandler,
95
91
  orderAndTrimCollection,
96
92
  pathField,
97
93
  projectFrom
98
- } from "./chunk-PNIZS37E.js";
94
+ } from "./chunk-GWFZVNAV.js";
99
95
  import {
100
96
  BATCH_GET_MAX_KEYS,
101
97
  BATCH_WRITE_MAX_ITEMS,
@@ -111,7 +107,6 @@ import {
111
107
  RetryingExecutor,
112
108
  TableMapping,
113
109
  TransactionContext,
114
- ViewRegistry,
115
110
  attachModelClass,
116
111
  buildConditionExpression,
117
112
  buildDeleteInput,
@@ -121,14 +116,10 @@ import {
121
116
  buildUpdateInput,
122
117
  collapseSameKeyItems,
123
118
  collectRawConditionParams,
119
+ collectViewDefinitions,
124
120
  commitTransaction,
125
121
  cond,
126
122
  createDefaultLinter,
127
- defineProjection,
128
- defineProjections,
129
- defineVersioned,
130
- defineView,
131
- defineViews,
132
123
  entityWrites,
133
124
  execItemKeySignature,
134
125
  executeDelete,
@@ -145,11 +136,9 @@ import {
145
136
  isLifecycleContract,
146
137
  isMaintainTrigger,
147
138
  isParam,
148
- isProjectionDefinition,
149
139
  isRawCondition,
150
140
  isRetryableError,
151
141
  isRetryableTransactionCancellation,
152
- isViewDefinition,
153
142
  k,
154
143
  key,
155
144
  lifecyclePhaseForIntent,
@@ -164,9 +153,8 @@ import {
164
153
  resolveKey,
165
154
  resolveModelClass,
166
155
  segmentFieldNames,
167
- serializeFieldValue,
168
- whenMember
169
- } from "./chunk-YIXXTGZ6.js";
156
+ serializeFieldValue
157
+ } from "./chunk-72VZYOSG.js";
170
158
 
171
159
  // src/metadata/prefix.ts
172
160
  function derivePrefix(customPrefix, className) {
@@ -217,6 +205,7 @@ var pendingFields = [];
217
205
  var pendingEmbedded = [];
218
206
  var pendingRelations = [];
219
207
  var pendingAggregates = [];
208
+ var pendingMaintainedFrom = [];
220
209
  function collectField(field2) {
221
210
  pendingFields.push(field2);
222
211
  }
@@ -229,6 +218,9 @@ function collectRelation(relation) {
229
218
  function collectAggregate(aggregate2) {
230
219
  pendingAggregates.push(aggregate2);
231
220
  }
221
+ function collectMaintainedFrom(decl) {
222
+ pendingMaintainedFrom.push(decl);
223
+ }
232
224
  function drainFields() {
233
225
  const result = pendingFields;
234
226
  pendingFields = [];
@@ -249,6 +241,11 @@ function drainAggregates() {
249
241
  pendingAggregates = [];
250
242
  return result;
251
243
  }
244
+ function drainMaintainedFrom() {
245
+ const result = pendingMaintainedFrom;
246
+ pendingMaintainedFrom = [];
247
+ return result;
248
+ }
252
249
 
253
250
  // src/decorators/model.ts
254
251
  function model(options) {
@@ -257,7 +254,19 @@ function model(options) {
257
254
  const embeddedFields = drainEmbedded();
258
255
  const relations = drainRelations();
259
256
  const aggregates = drainAggregates();
257
+ const maintainedFrom2 = drainMaintainedFrom();
260
258
  const prefix = derivePrefix(options.prefix, context.name);
259
+ const kind = options.kind ?? "entity";
260
+ if (kind === "entity" && maintainedFrom2.length > 0) {
261
+ throw new Error(
262
+ `@model on '${String(context.name)}': ${maintainedFrom2.length} \`@maintainedFrom\` declaration(s) are present but \`kind\` is 'entity' (the default). A maintained view must declare \`@model({ kind: 'materializedView' | 'sparseView' })\` \u2014 a plain entity does not carry view maintainers.`
263
+ );
264
+ }
265
+ if (kind !== "entity" && maintainedFrom2.length === 0) {
266
+ throw new Error(
267
+ `@model on '${String(context.name)}': \`kind: '${kind}'\` declares a maintained view but no \`@maintainedFrom(...)\` source is stacked. A view with no source is maintained by nothing \u2014 add at least one \`@maintainedFrom\` decorator.`
268
+ );
269
+ }
261
270
  MetadataRegistry.register(target, {
262
271
  tableName: options.table,
263
272
  prefix,
@@ -266,7 +275,9 @@ function model(options) {
266
275
  gsiDefinitions: [],
267
276
  relations,
268
277
  aggregates,
269
- embeddedFields
278
+ embeddedFields,
279
+ kind,
280
+ maintainedFrom: maintainedFrom2
270
281
  });
271
282
  };
272
283
  }
@@ -336,7 +347,95 @@ function embedded(modelFactory) {
336
347
  };
337
348
  }
338
349
 
350
+ // src/decorators/maintained-symbolic.ts
351
+ var SOURCE_REF = /* @__PURE__ */ Symbol("graphddb:maintainedSourceRef");
352
+ var SELF_REF = /* @__PURE__ */ Symbol("graphddb:maintainedSelfRef");
353
+ function isSourceRef(value) {
354
+ return typeof value === "object" && value !== null && value[SOURCE_REF] === true;
355
+ }
356
+ function isSelfRef(value) {
357
+ return typeof value === "object" && value !== null && value[SELF_REF] === true;
358
+ }
359
+ function createSourceProxy() {
360
+ const cache = /* @__PURE__ */ new Map();
361
+ return new Proxy(
362
+ {},
363
+ {
364
+ get(_t, prop) {
365
+ if (typeof prop !== "string") return void 0;
366
+ let ref = cache.get(prop);
367
+ if (!ref) {
368
+ ref = { [SOURCE_REF]: true, field: prop, path: `$.entity.${prop}` };
369
+ cache.set(prop, ref);
370
+ }
371
+ return ref;
372
+ }
373
+ }
374
+ );
375
+ }
376
+ function createSelfProxy() {
377
+ const cache = /* @__PURE__ */ new Map();
378
+ return new Proxy(
379
+ {},
380
+ {
381
+ get(_t, prop) {
382
+ if (typeof prop !== "string") return void 0;
383
+ let ref = cache.get(prop);
384
+ if (!ref) {
385
+ ref = { [SELF_REF]: true, field: prop };
386
+ cache.set(prop, ref);
387
+ }
388
+ return ref;
389
+ }
390
+ }
391
+ );
392
+ }
393
+
339
394
  // src/decorators/relation.ts
395
+ function lowerVersionedProjectionValue(propertyName, attr, value) {
396
+ if (isSourceRef(value)) return identity(value.path);
397
+ if (value && typeof value === "object" && value.kind === "transform") {
398
+ return value;
399
+ }
400
+ throw new Error(
401
+ `@hasOne/@hasMany versioned relation '${propertyName}': projection target '${attr}' must be a \`source.<field>\` reference or a transform (e.g. \`preview(source.body, 120)\`), got ${JSON.stringify(value)}.`
402
+ );
403
+ }
404
+ function lowerVersionedOptions(propertyName, opts) {
405
+ if (!opts || typeof opts !== "object" || opts.pattern !== "versionedLatest" && opts.pattern !== "versionedHistory") {
406
+ throw new Error(
407
+ `@hasOne/@hasMany versioned relation '${propertyName}': the (self, source) => options callback must return \`{ pattern: 'versionedLatest' | 'versionedHistory', on, project }\`.`
408
+ );
409
+ }
410
+ if (!Array.isArray(opts.on) || opts.on.length === 0) {
411
+ throw new Error(
412
+ `@hasOne/@hasMany versioned relation '${propertyName}': \`on\` must be a non-empty array of lifecycle events (e.g. \`['created', 'updated']\`).`
413
+ );
414
+ }
415
+ const projectEntries = Object.entries(opts.project ?? {});
416
+ if (projectEntries.length === 0) {
417
+ throw new Error(
418
+ `@hasOne/@hasMany versioned relation '${propertyName}': \`project\` is empty \u2014 a versioned row with no captured attributes projects nothing.`
419
+ );
420
+ }
421
+ const project = {};
422
+ for (const [attr, value] of projectEntries) {
423
+ project[attr] = lowerVersionedProjectionValue(propertyName, attr, value);
424
+ }
425
+ return {
426
+ pattern: opts.pattern,
427
+ versioned: {
428
+ mode: opts.pattern === "versionedLatest" ? "latest" : "history",
429
+ on: [...opts.on],
430
+ project,
431
+ ...opts.consistency !== void 0 ? { consistency: opts.consistency } : {},
432
+ ...opts.updateMode !== void 0 ? { updateMode: opts.updateMode } : {}
433
+ }
434
+ };
435
+ }
436
+ function isVersionedCallback(arg) {
437
+ return typeof arg === "function";
438
+ }
340
439
  var RESERVED_RELATION_PROPERTY_NAMES = /* @__PURE__ */ new Set([
341
440
  "items"
342
441
  ]);
@@ -347,15 +446,34 @@ function assertRelationPropertyNameAllowed(propertyName) {
347
446
  );
348
447
  }
349
448
  }
350
- function hasMany(targetFactory, keyBinding, options) {
449
+ function hasMany(targetFactory, keyBindingOrCallback, options) {
351
450
  return function(_value, context) {
352
451
  const propertyName = String(context.name);
353
452
  assertRelationPropertyNameAllowed(propertyName);
453
+ if (isVersionedCallback(keyBindingOrCallback)) {
454
+ const opts = keyBindingOrCallback(
455
+ createSelfProxy(),
456
+ createSourceProxy()
457
+ );
458
+ if (opts.pattern !== "versionedHistory") {
459
+ throw new Error(
460
+ `@hasMany versioned relation '${propertyName}': a \`@hasMany\` maintains an append-only HISTORY, so its pattern must be 'versionedHistory' (got '${opts.pattern}'). Use \`@hasOne\` for 'versionedLatest' (the single overwritten pointer).`
461
+ );
462
+ }
463
+ collectRelation({
464
+ type: "hasMany",
465
+ propertyName,
466
+ targetFactory,
467
+ keyBinding: {},
468
+ options: lowerVersionedOptions(propertyName, opts)
469
+ });
470
+ return;
471
+ }
354
472
  collectRelation({
355
473
  type: "hasMany",
356
474
  propertyName,
357
475
  targetFactory,
358
- keyBinding,
476
+ keyBinding: keyBindingOrCallback,
359
477
  options
360
478
  });
361
479
  };
@@ -373,15 +491,34 @@ function belongsTo(targetFactory, keyBinding, options) {
373
491
  });
374
492
  };
375
493
  }
376
- function hasOne(targetFactory, keyBinding) {
494
+ function hasOne(targetFactory, keyBindingOrCallback) {
377
495
  return function(_value, context) {
378
496
  const propertyName = String(context.name);
379
497
  assertRelationPropertyNameAllowed(propertyName);
498
+ if (isVersionedCallback(keyBindingOrCallback)) {
499
+ const opts = keyBindingOrCallback(
500
+ createSelfProxy(),
501
+ createSourceProxy()
502
+ );
503
+ if (opts.pattern !== "versionedLatest") {
504
+ throw new Error(
505
+ `@hasOne versioned relation '${propertyName}': a \`@hasOne\` maintains a single LATEST pointer (overwritten per revision), so its pattern must be 'versionedLatest' (got '${opts.pattern}'). Use \`@hasMany\` for 'versionedHistory' (append-only per-version rows).`
506
+ );
507
+ }
508
+ collectRelation({
509
+ type: "hasOne",
510
+ propertyName,
511
+ targetFactory,
512
+ keyBinding: {},
513
+ options: lowerVersionedOptions(propertyName, opts)
514
+ });
515
+ return;
516
+ }
380
517
  collectRelation({
381
518
  type: "hasOne",
382
519
  propertyName,
383
520
  targetFactory,
384
- keyBinding
521
+ keyBinding: keyBindingOrCallback
385
522
  });
386
523
  };
387
524
  }
@@ -409,6 +546,118 @@ function max(field2) {
409
546
  return { op: "max", field: field2 };
410
547
  }
411
548
 
549
+ // src/decorators/maintained.ts
550
+ function whenMember(operand, op, value) {
551
+ const raw = isSourceRef(operand) ? operand.path : operand;
552
+ const path = raw.startsWith("$.input.") || raw.startsWith("$.entity.") ? raw : `$.entity.${raw}`;
553
+ const needsValue = op === "eq" || op === "ne";
554
+ if (needsValue && value === void 0) {
555
+ throw new Error(
556
+ `whenMember(source.\u2026, '${op}', \u2026): the '${op}' op requires a comparand value (e.g. \`whenMember(source.status, '${op}', 'active')\`).`
557
+ );
558
+ }
559
+ return needsValue ? { path, op, value } : { path, op };
560
+ }
561
+ function lowerProjectionValue(modelName, attr, value) {
562
+ if (isSourceRef(value)) return identity(value.path);
563
+ if (value && typeof value === "object" && value.kind === "transform") {
564
+ return value;
565
+ }
566
+ throw new Error(
567
+ `@maintainedFrom on '${modelName}': projection target '${attr}' must be a \`source.<field>\` reference or a transform (e.g. \`preview(source.body, 120)\`), got ${JSON.stringify(value)}. A maintained projection captures only symbolic source references \u2014 a literal / computed value is not allowed.`
568
+ );
569
+ }
570
+ function lowerKeyBind(modelName, keyBind) {
571
+ const out = {};
572
+ const entries = Object.entries(keyBind);
573
+ if (entries.length === 0) {
574
+ throw new Error(
575
+ `@maintainedFrom on '${modelName}': \`keyBind\` is empty. The view-row key binding (view-key field \u2192 \`source.<field>\`) is required to resolve which view row a source event maintains.`
576
+ );
577
+ }
578
+ for (const [viewKeyField, ref] of entries) {
579
+ if (!isSourceRef(ref)) {
580
+ throw new Error(
581
+ `@maintainedFrom on '${modelName}': \`keyBind.${viewKeyField}\` must be a \`source.<field>\` reference (e.g. \`{ ${viewKeyField}: source.${viewKeyField} }\`), got ${JSON.stringify(ref)}.`
582
+ );
583
+ }
584
+ out[viewKeyField] = ref.path;
585
+ }
586
+ return out;
587
+ }
588
+ function maintainedFrom(source, callback) {
589
+ return function(target, context) {
590
+ const modelName = String(context.name ?? target.name ?? "(anonymous)");
591
+ const selfProxy = createSelfProxy();
592
+ const sourceProxy = createSourceProxy();
593
+ const opts = callback(selfProxy, sourceProxy);
594
+ if (!opts || typeof opts !== "object") {
595
+ throw new Error(
596
+ `@maintainedFrom on '${modelName}': the (self, source) => options callback must return an options object.`
597
+ );
598
+ }
599
+ if (!Array.isArray(opts.on) || opts.on.length === 0) {
600
+ throw new Error(
601
+ `@maintainedFrom on '${modelName}': \`on\` must be a non-empty array of lifecycle events (e.g. \`['created', 'removed']\`).`
602
+ );
603
+ }
604
+ for (const ev of opts.on) {
605
+ if (ev !== "created" && ev !== "updated" && ev !== "removed") {
606
+ throw new Error(
607
+ `@maintainedFrom on '${modelName}': \`on\` event ${JSON.stringify(ev)} is not one of 'created' / 'updated' / 'removed'.`
608
+ );
609
+ }
610
+ }
611
+ const keyBind = lowerKeyBind(modelName, opts.keyBind);
612
+ const projectEntries = Object.entries(opts.project ?? {});
613
+ if (projectEntries.length === 0) {
614
+ throw new Error(
615
+ `@maintainedFrom on '${modelName}': \`project\` is empty. A maintained view with no captured attributes projects nothing \u2014 declare a \`project\` map (e.g. \`{ title: source.title, textPreview: preview(source.body, 120) }\`).`
616
+ );
617
+ }
618
+ const project = {};
619
+ for (const [attr, value] of projectEntries) {
620
+ project[attr] = lowerProjectionValue(modelName, attr, value);
621
+ }
622
+ if (opts.collection && opts.when) {
623
+ throw new Error(
624
+ `@maintainedFrom on '${modelName}': a declaration carries both \`collection\` and \`when\`. They are mutually exclusive \u2014 a collection slice holds a maintained list; a membership (\`when\`) row appears/disappears as a whole.`
625
+ );
626
+ }
627
+ let collection;
628
+ if (opts.collection) {
629
+ const c = opts.collection;
630
+ if (!isSelfRef(c.field)) {
631
+ throw new Error(
632
+ `@maintainedFrom on '${modelName}': \`collection.field\` must be a \`self.<field>\` reference (the view attribute holding the list, e.g. \`self.latestPosts\`), got ${JSON.stringify(c.field)}.`
633
+ );
634
+ }
635
+ if (c.orderBy !== void 0 && !isSourceRef(c.orderBy)) {
636
+ throw new Error(
637
+ `@maintainedFrom on '${modelName}': \`collection.orderBy\` must be a \`source.<field>\` reference, got ${JSON.stringify(c.orderBy)}.`
638
+ );
639
+ }
640
+ collection = {
641
+ field: c.field.field,
642
+ ...c.maxItems !== void 0 ? { maxItems: c.maxItems } : {},
643
+ ...c.order !== void 0 ? { order: c.order } : {},
644
+ ...c.orderBy !== void 0 ? { orderBy: c.orderBy.path } : {}
645
+ };
646
+ }
647
+ const metadata = {
648
+ sourceFactory: source,
649
+ on: [...opts.on],
650
+ keyBind,
651
+ project,
652
+ ...collection ? { collection } : {},
653
+ ...opts.when ? { when: opts.when } : {},
654
+ ...opts.consistency !== void 0 ? { consistency: opts.consistency } : {},
655
+ ...opts.updateMode !== void 0 ? { updateMode: opts.updateMode } : {}
656
+ };
657
+ collectMaintainedFrom(metadata);
658
+ };
659
+ }
660
+
412
661
  // src/operations/declarative-transaction.ts
413
662
  var PLACEHOLDER_RE = /\{[^{}]+\}/g;
414
663
  function resolveTemplate(template, params, element) {
@@ -989,10 +1238,13 @@ var MaintenanceRebuilder = class {
989
1238
  graph;
990
1239
  constructor(opts = {}) {
991
1240
  const classes = (opts.models ?? []).map(toClass);
992
- const views = opts.views ?? [];
993
- const allClasses = [...classes, ...views.map((v) => v.viewClass)];
1241
+ const explicitViews = opts.views;
1242
+ const allClasses = [
1243
+ ...classes,
1244
+ ...(explicitViews ?? []).map((v) => v.viewClass)
1245
+ ];
994
1246
  const scoped = allClasses.length > 0 ? new Map(allClasses.map((c) => [c, MetadataRegistry.get(c)])) : void 0;
995
- this.graph = buildMaintenanceGraph(scoped, views);
1247
+ this.graph = buildMaintenanceGraph(scoped, explicitViews);
996
1248
  }
997
1249
  /**
998
1250
  * Re-derive the materialized value for one owner row × maintainer from the source
@@ -1546,7 +1798,6 @@ export {
1546
1798
  DynamoExecutor,
1547
1799
  EDGE_WRITES_MARKER,
1548
1800
  ENTITY_WRITES_MARKER,
1549
- InMemoryProjectionSink,
1550
1801
  LIFECYCLE_CONTRACT_MARKER,
1551
1802
  Linter,
1552
1803
  MAINT_OUTBOX_PK_PREFIX,
@@ -1555,12 +1806,10 @@ export {
1555
1806
  MaintenanceRebuilder,
1556
1807
  MetadataRegistry,
1557
1808
  OLD_VALUE_NAMESPACE,
1558
- ProjectionSinkDrain,
1559
1809
  RetryingExecutor,
1560
1810
  SPEC_VERSION,
1561
1811
  TableMapping,
1562
1812
  TransactionContext,
1563
- ViewRegistry,
1564
1813
  aggregate,
1565
1814
  assertBundleSerializable,
1566
1815
  assertContractBoundaries,
@@ -1588,6 +1837,7 @@ export {
1588
1837
  buildUpdateInput,
1589
1838
  collectContractBoundaryViolations,
1590
1839
  collectContractN1Violations,
1840
+ collectViewDefinitions,
1591
1841
  compileFilterExpression,
1592
1842
  compileFragment,
1593
1843
  compileMutationPlan,
@@ -1600,8 +1850,6 @@ export {
1600
1850
  createMaintenanceDrain,
1601
1851
  createMaintenanceDrainHandler,
1602
1852
  createMaintenanceRebuilder,
1603
- createProjectionSinkDrain,
1604
- createProjectionSinkHandler,
1605
1853
  datetime,
1606
1854
  decodeCursor,
1607
1855
  decodePerKeyCursor,
@@ -1609,17 +1857,12 @@ export {
1609
1857
  defineDelete,
1610
1858
  defineList,
1611
1859
  definePlan,
1612
- defineProjection,
1613
- defineProjections,
1614
1860
  definePut,
1615
1861
  defineQueries,
1616
1862
  defineQuery,
1617
1863
  defineTransaction,
1618
1864
  defineTransactions,
1619
1865
  defineUpdate,
1620
- defineVersioned,
1621
- defineView,
1622
- defineViews,
1623
1866
  deriveEdgeWriteItems,
1624
1867
  deriveEdgeWriteItemsFor,
1625
1868
  deriveModelEdgeWriteItems,
@@ -1675,19 +1918,18 @@ export {
1675
1918
  isMutationInputRef,
1676
1919
  isParam,
1677
1920
  isPlannedCommandMethod,
1678
- isProjectionDefinition,
1679
1921
  isQueryModelContract,
1680
1922
  isRetryableError,
1681
1923
  isRetryableTransactionCancellation,
1682
1924
  isSelectBuilder,
1683
1925
  isTransactionRef,
1684
- isViewDefinition,
1685
1926
  k,
1686
1927
  key,
1687
1928
  lifecyclePhaseForIntent,
1688
1929
  list,
1689
1930
  literal,
1690
1931
  maintainTrigger,
1932
+ maintainedFrom,
1691
1933
  map,
1692
1934
  max,
1693
1935
  mintContractKeyFieldRef,
@@ -1,4 +1,4 @@
1
- import { E as Executor, D as DynamoDBOperation, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, M as ModelStatic, f as DDBModel, C as ChangeEvent } from '../types-DuJ08bgc.js';
1
+ import { E as Executor, D as DynamoDBOperation, R as ReadExecOptions, a as ExecutorResult, B as BatchGetExecInput, P as PutInput, W as WriteExecOptions, b as WriteResult, U as UpdateInput, c as DeleteInput, d as BatchWriteExecItem, e as BatchExecOptions, T as TransactWriteExecItem, M as ModelStatic, f as DDBModel, C as ChangeEvent } from '../types-CpCo8yHP.js';
2
2
  import '@aws-sdk/client-dynamodb';
3
3
 
4
4
  /**
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  createCdcEmulator
3
- } from "../chunk-PNIZS37E.js";
3
+ } from "../chunk-GWFZVNAV.js";
4
4
  import {
5
5
  ClientManager,
6
6
  MetadataRegistry,
7
7
  TableMapping,
8
8
  resolveModelClass
9
- } from "../chunk-YIXXTGZ6.js";
9
+ } from "../chunk-72VZYOSG.js";
10
10
 
11
11
  // src/memory/memory-store.ts
12
12
  function deepClone(value) {