dinah 0.12.0 → 0.13.1

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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { i as resolveAttrType, n as extractTableDesc, r as removeUndefined, t as chunk } from "./util-BfKXObBN.mjs";
1
+ import { a as resolveAttrType, i as removeUndefined, n as extractTableDesc, r as matchesPartial, t as chunk } from "./util-0kc6E2VD.mjs";
2
2
  import { ConditionalCheckFailedException, CreateTableCommand, DeleteTableCommand, DynamoDB, DynamoDBClient, ListTablesCommand, TransactionCanceledException } from "@aws-sdk/client-dynamodb";
3
3
  import * as Lib from "@aws-sdk/lib-dynamodb";
4
4
  //#region src/error.ts
@@ -7,6 +7,7 @@ function dinahErrorMessage(details) {
7
7
  case "NOT_FOUND": return `${details.resource ?? "Item"} not found: ${JSON.stringify(details.key)}`;
8
8
  case "CONDITIONAL_CHECK_FAILED": return details.key ? `${details.resource ?? "Item"} condition check failed: ${JSON.stringify(details.key)}` : `${details.resource ?? "Item"} condition check failed`;
9
9
  case "VALIDATION": return `Validation error: ${details.message}`;
10
+ case "DATA_INTEGRITY": return `Data integrity error: ${details.message}`;
10
11
  case "TRANSACTION_CANCELED": return `Transaction canceled: ${details.reasons.map((r) => r.type).join(", ")}`;
11
12
  }
12
13
  }
@@ -249,8 +250,8 @@ var Repo = class {
249
250
  const t = this.table.def.name;
250
251
  return t.charAt(0).toUpperCase() + t.slice(1);
251
252
  }
252
- get defaultPutData() {
253
- return this.config.defaultPutData?.() ?? {};
253
+ get defaultCreateData() {
254
+ return this.config.defaultCreateData?.() ?? {};
254
255
  }
255
256
  get defaultUpdateData() {
256
257
  return this.config.defaultUpdateData?.() ?? {};
@@ -258,9 +259,6 @@ var Repo = class {
258
259
  transformOutput(item) {
259
260
  return this.config.transformOutput?.(item) ?? item;
260
261
  }
261
- transformInput(item) {
262
- return this.config.transformInput?.(item) ?? item;
263
- }
264
262
  extractKey(item) {
265
263
  const { partitionKey, sortKey } = this.table.def;
266
264
  if (sortKey) return {
@@ -270,43 +268,44 @@ var Repo = class {
270
268
  return { [partitionKey]: item[partitionKey] };
271
269
  }
272
270
  async get(key, options) {
271
+ const { filter, ...restOptions } = options ?? {};
273
272
  const item = await this.db.get({
274
273
  table: this.tableName,
275
274
  key: this.extractKey(key),
276
- ...options
275
+ filter,
276
+ ...restOptions
277
277
  });
278
278
  return item && this.applyTransformIfNeeded(item, options);
279
279
  }
280
280
  async getOrThrow(key, options) {
281
+ const { filter, ...restOptions } = options ?? {};
281
282
  const item = await this.db.getOrThrow({
282
283
  table: this.tableName,
283
284
  key: this.extractKey(key),
284
285
  resource: this.resourceName,
285
- ...options
286
+ filter,
287
+ ...restOptions
286
288
  });
287
289
  return this.applyTransformIfNeeded(item, options);
288
290
  }
289
291
  async put(item, options) {
290
- const itemWithDefaults = this.applyPutTransforms(item);
291
292
  const result = await this.db.put({
292
293
  table: this.tableName,
293
- item: itemWithDefaults,
294
+ item,
294
295
  resource: this.resourceName,
295
296
  ...options
296
297
  });
297
298
  return this.applyTransformIfNeeded(result);
298
299
  }
299
300
  async update(key, update, options) {
300
- const updateWithDefaults = this.applyNormalizersToExpression({
301
- ...this.defaultUpdateData,
302
- ...update
303
- });
301
+ const updateWithDefaults = this.applyNormalizersToExpression(update);
304
302
  const result = await this.db.update({
305
303
  table: this.tableName,
306
304
  key: this.extractKey(key),
307
305
  resource: this.resourceName,
308
306
  update: updateWithDefaults,
309
- ...options
307
+ ...options,
308
+ condition: this.withDiscriminatorCondition(key, options?.condition)
310
309
  });
311
310
  return this.applyTransformIfNeeded(result);
312
311
  }
@@ -314,10 +313,15 @@ var Repo = class {
314
313
  const { condition: otherCondition, ...otherOptions } = options ?? {};
315
314
  const condition = { $and: [{ [this.table.def.partitionKey]: { $exists: false } }] };
316
315
  if (otherCondition) condition.$and.push(otherCondition);
317
- return this.put(item, {
316
+ const normalizedItem = this.applyCreateTransforms(item);
317
+ const result = await this.db.put({
318
+ table: this.tableName,
319
+ item: normalizedItem,
320
+ resource: this.resourceName,
318
321
  condition,
319
322
  ...otherOptions
320
323
  });
324
+ return this.applyTransformIfNeeded(result);
321
325
  }
322
326
  async delete(key, options) {
323
327
  const item = await this.db.delete({
@@ -375,6 +379,35 @@ var Repo = class {
375
379
  gsi
376
380
  });
377
381
  }
382
+ async getGsi(gsi, query, options) {
383
+ const { filter, ...restOptions } = options ?? {};
384
+ const items = await this.db.query({
385
+ table: this.tableName,
386
+ index: gsi,
387
+ query,
388
+ ...restOptions
389
+ });
390
+ if (items.length > 1) throw new DinahError({
391
+ type: "DATA_INTEGRITY",
392
+ message: `getGsi on "${gsi}" returned ${items.length} items; expected at most 1.`
393
+ });
394
+ const item = items[0];
395
+ if (!item) return void 0;
396
+ if (filter && !matchesPartial(filter, item)) return void 0;
397
+ return this.applyTransformIfNeeded(item, {
398
+ ...options,
399
+ gsi
400
+ });
401
+ }
402
+ async getGsiOrThrow(gsi, query, options) {
403
+ const item = await this.getGsi(gsi, query, options);
404
+ if (item === void 0) throw new DinahError({
405
+ type: "NOT_FOUND",
406
+ key: query,
407
+ resource: this.resourceName
408
+ });
409
+ return item;
410
+ }
378
411
  async scan(options) {
379
412
  const items = await this.db.scan({
380
413
  table: this.tableName,
@@ -425,9 +458,11 @@ var Repo = class {
425
458
  });
426
459
  }
427
460
  async batchGet(keys, options) {
461
+ const { filter, ...restOptions } = options ?? {};
428
462
  const { items, unprocessed } = await this.db.batchGet({ [this.tableName]: {
429
463
  keys: keys.map((key) => this.extractKey(key)),
430
- ...options
464
+ filter,
465
+ ...restOptions
431
466
  } });
432
467
  const tableItems = items[this.tableName];
433
468
  return {
@@ -436,10 +471,12 @@ var Repo = class {
436
471
  };
437
472
  }
438
473
  async batchGetOrThrow(keys, options) {
474
+ const { filter, ...restOptions } = options ?? {};
439
475
  const result = await this.db.batchGetOrThrow({ [this.tableName]: {
440
476
  keys: keys.map((key) => this.extractKey(key)),
441
477
  resource: this.resourceName,
442
- ...options
478
+ filter,
479
+ ...restOptions
443
480
  } });
444
481
  return this.applyTransformsIfNeeded(result[this.tableName] ?? [], options);
445
482
  }
@@ -451,7 +488,7 @@ var Repo = class {
451
488
  };
452
489
  else return {
453
490
  type: "PUT",
454
- item: this.applyPutTransforms(request.item)
491
+ item: request.item
455
492
  };
456
493
  }) });
457
494
  return {
@@ -460,28 +497,29 @@ var Repo = class {
460
497
  };
461
498
  }
462
499
  async batchUpdate(keys, update) {
463
- const updateWithDefaults = this.applyNormalizersToExpression({
464
- ...this.defaultUpdateData,
465
- ...update
466
- });
500
+ const updateWithDefaults = this.applyNormalizersToExpression(update);
467
501
  return { unprocessed: (await this.db.batchUpdate({ [this.tableName]: {
468
502
  keys: keys.map((key) => this.extractKey(key)),
469
503
  update: updateWithDefaults
470
504
  } })).unprocessed?.[this.tableName]?.keys };
471
505
  }
472
506
  async trxGet(keys, options) {
507
+ const { filter, ...restOptions } = options ?? {};
473
508
  return (await this.db.trxGet(...keys.map((key) => ({
474
509
  table: this.tableName,
475
510
  key: this.extractKey(key),
476
- ...options
511
+ filter,
512
+ ...restOptions
477
513
  })))).map((item) => item && this.applyTransformIfNeeded(item, options));
478
514
  }
479
515
  async trxGetOrThrow(keys, options) {
516
+ const { filter, ...restOptions } = options ?? {};
480
517
  const items = await this.db.trxGetOrThrow(...keys.map((key) => ({
481
518
  table: this.tableName,
482
519
  key: this.extractKey(key),
483
520
  resource: this.resourceName,
484
- ...options
521
+ filter,
522
+ ...restOptions
485
523
  })));
486
524
  return this.applyTransformsIfNeeded(items, options);
487
525
  }
@@ -489,8 +527,8 @@ var Repo = class {
489
527
  await this.db.trxWrite(...requests.map((request) => {
490
528
  switch (request.type) {
491
529
  case "CONDITION": {
492
- const { key, condition, ...options } = request;
493
- return this.trxConditionRequest(key, condition, options);
530
+ const { key, condition } = request;
531
+ return this.trxConditionRequest(key, condition);
494
532
  }
495
533
  case "DELETE": {
496
534
  const { key, ...options } = request;
@@ -535,85 +573,125 @@ var Repo = class {
535
573
  ...options
536
574
  };
537
575
  }
538
- trxConditionRequest(key, condition, options) {
576
+ trxConditionRequest(key, condition) {
539
577
  return {
540
578
  table: this.tableName,
541
579
  type: "CONDITION",
542
580
  key: this.extractKey(key),
543
- condition,
544
- ...options
581
+ condition
545
582
  };
546
583
  }
547
584
  trxPutRequest(item, options) {
548
- const itemWithDefaults = this.applyPutTransforms(item);
549
585
  return {
550
586
  table: this.tableName,
551
587
  type: "PUT",
552
- item: itemWithDefaults,
588
+ item,
553
589
  ...options
554
590
  };
555
591
  }
556
592
  trxUpdateRequest(key, update, options) {
557
- const updateWithDefaults = this.applyNormalizersToExpression({
558
- ...this.defaultUpdateData,
559
- ...update
560
- });
593
+ const updateWithDefaults = this.applyNormalizersToExpression(update);
561
594
  return {
562
595
  table: this.tableName,
563
596
  type: "UPDATE",
564
597
  key: this.extractKey(key),
565
598
  update: updateWithDefaults,
566
- ...options
599
+ ...options,
600
+ condition: this.withDiscriminatorCondition(key, options?.condition)
567
601
  };
568
602
  }
569
603
  trxCreateRequest(item, options) {
570
604
  const { condition: otherCondition, ...otherOptions } = options ?? {};
571
605
  const condition = { $and: [{ [this.table.def.partitionKey]: { $exists: false } }] };
572
606
  if (otherCondition) condition.$and.push(otherCondition);
573
- const itemWithDefaults = this.applyPutTransforms(item);
607
+ const normalizedItem = this.applyCreateTransforms(item);
574
608
  return {
575
609
  table: this.tableName,
576
610
  type: "PUT",
577
- item: itemWithDefaults,
611
+ item: normalizedItem,
578
612
  condition,
579
613
  ...otherOptions
580
614
  };
581
615
  }
582
- applyPutTransforms(item) {
583
- const result = { ...this.transformInput({
584
- ...this.defaultPutData,
616
+ applyCreateTransforms(item) {
617
+ const { transformAttributes, computedAttributes } = this.config;
618
+ if (computedAttributes) {
619
+ for (const key of Object.keys(computedAttributes)) if (key in item) throw new DinahError({
620
+ type: "VALIDATION",
621
+ message: `Field "${key}" is a computed attribute and cannot be set directly. Remove it from the create input.`
622
+ });
623
+ }
624
+ const merged = {
625
+ ...this.defaultCreateData,
585
626
  ...item
586
- }) };
587
- for (const key of this.config.derivedAttributes ?? []) delete result[key];
588
- return result;
589
- }
590
- applyNormalizersToExpression(expression) {
591
- const partial = {};
592
- for (const [key, val] of Object.entries(expression)) if (val === void 0 || isOperation(val) && val.$remove === true) partial[key] = void 0;
593
- else if (!isOperation(val)) partial[key] = val;
594
- else if (val.$set !== void 0) partial[key] = val.$set;
595
- else if (val.$ifNotExists !== void 0) partial[key] = Array.isArray(val.$ifNotExists) ? val.$ifNotExists[1] : val.$ifNotExists;
596
- const normalized = this.transformInput(partial);
597
- const result = { ...expression };
598
- for (const [key, normalizedVal] of Object.entries(normalized)) {
599
- if (normalizedVal === void 0) continue;
600
- if (key in partial && partial[key] === void 0 || !(key in partial)) result[key] = normalizedVal;
601
- else {
602
- const original = expression[key];
603
- if (!isOperation(original)) result[key] = normalizedVal;
604
- else if (original.$set !== void 0) result[key] = {
605
- ...original,
606
- $set: normalizedVal
607
- };
608
- else if (original.$ifNotExists !== void 0) result[key] = {
609
- ...original,
610
- $ifNotExists: Array.isArray(original.$ifNotExists) ? [original.$ifNotExists[0], normalizedVal] : normalizedVal
627
+ };
628
+ if (transformAttributes) {
629
+ for (const [key, transform] of Object.entries(transformAttributes)) if (key in merged && merged[key] !== void 0) merged[key] = transform(merged[key]);
630
+ }
631
+ if (computedAttributes) for (const [key, def] of Object.entries(computedAttributes)) {
632
+ const { from, compute } = def;
633
+ const computed = compute(merged[from]);
634
+ if (computed !== void 0) merged[key] = computed;
635
+ else delete merged[key];
636
+ }
637
+ return merged;
638
+ }
639
+ applyNormalizersToExpression(userUpdate) {
640
+ const { transformAttributes, computedAttributes, immutableAttributes } = this.config;
641
+ if (computedAttributes) {
642
+ for (const key of Object.keys(computedAttributes)) if (key in userUpdate) throw new DinahError({
643
+ type: "VALIDATION",
644
+ message: `Field "${key}" is a computed attribute and cannot be set directly in an update. Update the source field instead.`
645
+ });
646
+ }
647
+ for (const key of immutableAttributes ?? []) if (key in userUpdate) throw new DinahError({
648
+ type: "VALIDATION",
649
+ message: `Field "${key}" is immutable and cannot be updated.`
650
+ });
651
+ const result = {
652
+ ...this.defaultUpdateData,
653
+ ...userUpdate
654
+ };
655
+ if (transformAttributes) for (const [key, transform] of Object.entries(transformAttributes)) {
656
+ if (!(key in result)) continue;
657
+ const val = result[key];
658
+ const fn = transform;
659
+ if (val === void 0 || isOperation(val) && val.$remove === true) {} else if (!isOperation(val)) result[key] = fn(val);
660
+ else if (val.$set !== void 0) result[key] = {
661
+ ...val,
662
+ $set: fn(val.$set)
663
+ };
664
+ else if (val.$ifNotExists !== void 0) {
665
+ const ifne = val.$ifNotExists;
666
+ result[key] = {
667
+ ...val,
668
+ $ifNotExists: Array.isArray(ifne) ? [ifne[0], fn(ifne[1])] : fn(ifne)
611
669
  };
612
670
  }
613
671
  }
614
- for (const key of [...this.config.derivedAttributes ?? [], ...this.config.immutableAttributes ?? []]) delete result[key];
672
+ if (computedAttributes) for (const [computedKey, def] of Object.entries(computedAttributes)) {
673
+ const { from, compute } = def;
674
+ if (!(from in result)) continue;
675
+ const val = result[from];
676
+ if (val === void 0 || isOperation(val) && val.$remove === true) result[computedKey] = void 0;
677
+ else if (!isOperation(val)) result[computedKey] = compute(val);
678
+ else if (val.$set !== void 0) result[computedKey] = compute(val.$set);
679
+ else throw new DinahError({
680
+ type: "VALIDATION",
681
+ message: `Field "${from}" drives computed field "${computedKey}" and cannot use arithmetic or list operators in updates.`
682
+ });
683
+ }
615
684
  return result;
616
685
  }
686
+ withDiscriminatorCondition(key, condition) {
687
+ const discriminator = this.config.discriminator;
688
+ if (!discriminator) return condition;
689
+ const value = key[discriminator];
690
+ if (value === void 0) return condition;
691
+ const guard = { [discriminator]: value };
692
+ if (!condition) return guard;
693
+ return { $and: [guard, condition] };
694
+ }
617
695
  applyTransformsIfNeeded(items, options) {
618
696
  if (options?.projection?.length) return items;
619
697
  if (options?.gsi) {
@@ -683,7 +761,7 @@ var Db = class {
683
761
  ExpressionAttributeNames: exp.attributeNames
684
762
  });
685
763
  const output = await this.client.send(input);
686
- if (output.Item && data.filter && !data.filter(output.Item)) return;
764
+ if (output.Item && data.filter && !matchesPartial(data.filter, output.Item)) return;
687
765
  return output.Item;
688
766
  }
689
767
  async getOrThrow(data) {
@@ -886,7 +964,7 @@ var Db = class {
886
964
  const output = await this.client.send(input);
887
965
  for (const [table, items] of Object.entries(output.Responses ?? {})) {
888
966
  if (!result.items[table]) continue;
889
- const filteredItems = tableData[table]?.filter ? items.filter(tableData[table].filter) : items;
967
+ const filteredItems = tableData[table]?.filter ? items.filter((item) => matchesPartial(tableData[table].filter, item)) : items;
890
968
  result.items[table].push(...filteredItems);
891
969
  }
892
970
  let unprocessedKeyCount = 0;
@@ -1047,7 +1125,7 @@ var Db = class {
1047
1125
  return (await this.client.send(input)).Responses?.map((response, i) => {
1048
1126
  if (!response.Item) return;
1049
1127
  if (!requests[i]?.filter) return response.Item;
1050
- return requests[i].filter(response.Item) ? response.Item : void 0;
1128
+ return matchesPartial(requests[i].filter, response.Item) ? response.Item : void 0;
1051
1129
  }) ?? [];
1052
1130
  }
1053
1131
  async trxGetOrThrow(...requests) {
@@ -1111,6 +1189,6 @@ var Table = class {
1111
1189
  }
1112
1190
  };
1113
1191
  //#endregion
1114
- export { Db, DinahError, Repo, Table, chunk, extractTableDesc, isDinahError, makeRepo, removeUndefined, resolveAttrType };
1192
+ export { Db, DinahError, Repo, Table, chunk, extractTableDesc, isDinahError, makeRepo, matchesPartial, removeUndefined, resolveAttrType };
1115
1193
 
1116
1194
  //# sourceMappingURL=index.mjs.map