dyno-table 0.0.1 → 0.0.2

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
@@ -1,34 +1,3 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- BaseRepository: () => BaseRepository,
24
- ConditionalCheckFailedError: () => ConditionalCheckFailedError,
25
- DynamoError: () => DynamoError,
26
- ExponentialBackoffStrategy: () => ExponentialBackoffStrategy,
27
- ResourceNotFoundError: () => ResourceNotFoundError,
28
- Table: () => Table
29
- });
30
- module.exports = __toCommonJS(index_exports);
31
-
32
1
  // src/builders/expression-builder.ts
33
2
  var ExpressionBuilder = class {
34
3
  nameCount = 0;
@@ -98,10 +67,7 @@ var ExpressionBuilder = class {
98
67
  attributes: this.formatAttributes(attributes)
99
68
  };
100
69
  }
101
- formatAttributes({
102
- names,
103
- values
104
- }) {
70
+ formatAttributes({ names, values }) {
105
71
  return {
106
72
  ...Object.keys(names).length && { names },
107
73
  ...Object.keys(values).length && { values }
@@ -118,18 +84,9 @@ var ExpressionBuilder = class {
118
84
  const skName = this.generateAlias("name", "sk");
119
85
  attributes.names[skName] = indexConfig.skName;
120
86
  if (typeof key.sk === "string") {
121
- conditions.push(
122
- `${skName} = ${this.addValue(attributes, key.sk, "sk")}`
123
- );
87
+ conditions.push(`${skName} = ${this.addValue(attributes, key.sk, "sk")}`);
124
88
  } else {
125
- conditions.push(
126
- this.buildComparison(
127
- skName,
128
- key.sk.operator,
129
- key.sk.value,
130
- attributes
131
- )
132
- );
89
+ conditions.push(this.buildComparison(skName, key.sk.operator, key.sk.value, attributes));
133
90
  }
134
91
  }
135
92
  return {
@@ -192,6 +149,39 @@ var OperationBuilder = class {
192
149
  whereIn(field, values) {
193
150
  return this.where(field, "IN", values);
194
151
  }
152
+ whereLessThan(field, value) {
153
+ return this.where(field, "<", value);
154
+ }
155
+ whereLessThanOrEqual(field, value) {
156
+ return this.where(field, "<=", value);
157
+ }
158
+ whereGreaterThan(field, value) {
159
+ return this.where(field, ">", value);
160
+ }
161
+ whereGreaterThanOrEqual(field, value) {
162
+ return this.where(field, ">=", value);
163
+ }
164
+ whereNotEqual(field, value) {
165
+ return this.where(field, "<>", value);
166
+ }
167
+ whereBeginsWith(field, value) {
168
+ return this.where(field, "begins_with", value);
169
+ }
170
+ whereContains(field, value) {
171
+ return this.where(field, "contains", value);
172
+ }
173
+ whereNotContains(field, value) {
174
+ this.conditions.push({ field, operator: "not_contains", value });
175
+ return this;
176
+ }
177
+ whereAttributeType(field, value) {
178
+ this.conditions.push({ field, operator: "attribute_type", value });
179
+ return this;
180
+ }
181
+ whereSize(field, value) {
182
+ this.conditions.push({ field, operator: "size", value });
183
+ return this;
184
+ }
195
185
  buildConditionExpression() {
196
186
  return this.expressionBuilder.createExpression(this.conditions);
197
187
  }
@@ -201,8 +191,17 @@ var OperationBuilder = class {
201
191
  var PutBuilder = class extends OperationBuilder {
202
192
  constructor(item, expressionBuilder, onBuild) {
203
193
  super(expressionBuilder);
204
- this.item = item;
205
194
  this.onBuild = onBuild;
195
+ this.item = item;
196
+ }
197
+ item;
198
+ set(field, value) {
199
+ this.item[field] = value;
200
+ return this;
201
+ }
202
+ setMany(attributes) {
203
+ this.item = { ...this.item, ...attributes };
204
+ return this;
206
205
  }
207
206
  build() {
208
207
  const { expression, attributes } = this.buildConditionExpression();
@@ -216,6 +215,11 @@ var PutBuilder = class extends OperationBuilder {
216
215
  } : void 0
217
216
  };
218
217
  }
218
+ /**
219
+ * Runs the put operation to insert the provided attributes into the table.
220
+ *
221
+ * @returns The provided attributes. This does not load the model from the DB after insert
222
+ */
219
223
  async execute() {
220
224
  return this.onBuild(this.build());
221
225
  }
@@ -241,10 +245,7 @@ var QueryBuilder = class extends OperationBuilder {
241
245
  }
242
246
  build() {
243
247
  const filter = this.buildConditionExpression();
244
- const keyCondition = this.expressionBuilder.buildKeyCondition(
245
- this.key,
246
- this.indexConfig
247
- );
248
+ const keyCondition = this.expressionBuilder.buildKeyCondition(this.key, this.indexConfig);
248
249
  return {
249
250
  type: "query",
250
251
  keyCondition: {
@@ -278,8 +279,8 @@ var UpdateBuilder = class extends OperationBuilder {
278
279
  this.updates[field] = value;
279
280
  return this;
280
281
  }
281
- setMany(attribtues) {
282
- this.updates = { ...this.updates, ...attribtues };
282
+ setMany(attributes) {
283
+ this.updates = { ...this.updates, ...attributes };
283
284
  return this;
284
285
  }
285
286
  remove(...fields) {
@@ -351,10 +352,7 @@ function translateExpression(expression, attributes) {
351
352
  }
352
353
  if (attributes.values) {
353
354
  for (const [alias, value] of Object.entries(attributes.values)) {
354
- translated = translated.replace(
355
- new RegExp(alias, "g"),
356
- typeof value === "string" ? `"${value}"` : String(value)
357
- );
355
+ translated = translated.replace(new RegExp(alias, "g"), typeof value === "string" ? `"${value}"` : String(value));
358
356
  }
359
357
  }
360
358
  return translated;
@@ -372,28 +370,20 @@ Key: ${JSON.stringify(context.key, null, 2)}`);
372
370
  if (context.expression) {
373
371
  const { condition, update, filter, keyCondition } = context.expression;
374
372
  if (condition) {
375
- parts.push(
376
- `
377
- Condition: ${translateExpression(condition, context.attributes)}`
378
- );
373
+ parts.push(`
374
+ Condition: ${translateExpression(condition, context.attributes)}`);
379
375
  }
380
376
  if (update) {
381
- parts.push(
382
- `
383
- Update: ${translateExpression(update, context.attributes)}`
384
- );
377
+ parts.push(`
378
+ Update: ${translateExpression(update, context.attributes)}`);
385
379
  }
386
380
  if (filter) {
387
- parts.push(
388
- `
389
- Filter: ${translateExpression(filter, context.attributes)}`
390
- );
381
+ parts.push(`
382
+ Filter: ${translateExpression(filter, context.attributes)}`);
391
383
  }
392
384
  if (keyCondition) {
393
- parts.push(
394
- `
395
- Key Condition: ${translateExpression(keyCondition, context.attributes)}`
396
- );
385
+ parts.push(`
386
+ Key Condition: ${translateExpression(keyCondition, context.attributes)}`);
397
387
  }
398
388
  }
399
389
  parts.push(`
@@ -682,7 +672,10 @@ var DynamoService = class {
682
672
  async put(options) {
683
673
  try {
684
674
  const params = this.converter.toPutCommand(options);
685
- return await this.withRetry(() => this.client.put(params));
675
+ return await this.withRetry(async () => {
676
+ await this.client.put(params);
677
+ return options.item;
678
+ });
686
679
  } catch (error) {
687
680
  handleDynamoError(error, {
688
681
  operation: "PUT",
@@ -772,9 +765,7 @@ var DynamoService = class {
772
765
  async batchWrite(items) {
773
766
  try {
774
767
  const chunks = this.chunkArray(items, BATCH_WRITE_LIMIT);
775
- return await Promise.all(
776
- chunks.map((chunk) => this.processBatchWrite(chunk))
777
- );
768
+ return await Promise.all(chunks.map((chunk) => this.processBatchWrite(chunk)));
778
769
  } catch (error) {
779
770
  handleDynamoError(error, {
780
771
  operation: "BATCH_WRITE",
@@ -784,9 +775,7 @@ var DynamoService = class {
784
775
  }
785
776
  async transactWrite(items) {
786
777
  if (items.length > TRANSACTION_LIMIT) {
787
- throw new Error(
788
- `Transaction limit exceeded. Maximum is ${TRANSACTION_LIMIT} items, got ${items.length}`
789
- );
778
+ throw new Error(`Transaction limit exceeded. Maximum is ${TRANSACTION_LIMIT} items, got ${items.length}`);
790
779
  }
791
780
  try {
792
781
  const params = this.converter.toTransactWriteCommand(items);
@@ -823,10 +812,9 @@ var DynamoService = class {
823
812
  const processUnprocessedItems = async (unprocessedItems2) => {
824
813
  const params2 = this.converter.toBatchWriteCommand(unprocessedItems2);
825
814
  const result = await this.client.batchWrite(params2);
826
- if (result.UnprocessedItems?.[this.tableName]?.length) {
827
- const remainingItems = this.converter.fromBatchWriteResponse(
828
- result.UnprocessedItems[this.tableName]
829
- );
815
+ const outstandingItems = result.UnprocessedItems?.[this.tableName];
816
+ if (outstandingItems && outstandingItems.length > 0) {
817
+ const remainingItems = this.converter.fromBatchWriteResponse(outstandingItems);
830
818
  throw {
831
819
  name: "UnprocessedItemsError",
832
820
  unprocessedItems: remainingItems
@@ -836,12 +824,11 @@ var DynamoService = class {
836
824
  };
837
825
  const params = this.converter.toBatchWriteCommand(items);
838
826
  const initialResult = await this.client.batchWrite(params);
839
- if (!initialResult.UnprocessedItems?.[this.tableName]?.length) {
827
+ const rawUnprocessedItems = initialResult.UnprocessedItems?.[this.tableName];
828
+ if (!rawUnprocessedItems || rawUnprocessedItems.length === 0) {
840
829
  return initialResult;
841
830
  }
842
- const unprocessedItems = this.converter.fromBatchWriteResponse(
843
- initialResult.UnprocessedItems[this.tableName]
844
- );
831
+ const unprocessedItems = this.converter.fromBatchWriteResponse(rawUnprocessedItems);
845
832
  return this.withRetry(() => processUnprocessedItems(unprocessedItems));
846
833
  }
847
834
  async withRetry(operation, strategy = new ExponentialBackoffStrategy()) {
@@ -853,9 +840,7 @@ var DynamoService = class {
853
840
  if (!strategy.shouldRetry(error, attempt)) {
854
841
  throw error;
855
842
  }
856
- await new Promise(
857
- (resolve) => setTimeout(resolve, strategy.getDelay(attempt))
858
- );
843
+ await new Promise((resolve) => setTimeout(resolve, strategy.getDelay(attempt)));
859
844
  attempt++;
860
845
  }
861
846
  }
@@ -868,6 +853,46 @@ var DynamoService = class {
868
853
  }
869
854
  };
870
855
 
856
+ // src/builders/scan-builder.ts
857
+ var ScanBuilder = class extends OperationBuilder {
858
+ constructor(expressionBuilder, onBuild) {
859
+ super(expressionBuilder);
860
+ this.onBuild = onBuild;
861
+ }
862
+ limitValue;
863
+ indexNameValue;
864
+ pageKeyValue;
865
+ limit(value) {
866
+ this.limitValue = value;
867
+ return this;
868
+ }
869
+ useIndex(indexName) {
870
+ this.indexNameValue = indexName;
871
+ return this;
872
+ }
873
+ startKey(key) {
874
+ this.pageKeyValue = key;
875
+ return this;
876
+ }
877
+ build() {
878
+ const filter = this.buildConditionExpression();
879
+ return {
880
+ type: "scan",
881
+ filter: filter.expression ? {
882
+ expression: filter.expression,
883
+ names: filter.attributes.names,
884
+ values: filter.attributes.values
885
+ } : void 0,
886
+ limit: this.limitValue,
887
+ pageKey: this.pageKeyValue,
888
+ indexName: this.indexNameValue
889
+ };
890
+ }
891
+ async execute() {
892
+ return this.onBuild(this.build());
893
+ }
894
+ };
895
+
871
896
  // src/table.ts
872
897
  var Table = class {
873
898
  dynamoService;
@@ -893,18 +918,10 @@ var Table = class {
893
918
  throw new Error(`Index ${indexName} does not exist`);
894
919
  }
895
920
  put(item) {
896
- return new PutBuilder(
897
- item,
898
- this.expressionBuilder,
899
- (operation) => this.executeOperation(operation)
900
- );
921
+ return new PutBuilder(item, this.expressionBuilder, (operation) => this.executeOperation(operation));
901
922
  }
902
923
  update(key, data) {
903
- const builder = new UpdateBuilder(
904
- key,
905
- this.expressionBuilder,
906
- (operation) => this.executeOperation(operation)
907
- );
924
+ const builder = new UpdateBuilder(key, this.expressionBuilder, (operation) => this.executeOperation(operation));
908
925
  if (data) {
909
926
  builder.setMany(data);
910
927
  }
@@ -931,22 +948,8 @@ var Table = class {
931
948
  };
932
949
  return this.executeOperation(operation);
933
950
  }
934
- async scan(filters, options) {
935
- let filter = void 0;
936
- if (filters?.length) {
937
- const filterResult = this.expressionBuilder.createExpression(filters);
938
- filter = {
939
- expression: filterResult.expression,
940
- names: filterResult.attributes.names,
941
- values: filterResult.attributes.values
942
- };
943
- }
944
- return this.dynamoService.scan({
945
- filter,
946
- limit: options?.limit,
947
- pageKey: options?.pageKey,
948
- indexName: options?.indexName
949
- });
951
+ scan() {
952
+ return new ScanBuilder(this.expressionBuilder, (operation) => this.executeOperation(operation));
950
953
  }
951
954
  async batchWrite(operations) {
952
955
  const batchOperation = {
@@ -996,6 +999,13 @@ var Table = class {
996
999
  return this.dynamoService.batchWrite(operation.operations);
997
1000
  case "transactWrite":
998
1001
  return this.dynamoService.transactWrite(operation.operations);
1002
+ case "scan":
1003
+ return this.dynamoService.scan({
1004
+ filter: operation.filter,
1005
+ limit: operation.limit,
1006
+ pageKey: operation.pageKey,
1007
+ indexName: operation.indexName
1008
+ });
999
1009
  default:
1000
1010
  throw new Error("Unknown operation type");
1001
1011
  }
@@ -1025,59 +1035,143 @@ var Table = class {
1025
1035
 
1026
1036
  // src/repository/base-repository.ts
1027
1037
  var BaseRepository = class {
1028
- constructor(table, schema) {
1038
+ constructor(table) {
1029
1039
  this.table = table;
1030
- this.schema = schema;
1031
1040
  }
1041
+ /**
1042
+ * Hook method called before inserting a record.
1043
+ * Subclasses can override this method to modify the data before insertion.
1044
+ * @param data - The record data.
1045
+ * @returns The modified record data.
1046
+ */
1032
1047
  beforeInsert(data) {
1033
1048
  return data;
1034
1049
  }
1050
+ /**
1051
+ * Hook method called before updating a record.
1052
+ * Subclasses can override this method to modify the data before updating.
1053
+ * @param data - The partial record data to be updated.
1054
+ * @returns The modified partial record data.
1055
+ */
1035
1056
  beforeUpdate(data) {
1036
1057
  return data;
1037
1058
  }
1038
- async create(data) {
1039
- const parsed = this.schema.parse(data);
1040
- const key = this.createPrimaryKey(parsed);
1059
+ /**
1060
+ * Checks if a record exists in the table.
1061
+ * @param key - The primary key of the record.
1062
+ * @returns A promise that resolves to true if the record exists, false otherwise.
1063
+ */
1064
+ async exists(key) {
1065
+ const item = await this.table.get(key);
1066
+ return item !== null;
1067
+ }
1068
+ /**
1069
+ * Type guard to check if the value is a primary key.
1070
+ * @param value - The value to check.
1071
+ * @returns True if the value is a primary key, false otherwise.
1072
+ */
1073
+ isPrimaryKey(value) {
1074
+ return "pk" in value && typeof value.pk === "string";
1075
+ }
1076
+ /**
1077
+ * Creates a new record in the table.
1078
+ * @param data - The record data.
1079
+ * @returns A PutBuilder instance to execute the put operation.
1080
+ */
1081
+ create(data) {
1082
+ const key = this.createPrimaryKey(data);
1041
1083
  const item = {
1042
- ...parsed,
1084
+ ...data,
1043
1085
  ...key
1044
1086
  };
1045
1087
  const indexConfig = this.table.getIndexConfig();
1046
- await this.table.put(item).whereNotExists(indexConfig.pkName).execute();
1047
- return parsed;
1088
+ const builder = this.table.put(item).set(this.getTypeAttributeName(), this.getType()).whereNotExists(indexConfig.pkName);
1089
+ if (indexConfig.skName) {
1090
+ builder.whereNotExists(indexConfig.skName);
1091
+ }
1092
+ return builder;
1048
1093
  }
1094
+ /**
1095
+ * Updates an existing record in the table.
1096
+ * @param key - The primary key of the record.
1097
+ * @param updates - The partial record data to be updated.
1098
+ * @returns A promise that resolves to the updated record or null if the record does not exist.
1099
+ */
1049
1100
  async update(key, updates) {
1050
- const parsed = this.schema.parse(updates);
1051
- const result = await this.table.update(key).setMany(parsed).execute();
1052
- return result.Attributes ? this.schema.parse(result.Attributes) : null;
1101
+ const processed = this.beforeUpdate(updates);
1102
+ const updateData = {
1103
+ ...processed,
1104
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1105
+ };
1106
+ const result = await this.table.update(key).setMany(updateData).execute();
1107
+ if (!result.Attributes) return null;
1108
+ return this.findOne(key);
1053
1109
  }
1054
- async delete(key) {
1110
+ /**
1111
+ * Upserts (inserts or updates) a record in the table.
1112
+ * @param data - The record data.
1113
+ * @returns A PutBuilder instance to execute the put operation.
1114
+ */
1115
+ upsert(data) {
1116
+ const key = this.createPrimaryKey(data);
1117
+ return this.table.put({
1118
+ ...data,
1119
+ ...key
1120
+ });
1121
+ }
1122
+ /**
1123
+ * Deletes a record from the table.
1124
+ * @param keyOrDTO - The primary key or the record data.
1125
+ * @returns A promise that resolves when the record is deleted.
1126
+ */
1127
+ async delete(keyOrDTO) {
1128
+ if (this.isPrimaryKey(keyOrDTO)) {
1129
+ this.table.delete(keyOrDTO);
1130
+ return;
1131
+ }
1132
+ const key = this.createPrimaryKey(keyOrDTO);
1055
1133
  await this.table.delete(key);
1056
1134
  }
1135
+ /**
1136
+ * Finds a single record by its primary key.
1137
+ * @param key - The primary key of the record.
1138
+ * @returns A promise that resolves to the record or null if the record does not exist.
1139
+ */
1057
1140
  async findOne(key) {
1058
- const item = await this.table.query(key).where(this.getTypeAttributeName(), "=", this.getType()).execute();
1141
+ const results = await this.table.query(key).whereEquals(this.getTypeAttributeName(), this.getType()).limit(1).execute();
1142
+ const item = results.Items?.[0];
1059
1143
  if (!item) {
1060
1144
  return null;
1061
1145
  }
1062
- return this.schema.parse(item);
1146
+ return item;
1063
1147
  }
1148
+ /**
1149
+ * Finds a single record by its primary key or throws an error if the record does not exist.
1150
+ * @param key - The primary key of the record.
1151
+ * @returns A promise that resolves to the record.
1152
+ * @throws An error if the record does not exist.
1153
+ */
1064
1154
  async findOrFail(key) {
1065
1155
  const result = await this.findOne(key);
1066
1156
  if (!result) {
1067
1157
  throw new Error("Item not found");
1068
1158
  }
1069
- return this.schema.parse(result);
1159
+ return result;
1070
1160
  }
1161
+ /**
1162
+ * Creates a query builder for querying records by their primary key.
1163
+ * @param key - The primary key of the record.
1164
+ * @returns A QueryBuilder instance to build and execute the query.
1165
+ */
1071
1166
  query(key) {
1072
- return this.table.query(key).where(this.getTypeAttributeName(), "=", this.getType());
1167
+ return this.table.query(key).whereEquals(this.getTypeAttributeName(), this.getType());
1073
1168
  }
1074
1169
  };
1075
- // Annotate the CommonJS export names for ESM import in node:
1076
- 0 && (module.exports = {
1170
+ export {
1077
1171
  BaseRepository,
1078
1172
  ConditionalCheckFailedError,
1079
1173
  DynamoError,
1080
1174
  ExponentialBackoffStrategy,
1081
1175
  ResourceNotFoundError,
1082
1176
  Table
1083
- });
1177
+ };
package/package.json CHANGED
@@ -1,16 +1,25 @@
1
1
  {
2
2
  "name": "dyno-table",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A TypeScript library to simplify working with DynamoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
- "type": "commonjs",
7
+ "type": "module",
8
8
  "exports": {
9
9
  ".": {
10
10
  "import": "./dist/index.mjs",
11
11
  "require": "./dist/index.js"
12
12
  }
13
13
  },
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm --dts",
16
+ "clean": "rm -rf dist",
17
+ "test": "vitest",
18
+ "test:int": "vitest --config vitest.integration.ts",
19
+ "lint": "biome lint",
20
+ "check-types": "tsc --noEmit",
21
+ "ddb:start": "docker compose up -d dynamodb"
22
+ },
14
23
  "keywords": [
15
24
  "dynamodb",
16
25
  "aws",
@@ -24,28 +33,19 @@
24
33
  "url": "git+https://github.com/Kysumi/dyno-table.git"
25
34
  },
26
35
  "devDependencies": {
36
+ "@babel/preset-typescript": "^7.26.0",
27
37
  "@biomejs/biome": "1.9.4",
28
38
  "@types/node": "^20.0.0",
29
- "typescript": "^5.0.0",
30
- "vitest": "^2.1.8",
31
39
  "rimraf": "^5.0.0",
32
- "tsup": "^8.0.0"
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^5.0.0",
42
+ "vitest": "^2.1.8"
33
43
  },
34
44
  "peerDependencies": {
35
45
  "@aws-sdk/client-dynamodb": "^3.0.0",
36
- "@aws-sdk/lib-dynamodb": "^3.0.0",
37
- "zod": "^3.0.0"
46
+ "@aws-sdk/lib-dynamodb": "^3.0.0"
38
47
  },
39
48
  "files": [
40
49
  "dist"
41
- ],
42
- "scripts": {
43
- "build": "tsup src/index.ts --format cjs,esm --dts",
44
- "clean": "rm -rf dist",
45
- "test": "vitest",
46
- "test:int": "vitest --config vitest.integration.ts",
47
- "lint": "biome lint",
48
- "check-types": "tsc --noEmit",
49
- "ddb:start": "docker compose up -d dynamodb"
50
- }
51
- }
50
+ ]
51
+ }