dinah 0.13.1 → 0.15.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.mjs CHANGED
@@ -4,8 +4,9 @@ import * as Lib from "@aws-sdk/lib-dynamodb";
4
4
  //#region src/error.ts
5
5
  function dinahErrorMessage(details) {
6
6
  switch (details.type) {
7
- case "NOT_FOUND": return `${details.resource ?? "Item"} not found: ${JSON.stringify(details.key)}`;
8
- case "CONDITIONAL_CHECK_FAILED": return details.key ? `${details.resource ?? "Item"} condition check failed: ${JSON.stringify(details.key)}` : `${details.resource ?? "Item"} condition check failed`;
7
+ case "NOT_FOUND": return `${details.resource ?? "Item"} not found.`;
8
+ case "ALREADY_EXISTS": return `${details.resource ?? "Item"} already exists.`;
9
+ case "CONDITIONAL_CHECK_FAILED": return `${details.resource ?? "Item"} condition check failed.`;
9
10
  case "VALIDATION": return `Validation error: ${details.message}`;
10
11
  case "DATA_INTEGRITY": return `Data integrity error: ${details.message}`;
11
12
  case "TRANSACTION_CANCELED": return `Transaction canceled: ${details.reasons.map((r) => r.type).join(", ")}`;
@@ -114,7 +115,10 @@ var ExpressionBuilder = class {
114
115
  const setDelOperations = [];
115
116
  for (const [path, valOrOperation] of Object.entries(expression)) {
116
117
  const placeholder = this.getPathSub(path);
117
- if (valOrOperation === void 0) removeOperations.push(placeholder);
118
+ if (valOrOperation === void 0) throw new DinahError({
119
+ type: "VALIDATION",
120
+ message: `Field "${path}" cannot be set to undefined in an update expression. Use { $remove: true } to remove an optional attribute.`
121
+ });
118
122
  else if (isOperation(valOrOperation)) if (valOrOperation.$remove === true) removeOperations.push(placeholder);
119
123
  else if (valOrOperation.$setAdd !== void 0 || valOrOperation.$setDel !== void 0) {
120
124
  const operations = valOrOperation.$setAdd ? setAddOperations : setDelOperations;
@@ -202,7 +206,10 @@ var ExpressionBuilder = class {
202
206
  const sets = [];
203
207
  const removes = [];
204
208
  const params = [];
205
- for (const [path, valOrOp] of Object.entries(expression)) if (valOrOp === void 0) removes.push(`"${path}"`);
209
+ for (const [path, valOrOp] of Object.entries(expression)) if (valOrOp === void 0) throw new DinahError({
210
+ type: "VALIDATION",
211
+ message: `Field "${path}" cannot be set to undefined in an update expression. Use { $remove: true } to remove an optional attribute.`
212
+ });
206
213
  else if (isOperation(valOrOp)) if (valOrOp.$remove === true) removes.push(`"${path}"`);
207
214
  else sets.push(`"${path}"=${this.resolvePartiQLSetOperand(path, valOrOp, params)}`);
208
215
  else {
@@ -309,18 +316,25 @@ var Repo = class {
309
316
  });
310
317
  return this.applyTransformIfNeeded(result);
311
318
  }
312
- async create(item, options) {
313
- const { condition: otherCondition, ...otherOptions } = options ?? {};
319
+ async create(item) {
314
320
  const condition = { $and: [{ [this.table.def.partitionKey]: { $exists: false } }] };
315
- if (otherCondition) condition.$and.push(otherCondition);
316
321
  const normalizedItem = this.applyCreateTransforms(item);
317
- const result = await this.db.put({
318
- table: this.tableName,
319
- item: normalizedItem,
320
- resource: this.resourceName,
321
- condition,
322
- ...otherOptions
323
- });
322
+ let result;
323
+ try {
324
+ result = await this.db.put({
325
+ table: this.tableName,
326
+ item: normalizedItem,
327
+ resource: this.resourceName,
328
+ condition
329
+ });
330
+ } catch (err) {
331
+ if (err instanceof DinahError && err.details.type === "CONDITIONAL_CHECK_FAILED") throw new DinahError({
332
+ type: "ALREADY_EXISTS",
333
+ key: this.extractKey(item),
334
+ resource: this.resourceName
335
+ }, { cause: err });
336
+ throw err;
337
+ }
324
338
  return this.applyTransformIfNeeded(result);
325
339
  }
326
340
  async delete(key, options) {
@@ -555,8 +569,8 @@ var Repo = class {
555
569
  async trxUpdate(keys, update, options) {
556
570
  return this.db.trxWrite(...keys.map((key) => this.trxUpdateRequest(key, update, options)));
557
571
  }
558
- async trxCreate(items, options) {
559
- return this.db.trxWrite(...items.map((item) => this.trxCreateRequest(item, options)));
572
+ async trxCreate(items) {
573
+ return this.db.trxWrite(...items.map((item) => this.trxCreateRequest(item)));
560
574
  }
561
575
  trxGetRequest(key, options) {
562
576
  return {
@@ -600,17 +614,14 @@ var Repo = class {
600
614
  condition: this.withDiscriminatorCondition(key, options?.condition)
601
615
  };
602
616
  }
603
- trxCreateRequest(item, options) {
604
- const { condition: otherCondition, ...otherOptions } = options ?? {};
617
+ trxCreateRequest(item) {
605
618
  const condition = { $and: [{ [this.table.def.partitionKey]: { $exists: false } }] };
606
- if (otherCondition) condition.$and.push(otherCondition);
607
619
  const normalizedItem = this.applyCreateTransforms(item);
608
620
  return {
609
621
  table: this.tableName,
610
622
  type: "PUT",
611
623
  item: normalizedItem,
612
- condition,
613
- ...otherOptions
624
+ condition
614
625
  };
615
626
  }
616
627
  applyCreateTransforms(item) {
@@ -630,7 +641,7 @@ var Repo = class {
630
641
  }
631
642
  if (computedAttributes) for (const [key, def] of Object.entries(computedAttributes)) {
632
643
  const { from, compute } = def;
633
- const computed = compute(merged[from]);
644
+ const computed = compute(...Array.isArray(from) ? from.map((k) => merged[k]) : [merged[from]]);
634
645
  if (computed !== void 0) merged[key] = computed;
635
646
  else delete merged[key];
636
647
  }
@@ -671,15 +682,42 @@ var Repo = class {
671
682
  }
672
683
  if (computedAttributes) for (const [computedKey, def] of Object.entries(computedAttributes)) {
673
684
  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
- });
685
+ if (Array.isArray(from)) {
686
+ const fromArr = from;
687
+ if (fromArr.filter((k) => k in result).length === 0) continue;
688
+ const missingKeys = fromArr.filter((k) => !(k in result));
689
+ if (missingKeys.length > 0) throw new DinahError({
690
+ type: "VALIDATION",
691
+ message: `Field "${computedKey}" is computed from [${fromArr.join(", ")}]. All source fields must be included in the update or none. Missing: [${missingKeys.join(", ")}].`
692
+ });
693
+ const args = [];
694
+ for (const key of fromArr) {
695
+ const val = result[key];
696
+ if (val === void 0 || isOperation(val) && val.$remove === true) args.push(void 0);
697
+ else if (!isOperation(val)) args.push(val);
698
+ else if (val.$set !== void 0) args.push(val.$set);
699
+ else throw new DinahError({
700
+ type: "VALIDATION",
701
+ message: `Field "${key}" drives computed field "${computedKey}" and cannot use arithmetic or list operators in updates.`
702
+ });
703
+ }
704
+ const computed = compute(...args);
705
+ result[computedKey] = computed !== void 0 ? computed : { $remove: true };
706
+ } else {
707
+ const fromKey = from;
708
+ if (!(fromKey in result)) continue;
709
+ const val = result[fromKey];
710
+ let resolvedVal;
711
+ if (val === void 0 || isOperation(val) && val.$remove === true) resolvedVal = void 0;
712
+ else if (!isOperation(val)) resolvedVal = val;
713
+ else if (val.$set !== void 0) resolvedVal = val.$set;
714
+ else throw new DinahError({
715
+ type: "VALIDATION",
716
+ message: `Field "${fromKey}" drives computed field "${computedKey}" and cannot use arithmetic or list operators in updates.`
717
+ });
718
+ const computed = compute(resolvedVal);
719
+ result[computedKey] = computed !== void 0 ? computed : { $remove: true };
720
+ }
683
721
  }
684
722
  return result;
685
723
  }
@@ -723,12 +761,19 @@ var Db = class {
723
761
  else {
724
762
  const { endpoint, ...clientConfig } = clientOrConfig;
725
763
  this.client = Lib.DynamoDBDocumentClient.from(new DynamoDBClient({
726
- endpoint: endpoint || void 0,
764
+ ...endpoint ? { endpoint } : {},
727
765
  ...clientConfig
728
766
  }));
729
767
  }
730
768
  this.config = dbConfig;
731
769
  }
770
+ normalizeUpdate(expression) {
771
+ const behavior = this.config?.updateUndefinedBehavior ?? "throw";
772
+ if (behavior === "throw") return expression;
773
+ const entries = Object.entries(expression);
774
+ if (behavior === "strip") return Object.fromEntries(entries.filter(([, v]) => v !== void 0));
775
+ return Object.fromEntries(entries.map(([k, v]) => [k, v === void 0 ? { $remove: true } : v]));
776
+ }
732
777
  makeRepo(table, config) {
733
778
  return new Repo(this, table, config);
734
779
  }
@@ -805,7 +850,7 @@ var Db = class {
805
850
  Key: data.key,
806
851
  ReturnValues: "ALL_NEW",
807
852
  ReturnValuesOnConditionCheckFailure: "ALL_OLD",
808
- UpdateExpression: exp.update(data.update),
853
+ UpdateExpression: exp.update(this.normalizeUpdate(data.update)),
809
854
  ConditionExpression: exp.condition(condition),
810
855
  ExpressionAttributeNames: exp.attributeNames,
811
856
  ExpressionAttributeValues: exp.attributeValues
@@ -1072,7 +1117,7 @@ var Db = class {
1072
1117
  while (queue.length && retryCount < 5) {
1073
1118
  const batch = queue.splice(0, batchSize || 1);
1074
1119
  const statements = batch.map(([table, key, update]) => {
1075
- const { sets, removes, params } = new ExpressionBuilder().partiqlUpdate(update);
1120
+ const { sets, removes, params } = new ExpressionBuilder().partiqlUpdate(this.normalizeUpdate(update));
1076
1121
  const whereParts = [];
1077
1122
  for (const [field, value] of Object.entries(key)) {
1078
1123
  whereParts.push(`"${field}"=?`);
@@ -1157,7 +1202,7 @@ var Db = class {
1157
1202
  return { Update: {
1158
1203
  Key: request.key,
1159
1204
  TableName: request.table,
1160
- UpdateExpression: exp.update(request.update),
1205
+ UpdateExpression: exp.update(this.normalizeUpdate(request.update)),
1161
1206
  ConditionExpression: exp.condition(request.condition),
1162
1207
  ExpressionAttributeNames: exp.attributeNames,
1163
1208
  ExpressionAttributeValues: exp.attributeValues