graphddb 0.5.2 → 0.6.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,103 +1,13 @@
1
- // src/select/column.ts
2
- var COLUMN_REF = /* @__PURE__ */ Symbol.for("graphddb.columnRef");
3
- function isColumn(value) {
4
- return typeof value === "object" && value !== null && value[COLUMN_REF] === true;
5
- }
6
- function createColumnMap() {
7
- const cache = /* @__PURE__ */ new Map();
8
- return new Proxy(
9
- {},
10
- {
11
- get(_target, prop) {
12
- if (typeof prop !== "string") return void 0;
13
- let col = cache.get(prop);
14
- if (!col) {
15
- col = { [COLUMN_REF]: true, name: prop };
16
- cache.set(prop, col);
17
- }
18
- return col;
19
- }
20
- }
21
- );
22
- }
23
-
24
- // src/decorators/key.ts
25
- var KEY_MARKER = /* @__PURE__ */ Symbol("graphddb:key");
26
- var KEY_SEGMENT = /* @__PURE__ */ Symbol("graphddb:keySegment");
27
- function isKeySegment(value) {
28
- return typeof value === "object" && value !== null && value[KEY_SEGMENT] === true;
29
- }
30
- function k(literals, ...columns) {
31
- for (let i = 0; i < columns.length; i++) {
32
- if (!isColumn(columns[i])) {
33
- throw new Error(
34
- `k\`...\`: interpolation #${i} must be a typed column reference (e.g. \`c.userId\`), not ${typeof columns[i]}. Key segments name fields via column references so they stay type-safe.`
35
- );
36
- }
37
- }
38
- return {
39
- [KEY_SEGMENT]: true,
40
- literals: [...literals],
41
- columns
42
- };
43
- }
44
- function isKeyDefinition(value) {
45
- return typeof value === "object" && value !== null && KEY_MARKER in value;
46
- }
47
- function normalizeSegments(spec) {
48
- if (spec === void 0) return [];
49
- const list = Array.isArray(spec) ? spec : [spec];
50
- for (const seg of list) {
51
- if (!isKeySegment(seg)) {
52
- throw new Error(
53
- "key()/gsi(): pk and sk must be built with the `k` tagged template (e.g. k`USER#${c.userId}`) or an array of them."
54
- );
55
- }
56
- }
57
- return list;
58
- }
59
- function segmentFieldNames(segments) {
60
- const seen = /* @__PURE__ */ new Set();
61
- const out = [];
62
- for (const seg of segments) {
63
- for (const col of seg.columns) {
64
- if (!seen.has(col.name)) {
65
- seen.add(col.name);
66
- out.push(col.name);
67
- }
68
- }
69
- }
70
- return out;
71
- }
72
- function buildSegmentedKey(structure) {
73
- const pkSegments = normalizeSegments(structure.pk);
74
- const skSegments = normalizeSegments(structure.sk);
75
- if (pkSegments.length === 0) {
76
- throw new Error("key()/gsi(): pk must define at least one segment.");
77
- }
78
- const seen = /* @__PURE__ */ new Set();
79
- const inputFieldNames = [];
80
- for (const name of [
81
- ...segmentFieldNames(pkSegments),
82
- ...segmentFieldNames(skSegments)
83
- ]) {
84
- if (!seen.has(name)) {
85
- seen.add(name);
86
- inputFieldNames.push(name);
87
- }
88
- }
89
- return { segmented: { pkSegments, skSegments }, inputFieldNames };
90
- }
91
- function key(builder) {
92
- const accessor = createColumnMap();
93
- const structure = builder(accessor);
94
- const { segmented, inputFieldNames } = buildSegmentedKey(structure);
95
- return {
96
- [KEY_MARKER]: true,
97
- segmented,
98
- inputFieldNames
99
- };
100
- }
1
+ import {
2
+ TableMapping,
3
+ buildSegmentedKey,
4
+ createColumnMap,
5
+ createDefaultLinter,
6
+ isColumn,
7
+ isKeyDefinition,
8
+ resolveKey,
9
+ segmentFieldNames
10
+ } from "./chunk-PDUVTYC5.js";
101
11
 
102
12
  // src/decorators/gsi.ts
103
13
  var GSI_MARKER = /* @__PURE__ */ Symbol("graphddb:gsi");
@@ -120,875 +30,13 @@ function gsi(indexName, builder, options) {
120
30
  };
121
31
  }
122
32
 
123
- // src/linter/linter.ts
124
- var Linter = class {
125
- rules = [];
126
- addRule(rule) {
127
- this.rules.push(rule);
128
- }
129
- run(metadata, registry) {
130
- const results = [];
131
- for (const rule of this.rules) {
132
- results.push(...rule.check(metadata, registry));
133
- }
134
- return results;
135
- }
136
- runAll(registry) {
137
- const results = [];
138
- const all = registry.getAll();
139
- for (const [, metadata] of all) {
140
- results.push(...this.run(metadata, registry));
141
- }
142
- return results;
143
- }
144
- };
145
-
146
- // src/linter/rules/no-scan.ts
147
- var noScanRule = {
148
- id: "no-scan",
149
- severity: "error",
150
- check(metadata) {
151
- if (metadata.primaryKey === null && metadata.gsiDefinitions.length === 0) {
152
- return [
153
- {
154
- ruleId: "no-scan",
155
- severity: "error",
156
- message: `Entity '${metadata.prefix}' has no primaryKey or GSI defined. All reads would require a full table scan.`,
157
- entity: metadata.prefix
158
- }
159
- ];
160
- }
161
- return [];
162
- }
163
- };
164
-
165
- // src/linter/rules/require-limit.ts
166
- var requireLimitRule = {
167
- id: "require-limit",
168
- severity: "warning",
169
- check(metadata) {
170
- const results = [];
171
- for (const relation of metadata.relations) {
172
- if (relation.type !== "hasMany") continue;
173
- if (relation.options?.versioned) continue;
174
- const limit = relation.options?.limit;
175
- if (!limit || limit.default === void 0 || limit.max === void 0) {
176
- const kb = Object.keys(relation.keyBinding).join(", ");
177
- results.push({
178
- ruleId: "require-limit",
179
- severity: "warning",
180
- message: `hasMany relation (keyBinding: [${kb}]) on entity '${metadata.prefix}' is missing limit.default or limit.max. Unbounded queries may consume excessive RCU.`,
181
- entity: metadata.prefix
182
- });
183
- }
184
- }
185
- return results;
186
- }
187
- };
188
-
189
- // src/linter/rules/gsi-ambiguity.ts
190
- function isSubsetOf(a, b) {
191
- const setB = new Set(b);
192
- return a.length > 0 && a.every((f) => setB.has(f));
193
- }
194
- var gsiAmbiguityRule = {
195
- id: "gsi-ambiguity",
196
- severity: "error",
197
- check(metadata) {
198
- const results = [];
199
- const patterns = [];
200
- if (metadata.primaryKey) {
201
- patterns.push({
202
- label: "primaryKey",
203
- fieldNames: metadata.primaryKey.inputFieldNames
204
- });
205
- }
206
- for (const gsi2 of metadata.gsiDefinitions) {
207
- patterns.push({ label: gsi2.indexName, fieldNames: gsi2.inputFieldNames });
208
- }
209
- for (let i = 0; i < patterns.length; i++) {
210
- for (let j = i + 1; j < patterns.length; j++) {
211
- const a = patterns[i];
212
- const b = patterns[j];
213
- if (isSubsetOf(a.fieldNames, b.fieldNames) || isSubsetOf(b.fieldNames, a.fieldNames)) {
214
- const smaller = a.fieldNames.length <= b.fieldNames.length ? a : b;
215
- results.push({
216
- ruleId: "gsi-ambiguity",
217
- severity: "error",
218
- message: `Ambiguous GSI resolution: field set [${smaller.fieldNames.join(", ")}] matches both '${a.label}' and '${b.label}'. Each access pattern must have a unique field combination.`,
219
- entity: metadata.prefix,
220
- field: smaller.fieldNames.join(", ")
221
- });
222
- }
223
- }
224
- }
225
- return results;
226
- }
227
- };
228
-
229
- // src/planner/key-resolver.ts
230
- function fieldSetsEqual(a, b) {
231
- if (a.length !== b.length) return false;
232
- const sorted = [...b].sort();
233
- return [...a].sort().every((v, i) => v === sorted[i]);
234
- }
235
- function classifySegmentMatch(segmented, queryFields) {
236
- const pkFields = segmentFieldNames(segmented.pkSegments);
237
- const present = new Set(queryFields);
238
- const allFields = /* @__PURE__ */ new Set([
239
- ...pkFields,
240
- ...segmentFieldNames(segmented.skSegments)
241
- ]);
242
- for (const f of queryFields) {
243
- if (!allFields.has(f)) return null;
244
- }
245
- for (const f of pkFields) {
246
- if (!present.has(f)) return null;
247
- }
248
- const sk = segmented.skSegments;
249
- let consumed = 0;
250
- let truncated = false;
251
- for (let i = 0; i < sk.length; i++) {
252
- const fields = sk[i].columns.map((c) => c.name);
253
- const all = fields.every((f) => present.has(f));
254
- const any = fields.some((f) => present.has(f));
255
- if (all && fields.length > 0) {
256
- if (truncated) return null;
257
- consumed = i + 1;
258
- continue;
259
- }
260
- if (fields.length === 0) {
261
- if (!truncated) consumed = i + 1;
262
- continue;
263
- }
264
- if (any) return null;
265
- truncated = true;
266
- }
267
- const skFieldCount = segmentFieldNames(sk).length;
268
- const allSkPresent = segmentFieldNames(sk).every((f) => present.has(f));
269
- if (skFieldCount === 0 || allSkPresent && consumed === sk.length) {
270
- return "full";
271
- }
272
- return "partial";
273
- }
274
- function resolveKey(queryFields, metadata) {
275
- if (metadata.primaryKey && fieldSetsEqual(queryFields, metadata.primaryKey.inputFieldNames)) {
276
- return {
277
- type: "pk",
278
- partial: false,
279
- inputFieldNames: metadata.primaryKey.inputFieldNames
280
- };
281
- }
282
- for (const gsi2 of metadata.gsiDefinitions) {
283
- if (fieldSetsEqual(queryFields, gsi2.inputFieldNames)) {
284
- return {
285
- type: "gsi",
286
- indexName: gsi2.indexName,
287
- unique: gsi2.unique,
288
- partial: false,
289
- inputFieldNames: gsi2.inputFieldNames
290
- };
291
- }
292
- }
293
- if (metadata.primaryKey) {
294
- const match = classifySegmentMatch(metadata.primaryKey.segmented, queryFields);
295
- if (match === "partial") {
296
- return { type: "pk", partial: true, inputFieldNames: queryFields };
297
- }
298
- if (match === "full") {
299
- return { type: "pk", partial: false, inputFieldNames: queryFields };
300
- }
301
- }
302
- for (const gsi2 of metadata.gsiDefinitions) {
303
- const match = classifySegmentMatch(gsi2.segmented, queryFields);
304
- if (match === "partial") {
305
- return {
306
- type: "gsi",
307
- indexName: gsi2.indexName,
308
- unique: false,
309
- partial: true,
310
- inputFieldNames: queryFields
311
- };
312
- }
313
- if (match === "full") {
314
- return {
315
- type: "gsi",
316
- indexName: gsi2.indexName,
317
- unique: gsi2.unique,
318
- partial: false,
319
- inputFieldNames: queryFields
320
- };
321
- }
322
- }
323
- throw new Error(
324
- `No access pattern found for fields [${queryFields.join(", ")}]. Available: primaryKey([${metadata.primaryKey?.inputFieldNames.join(", ") ?? ""}])` + metadata.gsiDefinitions.map((g) => `, ${g.indexName}([${g.inputFieldNames.join(", ")}])`).join("")
325
- );
326
- }
327
-
328
- // src/linter/rules/missing-gsi.ts
329
- var missingGsiRule = {
330
- id: "missing-gsi",
331
- severity: "error",
332
- check(metadata, registry) {
333
- const results = [];
334
- for (const relation of metadata.relations) {
335
- const targetFields = Object.keys(relation.keyBinding);
336
- if (targetFields.length === 0) {
337
- continue;
338
- }
339
- let targetMeta;
340
- try {
341
- const targetClass = relation.targetFactory();
342
- targetMeta = registry.get(targetClass);
343
- } catch {
344
- results.push({
345
- ruleId: "missing-gsi",
346
- severity: "error",
347
- message: `Relation '${relation.propertyName}' on entity '${metadata.prefix}' targets an entity that is not registered in MetadataRegistry.`,
348
- entity: metadata.prefix,
349
- field: relation.propertyName
350
- });
351
- continue;
352
- }
353
- try {
354
- resolveKey(targetFields, targetMeta);
355
- } catch {
356
- results.push({
357
- ruleId: "missing-gsi",
358
- severity: "error",
359
- message: `Relation '${relation.propertyName}' on entity '${metadata.prefix}' requires access pattern [${targetFields.join(", ")}] on target entity '${targetMeta.prefix}', but no matching primary key or GSI is defined.`,
360
- entity: metadata.prefix,
361
- field: relation.propertyName
362
- });
363
- }
364
- }
365
- return results;
366
- }
367
- };
368
-
369
- // src/linter/rules/relation-depth.ts
370
- var DEFAULT_MAX_DEPTH = 1;
371
- function computeRelationDepth(metadata, registry, visited = /* @__PURE__ */ new Set()) {
372
- if (metadata.relations.length === 0) {
373
- return 0;
374
- }
375
- let maxDepth = 0;
376
- for (const relation of metadata.relations) {
377
- let targetClass;
378
- try {
379
- targetClass = relation.targetFactory();
380
- } catch {
381
- continue;
382
- }
383
- if (visited.has(targetClass)) {
384
- continue;
385
- }
386
- let targetMeta;
387
- try {
388
- targetMeta = registry.get(targetClass);
389
- } catch {
390
- continue;
391
- }
392
- const nextVisited = new Set(visited);
393
- nextVisited.add(targetClass);
394
- const childDepth = 1 + computeRelationDepth(targetMeta, registry, nextVisited);
395
- maxDepth = Math.max(maxDepth, childDepth);
396
- }
397
- return maxDepth;
398
- }
399
- var relationDepthRule = {
400
- id: "relation-depth",
401
- severity: "warning",
402
- check(metadata, registry) {
403
- const depth = computeRelationDepth(metadata, registry);
404
- if (depth <= DEFAULT_MAX_DEPTH) {
405
- return [];
406
- }
407
- return [
408
- {
409
- ruleId: "relation-depth",
410
- severity: "warning",
411
- message: `Entity '${metadata.prefix}' has relation paths exceeding default maxDepth ${DEFAULT_MAX_DEPTH} (detected depth: ${depth}). Deep traversal requires explicit maxDepth at runtime.`,
412
- entity: metadata.prefix
413
- }
414
- ];
415
- }
416
- };
417
-
418
- // src/client/TableMapping.ts
419
- var TableMapping = class {
420
- static mapping = {};
421
- static set(mapping) {
422
- this.mapping = { ...mapping };
423
- }
424
- static resolve(declaredName) {
425
- return this.mapping[declaredName] ?? declaredName;
426
- }
427
- /** @internal — for testing only */
428
- static reset() {
429
- this.mapping = {};
430
- }
431
- };
432
-
433
- // src/linter/rules/same-partition-preset.ts
434
- var samePartitionPresetRule = {
435
- id: "same-partition-preset",
436
- severity: "error",
437
- check(metadata, registry) {
438
- const results = [];
439
- for (const relation of metadata.relations) {
440
- if (relation.options?.pattern !== "samePartition") continue;
441
- results.push(...checkRelation(metadata, relation, registry));
442
- }
443
- return results;
444
- }
445
- };
446
- function err(metadata, relation, message) {
447
- return {
448
- ruleId: "same-partition-preset",
449
- severity: "error",
450
- message: `Relation '${relation.propertyName}' on entity '${metadata.prefix}' declares pattern 'samePartition' but ${message}`,
451
- entity: metadata.prefix,
452
- field: relation.propertyName
453
- };
454
- }
455
- function checkRelation(metadata, relation, registry) {
456
- if (relation.type !== "hasMany") {
457
- return [
458
- err(
459
- metadata,
460
- relation,
461
- `is a '${relation.type}'. 'samePartition' lowers a parent\u2192children collection read (begins_with on a segmented sort key) and is only valid on @hasMany.`
462
- )
463
- ];
464
- }
465
- const results = [];
466
- const maintenanceOptions = ["read", "write", "projection"].filter(
467
- (k2) => relation.options?.[k2] !== void 0
468
- );
469
- if (maintenanceOptions.length > 0) {
470
- results.push(
471
- err(
472
- metadata,
473
- relation,
474
- `also sets [${maintenanceOptions.join(", ")}]. 'samePartition' has no maintained materialisation \u2014 children are read live by begins_with, so it carries no read/write/projection effect. Drop these options (or use a maintained preset such as 'embeddedSnapshot').`
475
- )
476
- );
477
- }
478
- const targetFields = Object.keys(relation.keyBinding);
479
- if (targetFields.length === 0) {
480
- results.push(
481
- err(metadata, relation, `has an empty keyBinding; nothing to lower.`)
482
- );
483
- return results;
484
- }
485
- let targetMeta;
486
- try {
487
- const targetClass = relation.targetFactory();
488
- targetMeta = registry.get(targetClass);
489
- } catch {
490
- results.push(
491
- err(
492
- metadata,
493
- relation,
494
- `targets an entity that is not registered in MetadataRegistry.`
495
- )
496
- );
497
- return results;
498
- }
499
- let resolved;
500
- try {
501
- resolved = resolveKey(targetFields, targetMeta);
502
- } catch {
503
- results.push(
504
- err(
505
- metadata,
506
- relation,
507
- `binds [${targetFields.join(", ")}] on target '${targetMeta.prefix}', which matches no access pattern. A same-partition relation must bind a contiguous prefix of the target's primary key.`
508
- )
509
- );
510
- return results;
511
- }
512
- if (resolved.type !== "pk") {
513
- results.push(
514
- err(
515
- metadata,
516
- relation,
517
- `binds [${targetFields.join(", ")}] to a ${resolved.type.toUpperCase()} on target '${targetMeta.prefix}'. A GSI is a different partition; 'samePartition' requires the binding to resolve on the target's primary key (same PK, segmented-SK begins_with).`
518
- )
519
- );
520
- return results;
521
- }
522
- if (!resolved.partial) {
523
- results.push(
524
- err(
525
- metadata,
526
- relation,
527
- `binds the target's full primary key on '${targetMeta.prefix}', which is a point read of a single item, not a child collection. 'samePartition' requires a partial key \u2014 the parent fields fully supply the PK and a proper prefix of the segmented SK, leaving a begins_with boundary for the children.`
528
- )
529
- );
530
- }
531
- return results;
532
- }
533
-
534
- // src/linter/rules/maintenance-shared.ts
535
- function isMaintainedRelation(relation) {
536
- const triggers = relation.options?.write?.maintainedOn;
537
- return Array.isArray(triggers) && triggers.length > 0;
538
- }
539
- function maintainedRelations(metadata) {
540
- return metadata.relations.filter(isMaintainedRelation);
541
- }
542
- function logicalName(metadata) {
543
- return metadata.prefix.endsWith("#") ? metadata.prefix.slice(0, -1) : metadata.prefix;
544
- }
545
- function payloadAttributes(meta) {
546
- const attrs = /* @__PURE__ */ new Set();
547
- for (const f of meta.fields) attrs.add(f.propertyName);
548
- if (meta.primaryKey) {
549
- for (const f of segmentFieldNames(meta.primaryKey.segmented.pkSegments)) {
550
- attrs.add(f);
551
- }
552
- for (const f of segmentFieldNames(meta.primaryKey.segmented.skSegments)) {
553
- attrs.add(f);
554
- }
555
- }
556
- for (const gsi2 of meta.gsiDefinitions) {
557
- for (const f of segmentFieldNames(gsi2.segmented.pkSegments)) attrs.add(f);
558
- for (const f of segmentFieldNames(gsi2.segmented.skSegments)) attrs.add(f);
559
- }
560
- return attrs;
561
- }
562
- function isPayloadRooted(value) {
563
- return value.startsWith("$.input.") || value.startsWith("$.entity.");
564
- }
565
- function projectionSourceAttribute(value) {
566
- const path = typeof value === "string" ? value : value.path;
567
- if (typeof value === "string") {
568
- if (!isPayloadRooted(path)) return path;
569
- } else if (!isPayloadRooted(path)) {
570
- return null;
571
- }
572
- const stripped = path.replace(/^\$\.(input|entity)\./, "");
573
- return stripped.split(".")[0] ?? stripped;
574
- }
575
- function projectionEntries(projection) {
576
- return projection ? Object.entries(projection) : [];
577
- }
578
-
579
- // src/linter/rules/missing-context.ts
580
- var missingContextRule = {
581
- id: "missing-context",
582
- severity: "error",
583
- check(metadata, registry) {
584
- const results = [];
585
- for (const relation of maintainedRelations(metadata)) {
586
- results.push(...checkRelation2(metadata, relation, registry));
587
- }
588
- return results;
589
- }
590
- };
591
- function err2(metadata, relation, message) {
592
- return {
593
- ruleId: "missing-context",
594
- severity: "error",
595
- message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' ${message}`,
596
- entity: metadata.prefix,
597
- field: relation.propertyName
598
- };
599
- }
600
- function checkRelation2(metadata, relation, registry) {
601
- let sourceMeta;
602
- try {
603
- sourceMeta = registry.get(relation.targetFactory());
604
- } catch {
605
- return [];
606
- }
607
- const sourceName = logicalName(sourceMeta);
608
- const payload = payloadAttributes(sourceMeta);
609
- const available = `available: ${[...payload].sort().map((f) => `'${f}'`).join(", ")}`;
610
- const results = [];
611
- for (const sourceField of Object.keys(relation.keyBinding)) {
612
- if (!payload.has(sourceField)) {
613
- results.push(
614
- err2(
615
- metadata,
616
- relation,
617
- `resolves its destination row from source field '${sourceField}', but the source entity '${sourceName}' carries no such attribute on its write payload (${available}). Phase 1 maintenance is payload \u540C\u68B1 only (no write-time fetch) \u2014 include '${sourceField}' on '${sourceName}' or bind a key field it does carry.`
618
- )
619
- );
620
- }
621
- }
622
- for (const [attr, value] of projectionEntries(relation.options?.projection)) {
623
- const path = typeof value === "string" ? value : value.path;
624
- const srcAttr = projectionSourceAttribute(value);
625
- if (srcAttr === null || typeof value !== "string" && !isPayloadRooted(path)) {
626
- results.push(
627
- err2(
628
- metadata,
629
- relation,
630
- `projects '${attr}' from '${path}', which is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). Phase 1 maintenance projects only from the source payload (payload \u540C\u68B1); there is no fetch / re-projection.`
631
- )
632
- );
633
- continue;
634
- }
635
- if (!payload.has(srcAttr)) {
636
- results.push(
637
- err2(
638
- metadata,
639
- relation,
640
- `projects '${attr}' from source attribute '${srcAttr}', but the source entity '${sourceName}' carries no such attribute on its write payload (${available}). Phase 1 maintenance is payload \u540C\u68B1 only (no write-time fetch / re-projection) \u2014 an attribute the source row does not carry cannot be projected.`
641
- )
642
- );
643
- }
644
- }
645
- return results;
646
- }
647
-
648
- // src/linter/rules/embedded-snapshot-size.ts
649
- var ITEM_LIMIT_BYTES = 400 * 1024;
650
- var SOFT_BUDGET_BYTES = ITEM_LIMIT_BYTES / 2;
651
- var DEFAULT_STRING_BYTES = 256;
652
- var ATTR_NAME_OVERHEAD_BYTES = 16;
653
- var embeddedSnapshotSizeRule = {
654
- id: "400kb",
655
- severity: "warning",
656
- check(metadata, _registry) {
657
- const results = [];
658
- for (const relation of maintainedRelations(metadata)) {
659
- const result = checkRelation3(metadata, relation);
660
- if (result) results.push(result);
661
- }
662
- return results;
663
- }
664
- };
665
- function estimateItemBytes(relation) {
666
- let bytes = 0;
667
- for (const [attr, value] of projectionEntries(relation.options?.projection)) {
668
- bytes += ATTR_NAME_OVERHEAD_BYTES + attr.length;
669
- bytes += attributeValueBytes(value);
670
- }
671
- return bytes;
672
- }
673
- function attributeValueBytes(value) {
674
- if (typeof value !== "string" && value.op === "preview") {
675
- const [n] = value.args;
676
- if (typeof n === "number" && Number.isFinite(n)) return n;
677
- }
678
- return DEFAULT_STRING_BYTES;
679
- }
680
- function checkRelation3(metadata, relation) {
681
- const itemBytes = estimateItemBytes(relation);
682
- if (itemBytes === 0) return null;
683
- const isCollection = relation.type === "hasMany";
684
- const maxItems = relation.options?.read?.maxItems;
685
- if (isCollection && maxItems === void 0) return null;
686
- const count = isCollection ? maxItems : 1;
687
- const estimate = itemBytes * count;
688
- const shape = isCollection ? `${count} item(s) \xD7 ~${itemBytes} B` : `~${itemBytes} B snapshot`;
689
- if (estimate >= ITEM_LIMIT_BYTES) {
690
- return {
691
- ruleId: "400kb",
692
- severity: "error",
693
- message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' materializes ${shape} \u2248 ${estimate} B onto the owner row, which meets or exceeds DynamoDB's 400 KB item limit (${ITEM_LIMIT_BYTES} B). The maintenance write would fail once the collection fills \u2014 reduce \`read.maxItems\` or the projected attributes (e.g. tighter \`preview(n)\` bounds).`,
694
- entity: metadata.prefix,
695
- field: relation.propertyName
696
- };
697
- }
698
- if (estimate >= SOFT_BUDGET_BYTES) {
699
- return {
700
- ruleId: "400kb",
701
- severity: "warning",
702
- message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' materializes ${shape} \u2248 ${estimate} B onto the owner row, over half of DynamoDB's 400 KB item limit (${ITEM_LIMIT_BYTES} B). Little headroom remains for the rest of the item \u2014 consider a tighter \`read.maxItems\` / projection.`,
703
- entity: metadata.prefix,
704
- field: relation.propertyName
705
- };
706
- }
707
- return null;
708
- }
709
-
710
- // src/linter/rules/hot-partition.ts
711
- var HOT_TRIGGER_FANIN = 2;
712
- var hotPartitionRule = {
713
- id: "hot-partition",
714
- severity: "warning",
715
- check(metadata, _registry) {
716
- const results = [];
717
- const maintained = maintainedRelations(metadata);
718
- for (const relation of maintained) {
719
- if (relation.type !== "hasMany") continue;
720
- if (relation.options?.read?.maxItems === void 0) {
721
- results.push(unboundedWarning(metadata, relation));
722
- }
723
- }
724
- const byTrigger = /* @__PURE__ */ new Map();
725
- for (const relation of maintained) {
726
- for (const trigger of relation.options?.write?.maintainedOn ?? []) {
727
- const bucket = byTrigger.get(trigger);
728
- if (bucket) bucket.push(relation.propertyName);
729
- else byTrigger.set(trigger, [relation.propertyName]);
730
- }
731
- }
732
- for (const [trigger, props] of byTrigger) {
733
- if (props.length > HOT_TRIGGER_FANIN) {
734
- results.push(fanInWarning(metadata, trigger, props));
735
- }
736
- }
737
- return results;
738
- }
739
- };
740
- function unboundedWarning(metadata, relation) {
741
- return {
742
- ruleId: "hot-partition",
743
- severity: "warning",
744
- message: `Maintained collection '${relation.propertyName}' on '${logicalName(metadata)}' has no \`read.maxItems\` bound: every maintained source row appends to the same owner ("root") item, so that single partition takes unbounded write traffic and the item trends toward the 400 KB limit. Declare a \`read.maxItems\` window (e.g. latest-N) or read the children from their own partition (\`samePartition\`) instead of inlining a snapshot.`,
745
- entity: metadata.prefix,
746
- field: relation.propertyName
747
- };
748
- }
749
- function fanInWarning(metadata, trigger, props) {
750
- return {
751
- ruleId: "hot-partition",
752
- severity: "warning",
753
- message: `${props.length} maintained relations on '${logicalName(metadata)}' ([${props.map((p) => `'${p}'`).join(", ")}]) all fire on trigger '${trigger}', concentrating that many maintenance writes onto the single owner ("root") row in one mutation. This amplifies per-source-row write cost on one partition \u2014 consider splitting the maintained shapes across rows / partitions.`,
754
- entity: metadata.prefix
755
- };
756
- }
757
-
758
- // src/linter/rules/fan-out.ts
759
- var MAX_MAINTENANCE_HOPS = 1;
760
- var fanOutRule = {
761
- id: "fan-out",
762
- severity: "warning",
763
- check(metadata, registry) {
764
- const results = [];
765
- for (const relation of maintainedRelations(metadata)) {
766
- const point = checkDestinationFanOut(metadata, relation);
767
- if (point) results.push(point);
768
- }
769
- const chain = maintenanceChain(metadata, registry, /* @__PURE__ */ new Set([modelKey(metadata)]));
770
- if (chain.hops > MAX_MAINTENANCE_HOPS) {
771
- results.push({
772
- ruleId: "fan-out",
773
- severity: "warning",
774
- message: `Entity '${chain.root}' is the root of a maintenance chain ${chain.hops} hops deep, terminating at '${logicalName(metadata)}': a single '${chain.root}' lifecycle event cascades through ${chain.hops} maintenance writes \u2014 each maintained row is itself the trigger source of the next hop \u2014 until it reaches '${logicalName(metadata)}'. A multi-step maintenance fan-out amplifies one write into many; flatten the chain or move downstream hops to a stream (\`updateMode: 'stream'\`).`,
775
- entity: metadata.prefix
776
- });
777
- }
778
- return results;
779
- }
780
- };
781
- function checkDestinationFanOut(metadata, relation) {
782
- const ownerKeyFields = Object.values(relation.keyBinding);
783
- if (ownerKeyFields.length === 0) return null;
784
- let resolved;
785
- try {
786
- resolved = resolveKey(ownerKeyFields, metadata);
787
- } catch {
788
- return null;
789
- }
790
- const isPoint = !resolved.partial && (resolved.type === "pk" || resolved.type === "gsi" && resolved.unique);
791
- if (isPoint) return null;
792
- const shape = resolved.partial ? `a partial ${resolved.type.toUpperCase()} (a begins_with range of rows)` : `a non-unique ${resolved.type.toUpperCase()} (many matching rows)`;
793
- return {
794
- ruleId: "fan-out",
795
- severity: "warning",
796
- message: `Maintained relation '${relation.propertyName}' on '${logicalName(metadata)}' binds its destination row by [${ownerKeyFields.map((f) => `'${f}'`).join(", ")}], which resolves to ${shape} on '${logicalName(metadata)}'. A single source event then fans its maintenance write out across every matching destination row in one mutation (unbounded fan-out). Bind the destination's full primary key (or a unique GSI) so each source event maintains exactly one row.`,
797
- entity: metadata.prefix,
798
- field: relation.propertyName
799
- };
800
- }
801
- function modelKey(metadata) {
802
- return logicalName(metadata);
803
- }
804
- function maintenanceChain(metadata, registry, visited) {
805
- const maintained = maintainedRelations(metadata);
806
- if (maintained.length === 0) return { hops: 0, root: logicalName(metadata) };
807
- let best = { hops: 0, root: logicalName(metadata) };
808
- for (const relation of maintained) {
809
- let targetMeta;
810
- try {
811
- targetMeta = registry.get(relation.targetFactory());
812
- } catch {
813
- continue;
814
- }
815
- const key2 = modelKey(targetMeta);
816
- if (visited.has(key2)) continue;
817
- const next = new Set(visited);
818
- next.add(key2);
819
- const upstream = maintenanceChain(targetMeta, registry, next);
820
- if (1 + upstream.hops > best.hops) {
821
- best = { hops: 1 + upstream.hops, root: upstream.root };
822
- }
823
- }
824
- return best;
825
- }
826
-
827
- // src/linter/rules/multi-maintainer-same-row.ts
828
- var multiMaintainerSameRowRule = {
829
- id: "multi-maintainer-same-row",
830
- severity: "error",
831
- check(metadata, _registry) {
832
- const maintained = maintainedRelations(metadata);
833
- if (maintained.length < 2) return [];
834
- const owner = logicalName(metadata);
835
- const groups = /* @__PURE__ */ new Map();
836
- for (const relation of maintained) {
837
- const rowKey = destinationRowKey(owner, relation);
838
- for (const trigger of relation.options?.write?.maintainedOn ?? []) {
839
- const groupKey = `${trigger} ${rowKey}`;
840
- const group = groups.get(groupKey);
841
- if (group) group.props.push(relation.propertyName);
842
- else groups.set(groupKey, { trigger, props: [relation.propertyName] });
843
- }
844
- }
845
- const results = [];
846
- for (const { trigger, props } of groups.values()) {
847
- if (props.length < 2) continue;
848
- results.push({
849
- ruleId: "multi-maintainer-same-row",
850
- severity: "error",
851
- message: `Maintained relations [${props.map((p) => `'${p}'`).join(", ")}] on '${owner}' all fire on trigger '${trigger}' and write the SAME destination row. Phase 1 maintenance is "1 mutation \xD7 1 target row = 1 effect" (\u8AD6\u70B92 = b): a single mutation cannot touch one row with ${props.length} maintain effects \u2014 DynamoDB rejects two writes to the same key in one TransactWriteItems (cf. #96). Merge the projections into one maintained relation, or split them onto distinct destination rows / triggers.`,
852
- entity: metadata.prefix,
853
- field: props[0]
854
- });
855
- }
856
- return results;
857
- }
858
- };
859
- function destinationRowKey(owner, relation) {
860
- const parts = Object.entries(relation.keyBinding).map(([sourceField, ownerField]) => `${ownerField}=$.entity.${sourceField}`).sort();
861
- return `${owner}#${parts.join("&")}`;
862
- }
863
-
864
- // src/linter/rules/cfn-schema-consistency.ts
865
- var MAX_GSIS_PER_TABLE = 20;
866
- function gsiShape(gsi2) {
867
- const hasSk = gsi2.segmented.skSegments.length > 0;
868
- return { indexName: gsi2.indexName, hasSk };
869
- }
870
- function err3(entity, message) {
871
- return {
872
- ruleId: "cfn-schema-consistency",
873
- severity: "error",
874
- message,
875
- entity
876
- };
877
- }
878
- var cfnSchemaConsistencyRule = {
879
- id: "cfn-schema-consistency",
880
- severity: "error",
881
- check(metadata, registry) {
882
- return checkAllTables(metadata, registry);
883
- }
884
- };
885
- function checkAllTables(current, registry) {
886
- const seenMeta = /* @__PURE__ */ new Set();
887
- const byPhysical = /* @__PURE__ */ new Map();
888
- const add = (meta) => {
889
- if (seenMeta.has(meta)) return;
890
- seenMeta.add(meta);
891
- const physical = TableMapping.resolve(meta.tableName);
892
- const list = byPhysical.get(physical) ?? [];
893
- list.push(meta);
894
- byPhysical.set(physical, list);
895
- };
896
- add(current);
897
- for (const [, meta] of registry.getFinalized()) add(meta);
898
- const currentPhysical = TableMapping.resolve(current.tableName);
899
- const entities = byPhysical.get(currentPhysical);
900
- if (!entities) return [];
901
- return checkTable(currentPhysical, entities);
902
- }
903
- function checkTable(physical, entities) {
904
- const ordered = [...entities].sort((a, b) => a.prefix.localeCompare(b.prefix));
905
- const results = [];
906
- results.push(...checkBaseKeySchema(physical, ordered));
907
- results.push(...checkGsiUnion(physical, ordered));
908
- return results;
909
- }
910
- function checkBaseKeySchema(physical, entities) {
911
- const withSk = [];
912
- const withoutSk = [];
913
- for (const meta of entities) {
914
- if (!meta.primaryKey) continue;
915
- const hasSk = meta.primaryKey.segmented.skSegments.length > 0;
916
- (hasSk ? withSk : withoutSk).push(meta.prefix);
917
- }
918
- if (withSk.length > 0 && withoutSk.length > 0) {
919
- const offender = [...withSk, ...withoutSk].sort()[0];
920
- return [
921
- err3(
922
- offender,
923
- `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.`
924
- )
925
- ];
926
- }
927
- return [];
928
- }
929
- function checkGsiUnion(physical, entities) {
930
- const results = [];
931
- const seen = /* @__PURE__ */ new Map();
932
- for (const meta of entities) {
933
- for (const gsi2 of meta.gsiDefinitions) {
934
- const shape = gsiShape(gsi2);
935
- const existing = seen.get(shape.indexName);
936
- if (!existing) {
937
- seen.set(shape.indexName, { shape, entity: meta.prefix });
938
- continue;
939
- }
940
- const conflict = describeConflict(existing.shape, shape);
941
- if (conflict) {
942
- results.push(
943
- err3(
944
- meta.prefix,
945
- `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.`
946
- )
947
- );
948
- }
949
- }
950
- }
951
- if (seen.size > MAX_GSIS_PER_TABLE) {
952
- const names = [...seen.keys()].sort();
953
- const offender = [...entities].map((e) => e.prefix).sort()[0];
954
- results.push(
955
- err3(
956
- offender,
957
- `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.`
958
- )
959
- );
960
- }
961
- return results;
962
- }
963
- function describeConflict(a, b) {
964
- if (a.hasSk !== b.hasSk) {
965
- return `one declaration has a sort key and the other does not (${a.hasSk ? "with" : "without"} SK vs. ${b.hasSk ? "with" : "without"} SK)`;
966
- }
967
- return null;
968
- }
969
-
970
- // src/linter/default-linter.ts
971
- function createDefaultLinter() {
972
- const linter = new Linter();
973
- linter.addRule(noScanRule);
974
- linter.addRule(requireLimitRule);
975
- linter.addRule(gsiAmbiguityRule);
976
- linter.addRule(missingGsiRule);
977
- linter.addRule(relationDepthRule);
978
- linter.addRule(samePartitionPresetRule);
979
- linter.addRule(missingContextRule);
980
- linter.addRule(embeddedSnapshotSizeRule);
981
- linter.addRule(hotPartitionRule);
982
- linter.addRule(fanOutRule);
983
- linter.addRule(multiMaintainerSameRowRule);
984
- linter.addRule(cfnSchemaConsistencyRule);
985
- return linter;
986
- }
987
-
988
33
  // src/metadata/registry.ts
34
+ function defaultLinterForEnv() {
35
+ return process.env.NODE_ENV === "production" ? null : createDefaultLinter();
36
+ }
989
37
  var MetadataRegistry = class {
990
38
  static store = /* @__PURE__ */ new Map();
991
- static _linter = createDefaultLinter();
39
+ static _linter = defaultLinterForEnv();
992
40
  /**
993
41
  * A monotonically-increasing counter bumped on every change to the set of
994
42
  * registered models ({@link register} / {@link clear}). It lets a consumer that
@@ -1059,14 +107,21 @@ var MetadataRegistry = class {
1059
107
  static set linter(linter) {
1060
108
  this._linter = linter;
1061
109
  }
1062
- /** Resets linter to the default built-in rule set. */
110
+ /** Resets linter to the environment default (built-in rule set outside production). */
1063
111
  static resetLinter() {
1064
- this._linter = createDefaultLinter();
112
+ this._linter = defaultLinterForEnv();
1065
113
  }
1066
- /** @internal — for testing only */
114
+ /**
115
+ * @internal — for testing only. Resets to a no-op linter (no rules): matches
116
+ * the pre-#189 `new Linter()` behaviour where a cleared registry does not lint
117
+ * on re-registration. `null` is finalize-equivalent to an empty linter (the
118
+ * `finalize` guard skips a null linter), and nothing reads the linter getter
119
+ * to invoke it, so this preserves existing test semantics without importing
120
+ * the `Linter` class as a value (keeping it out of a production bundle).
121
+ */
1067
122
  static clear() {
1068
123
  this.store.clear();
1069
- this._linter = new Linter();
124
+ this._linter = null;
1070
125
  this._generation++;
1071
126
  }
1072
127
  static finalize(target, meta) {
@@ -1094,8 +149,10 @@ var MetadataRegistry = class {
1094
149
  });
1095
150
  }
1096
151
  }
1097
- const results = this._linter.run(meta, this);
1098
- this.handleLintResults(results);
152
+ if (this._linter) {
153
+ const results = this._linter.run(meta, this);
154
+ this.handleLintResults(results);
155
+ }
1099
156
  meta._finalized = true;
1100
157
  }
1101
158
  static handleLintResults(results) {
@@ -1188,6 +245,73 @@ function buildRelation(select) {
1188
245
  });
1189
246
  }
1190
247
 
248
+ // src/relation/detect.ts
249
+ function isRelationSpec(value) {
250
+ if (typeof value !== "object" || value === null) return false;
251
+ return "select" in value || isSelectBuilder(value);
252
+ }
253
+ function isInlineSnapshotSpec(value) {
254
+ return typeof value === "object" && value !== null && !isSelectBuilder(value) && value.inline === true && !("select" in value);
255
+ }
256
+ function detectRelationFields(select, metadata) {
257
+ const result = [];
258
+ for (const [fieldName, value] of Object.entries(select)) {
259
+ if (isRelationSpec(value)) {
260
+ const rel = metadata.relations.find((r) => r.propertyName === fieldName);
261
+ if (rel) {
262
+ result.push(rel);
263
+ }
264
+ }
265
+ }
266
+ return result;
267
+ }
268
+ function validateInlineSnapshotSelect(select, metadata, resolveTargetMeta) {
269
+ for (const [fieldName, value] of Object.entries(select)) {
270
+ if (isInlineSnapshotSpec(value)) {
271
+ const rel = metadata.relations.find((r) => r.propertyName === fieldName);
272
+ if (!rel) {
273
+ throw new Error(
274
+ `Inline read '{ ${fieldName}: { inline: true } }' is only valid on an embeddedSnapshot relation, but '${fieldName}' is not a relation on '${metadata.prefix}'. Use '${fieldName}: true' for a scalar / embedded field.`
275
+ );
276
+ }
277
+ if (rel.options?.pattern !== "embeddedSnapshot") {
278
+ throw new Error(
279
+ `Inline read '{ ${fieldName}: { inline: true } }' is only valid on an embeddedSnapshot relation. '${fieldName}' is a plain '${rel.type}' relation (no 'pattern: embeddedSnapshot'); use '{ ${fieldName}: { select: \u2026 } }' to traverse to its child rows instead.`
280
+ );
281
+ }
282
+ continue;
283
+ }
284
+ if (typeof value === "object" && value !== null && isRelationSpec(value)) {
285
+ const rel = metadata.relations.find((r) => r.propertyName === fieldName);
286
+ if (!rel) continue;
287
+ const targetMeta = resolveTargetMeta(rel);
288
+ const nested = normalizeSelectSpec(value).select;
289
+ if (nested) {
290
+ validateInlineSnapshotSelect(nested, targetMeta, resolveTargetMeta);
291
+ }
292
+ }
293
+ }
294
+ }
295
+ function getImplicitKeyFields(select, metadata) {
296
+ const relations = detectRelationFields(select, metadata);
297
+ const needed = /* @__PURE__ */ new Set();
298
+ for (const rel of relations) {
299
+ if (rel.type === "refs" && rel.refs) {
300
+ const listField = rel.refs.from;
301
+ if (!(listField in select) || select[listField] !== true) {
302
+ needed.add(listField);
303
+ }
304
+ continue;
305
+ }
306
+ for (const sourceField of Object.values(rel.keyBinding)) {
307
+ if (!(sourceField in select) || select[sourceField] !== true) {
308
+ needed.add(sourceField);
309
+ }
310
+ }
311
+ }
312
+ return [...needed];
313
+ }
314
+
1191
315
  // src/define/param.ts
1192
316
  function isParamOptions(value) {
1193
317
  return typeof value === "object" && value !== null;
@@ -1959,7 +1083,7 @@ function isOperatorObject(value) {
1959
1083
  }
1960
1084
  const keys = Object.keys(value);
1961
1085
  if (keys.length === 0) return false;
1962
- return keys.every((k2) => OPERATOR_KEYS.has(k2));
1086
+ return keys.every((k) => OPERATOR_KEYS.has(k));
1963
1087
  }
1964
1088
  function joinAnd(clauses) {
1965
1089
  if (clauses.length === 1) return clauses[0];
@@ -2059,25 +1183,25 @@ function compileNode(ctx, node) {
2059
1183
  return compileRaw(ctx, node);
2060
1184
  }
2061
1185
  const clauses = [];
2062
- for (const [key2, value] of Object.entries(node)) {
1186
+ for (const [key, value] of Object.entries(node)) {
2063
1187
  if (value === void 0) continue;
2064
- if (key2 === "and" || key2 === "or") {
1188
+ if (key === "and" || key === "or") {
2065
1189
  const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
2066
1190
  if (parts.length === 0) continue;
2067
1191
  if (parts.length === 1) {
2068
1192
  clauses.push(parts[0]);
2069
1193
  } else {
2070
- const sep = key2 === "and" ? " AND " : " OR ";
1194
+ const sep = key === "and" ? " AND " : " OR ";
2071
1195
  clauses.push(`(${parts.map((p) => wrap(p)).join(sep)})`);
2072
1196
  }
2073
1197
  continue;
2074
1198
  }
2075
- if (key2 === "not") {
1199
+ if (key === "not") {
2076
1200
  const inner = compileNode(ctx, value);
2077
1201
  if (inner.length > 0) clauses.push(`NOT ${wrap(inner)}`);
2078
1202
  continue;
2079
1203
  }
2080
- const clause = compileField(ctx, key2, value);
1204
+ const clause = compileField(ctx, key, value);
2081
1205
  if (clause.length > 0) clauses.push(clause);
2082
1206
  }
2083
1207
  return joinAnd(clauses);
@@ -2095,11 +1219,11 @@ function resolveLeaf(value, resolveParam) {
2095
1219
  function resolveNode(node, resolveParam) {
2096
1220
  const obj = node;
2097
1221
  const out = {};
2098
- for (const [key2, value] of Object.entries(obj)) {
2099
- if (key2 === "and" || key2 === "or") {
2100
- out[key2] = value.map((s) => resolveNode(s, resolveParam));
2101
- } else if (key2 === "not") {
2102
- out[key2] = resolveNode(value, resolveParam);
1222
+ for (const [key, value] of Object.entries(obj)) {
1223
+ if (key === "and" || key === "or") {
1224
+ out[key] = value.map((s) => resolveNode(s, resolveParam));
1225
+ } else if (key === "not") {
1226
+ out[key] = resolveNode(value, resolveParam);
2103
1227
  } else {
2104
1228
  const ops = value;
2105
1229
  const rendered = {};
@@ -2120,7 +1244,7 @@ function resolveNode(node, resolveParam) {
2120
1244
  rendered[op] = resolveLeaf(opVal, resolveParam);
2121
1245
  }
2122
1246
  }
2123
- out[key2] = rendered;
1247
+ out[key] = rendered;
2124
1248
  }
2125
1249
  }
2126
1250
  return out;
@@ -2539,14 +1663,14 @@ var MiddlewareRuntime = class {
2539
1663
  * is the host's responsibility — a hook recovering a `query` should return an
2540
1664
  * item / `null`, one recovering a `list` a `{ items, cursor }`.
2541
1665
  */
2542
- async runRequestError(ctx, err4) {
1666
+ async runRequestError(ctx, err) {
2543
1667
  for (let i = this.chain.length - 1; i >= 0; i--) {
2544
1668
  const onError = this.chain[i].read?.onError;
2545
1669
  if (!onError) continue;
2546
- const recovered = await onError(ctx, err4);
1670
+ const recovered = await onError(ctx, err);
2547
1671
  if (recovered !== void 0) return recovered;
2548
1672
  }
2549
- throw err4;
1673
+ throw err;
2550
1674
  }
2551
1675
  /**
2552
1676
  * Drive ONE physical op (root read or any fan-out fetch) through R2 → send →
@@ -2586,14 +1710,14 @@ var MiddlewareRuntime = class {
2586
1710
  if (afterFetch) items = await afterFetch(ctx, items);
2587
1711
  }
2588
1712
  return items;
2589
- } catch (err4) {
1713
+ } catch (err) {
2590
1714
  for (let i = this.chain.length - 1; i >= 0; i--) {
2591
1715
  const onError = this.chain[i].read?.op?.onError;
2592
1716
  if (!onError) continue;
2593
- const recovered = await onError(ctx, err4);
1717
+ const recovered = await onError(ctx, err);
2594
1718
  if (recovered !== void 0) return recovered;
2595
1719
  }
2596
- throw err4;
1720
+ throw err;
2597
1721
  }
2598
1722
  }
2599
1723
  };
@@ -2657,14 +1781,14 @@ var WriteRuntime = class {
2657
1781
  * short-circuits the chain); the value becomes the write's resolved value.
2658
1782
  * Otherwise the original error rethrows (symmetric with the read `onError`).
2659
1783
  */
2660
- async runWriteError(ctx, err4) {
1784
+ async runWriteError(ctx, err) {
2661
1785
  for (let i = this.chain.length - 1; i >= 0; i--) {
2662
1786
  const onError = this.chain[i].write?.onError;
2663
1787
  if (!onError) continue;
2664
- const recovered = await onError(ctx, err4);
1788
+ const recovered = await onError(ctx, err);
2665
1789
  if (recovered !== void 0) return recovered;
2666
1790
  }
2667
- throw err4;
1791
+ throw err;
2668
1792
  }
2669
1793
  /** Build a W3/W4/W5 persist context (pure — no hooks run). */
2670
1794
  persistCtx(items, origins, transaction) {
@@ -2693,20 +1817,20 @@ var WriteRuntime = class {
2693
1817
  if (before) await before(ctx);
2694
1818
  }
2695
1819
  results = await send(ctx.items);
2696
- } catch (err4) {
1820
+ } catch (err) {
2697
1821
  let recovered;
2698
1822
  let didRecover = false;
2699
1823
  for (let i = this.chain.length - 1; i >= 0; i--) {
2700
1824
  const onError = this.chain[i].write?.persist?.onError;
2701
1825
  if (!onError) continue;
2702
- const r = await onError(ctx, err4);
1826
+ const r = await onError(ctx, err);
2703
1827
  if (r !== void 0) {
2704
1828
  recovered = r;
2705
1829
  didRecover = true;
2706
1830
  break;
2707
1831
  }
2708
1832
  }
2709
- if (!didRecover) throw err4;
1833
+ if (!didRecover) throw err;
2710
1834
  results = recovered;
2711
1835
  }
2712
1836
  for (let i = this.chain.length - 1; i >= 0; i--) {
@@ -2927,8 +2051,8 @@ async function executeSingleWriteWithHooks(req, mw) {
2927
2051
  await mw.runWriteBefore(ctx);
2928
2052
  const change2 = await persistRewrittenWrite(req.modelClass, ctx, mw);
2929
2053
  await mw.runWriteAfter(ctx, change2);
2930
- } catch (err4) {
2931
- await mw.runWriteError(ctx, err4);
2054
+ } catch (err) {
2055
+ await mw.runWriteError(ctx, err);
2932
2056
  }
2933
2057
  }
2934
2058
  async function persistRewrittenWrite(modelClass, ctx, mw) {
@@ -3506,12 +2630,12 @@ var TransactionContext = class {
3506
2630
  keys: { pk: String(updateInput.Key.PK), sk: String(updateInput.Key.SK ?? "") }
3507
2631
  });
3508
2632
  }
3509
- delete(model, key2, options) {
2633
+ delete(model, key, options) {
3510
2634
  this.assertWithinLimit();
3511
2635
  const modelClass = resolveModelClass(model);
3512
- const deleteInput = buildDeleteInput(modelClass, key2, options);
2636
+ const deleteInput = buildDeleteInput(modelClass, key, options);
3513
2637
  this.items.push({ Delete: deleteInput });
3514
- this.logicalOps.push({ kind: "delete", modelClass, key: key2, options });
2638
+ this.logicalOps.push({ kind: "delete", modelClass, key, options });
3515
2639
  this.captureMeta.push({
3516
2640
  modelName: modelClass.name,
3517
2641
  op: "delete",
@@ -3531,7 +2655,7 @@ var TransactionContext = class {
3531
2655
  * @throws if `options.condition` is missing — a ConditionCheck without an
3532
2656
  * assertion is meaningless.
3533
2657
  */
3534
- conditionCheck(model, key2, options) {
2658
+ conditionCheck(model, key, options) {
3535
2659
  if (!options || !options.condition) {
3536
2660
  throw new Error(
3537
2661
  "TransactionContext.conditionCheck requires a `condition` (the read-only assertion it makes); e.g. `{ condition: { attributeExists: 'PK' } }`."
@@ -3539,7 +2663,7 @@ var TransactionContext = class {
3539
2663
  }
3540
2664
  this.assertWithinLimit();
3541
2665
  const modelClass = resolveModelClass(model);
3542
- const { TableName, Key } = buildDeleteInput(modelClass, key2);
2666
+ const { TableName, Key } = buildDeleteInput(modelClass, key);
3543
2667
  const cond2 = buildConditionExpression(options.condition);
3544
2668
  const check = {
3545
2669
  TableName,
@@ -3661,6 +2785,11 @@ function hydrateItem(raw, select, metadata, updatable = false) {
3661
2785
  const fieldMeta = fieldMap.get(fieldName);
3662
2786
  result[fieldName] = deserializeValue(value, fieldMeta);
3663
2787
  }
2788
+ } else if (isInlineSnapshotSpec(selectValue)) {
2789
+ const value = raw[fieldName];
2790
+ if (value !== void 0) {
2791
+ result[fieldName] = value;
2792
+ }
3664
2793
  } else if (typeof selectValue === "object" && selectValue !== null) {
3665
2794
  if ("select" in selectValue || isSelectBuilder(selectValue)) {
3666
2795
  continue;
@@ -3683,19 +2812,19 @@ function hydrateItem(raw, select, metadata, updatable = false) {
3683
2812
  }
3684
2813
  function hydrateEmbedded(raw, select, _metadata, _fieldName) {
3685
2814
  const result = {};
3686
- for (const [key2, selectValue] of Object.entries(select)) {
2815
+ for (const [key, selectValue] of Object.entries(select)) {
3687
2816
  if (selectValue === true) {
3688
- if (raw[key2] !== void 0) {
3689
- result[key2] = raw[key2];
2817
+ if (raw[key] !== void 0) {
2818
+ result[key] = raw[key];
3690
2819
  }
3691
2820
  } else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
3692
- const nested = raw[key2];
2821
+ const nested = raw[key];
3693
2822
  if (nested && typeof nested === "object") {
3694
- result[key2] = hydrateEmbedded(
2823
+ result[key] = hydrateEmbedded(
3695
2824
  nested,
3696
2825
  selectValue,
3697
2826
  _metadata,
3698
- key2
2827
+ key
3699
2828
  );
3700
2829
  }
3701
2830
  }
@@ -3799,6 +2928,18 @@ var recorder = {
3799
2928
  deleteEdge(targetFactory, relationProperty) {
3800
2929
  return { kind: "deleteEdge", targetFactory, relationProperty };
3801
2930
  },
2931
+ dualEdge(forward, inverse) {
2932
+ return {
2933
+ kind: "putEdge",
2934
+ targetFactory: forward.target,
2935
+ relationProperty: forward.relationProperty,
2936
+ inverse: {
2937
+ adjacencyFactory: inverse.adjacency,
2938
+ targetFactory: inverse.target,
2939
+ relationProperty: inverse.relationProperty
2940
+ }
2941
+ };
2942
+ },
3802
2943
  increment(targetFactory, keys, attribute, amount) {
3803
2944
  return { kind: "derive", targetFactory, keys, attribute, amount };
3804
2945
  },
@@ -4646,22 +3787,7 @@ function buildSubscribeHandler(handlers) {
4646
3787
  }
4647
3788
 
4648
3789
  export {
4649
- isColumn,
4650
- createColumnMap,
4651
- isKeySegment,
4652
- k,
4653
- segmentFieldNames,
4654
- key,
4655
3790
  gsi,
4656
- Linter,
4657
- noScanRule,
4658
- requireLimitRule,
4659
- gsiAmbiguityRule,
4660
- resolveKey,
4661
- missingGsiRule,
4662
- relationDepthRule,
4663
- TableMapping,
4664
- createDefaultLinter,
4665
3791
  MetadataRegistry,
4666
3792
  pkTemplate,
4667
3793
  skTemplate,
@@ -4671,6 +3797,10 @@ export {
4671
3797
  normalizeTopLevelSelect,
4672
3798
  buildProject,
4673
3799
  buildRelation,
3800
+ isInlineSnapshotSpec,
3801
+ detectRelationFields,
3802
+ validateInlineSnapshotSelect,
3803
+ getImplicitKeyFields,
4674
3804
  param,
4675
3805
  isParam,
4676
3806
  BATCH_GET_MAX_KEYS,