dyno-table 0.1.4 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dyno-table",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "A TypeScript library to simplify working with DynamoDB",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/dist/index.d.mts DELETED
@@ -1,323 +0,0 @@
1
- import * as _aws_sdk_lib_dynamodb from '@aws-sdk/lib-dynamodb';
2
- import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
3
- import { z } from 'zod';
4
-
5
- interface ExpressionAttributes {
6
- names?: Record<string, string>;
7
- values?: Record<string, unknown>;
8
- }
9
- interface ExpressionResult {
10
- expression?: string;
11
- attributes: ExpressionAttributes;
12
- }
13
- type FunctionOperator = "attribute_exists" | "attribute_not_exists" | "begins_with" | "contains" | "not_contains" | "attribute_type";
14
- type ComparisonOperator = "=" | "<" | "<=" | ">" | ">=" | "<>";
15
- type SpecialOperator = "BETWEEN" | "IN" | "size";
16
- type ConditionOperator = FunctionOperator | ComparisonOperator | SpecialOperator;
17
- type FilterOperator = "=" | "<" | "<=" | ">" | ">=" | "<>" | "BETWEEN" | "IN" | "contains" | "begins_with";
18
- interface FilterCondition {
19
- field: string;
20
- operator: FilterOperator;
21
- value: unknown;
22
- }
23
- interface Condition {
24
- field: string;
25
- operator: ConditionOperator;
26
- value?: unknown;
27
- }
28
- type SKCondition = {
29
- operator: FilterOperator;
30
- value: string;
31
- };
32
- type PrimaryKey = {
33
- pk: string;
34
- sk?: SKCondition | string;
35
- };
36
- interface TableIndexConfig {
37
- pkName: string;
38
- skName?: string;
39
- }
40
-
41
- interface IExpressionBuilder {
42
- buildKeyCondition(key: PrimaryKey, indexConfig: TableIndexConfig): ExpressionResult;
43
- createExpression(filters: Condition[]): ExpressionResult;
44
- buildUpdateExpression(updates: Record<string, unknown>): ExpressionResult;
45
- }
46
- declare class ExpressionBuilder implements IExpressionBuilder {
47
- private nameCount;
48
- private valueCount;
49
- private generateAlias;
50
- private reset;
51
- private createAttributePath;
52
- private addValue;
53
- private buildComparison;
54
- createExpression(conditions: Array<{
55
- field: string;
56
- operator: ConditionOperator;
57
- value?: unknown;
58
- }>): ExpressionResult;
59
- private formatAttributes;
60
- buildKeyCondition(key: PrimaryKey, indexConfig: TableIndexConfig): ExpressionResult;
61
- buildUpdateExpression(updates: Record<string, unknown>): ExpressionResult;
62
- }
63
-
64
- type DynamoKey = Record<string, unknown>;
65
- interface DynamoExpression {
66
- expression?: string;
67
- names?: Record<string, string>;
68
- values?: Record<string, unknown>;
69
- }
70
- interface DynamoBatchWriteItem {
71
- put?: Record<string, unknown>;
72
- delete?: DynamoKey;
73
- }
74
- interface DynamoPutOperation {
75
- type: "put";
76
- item: Record<string, unknown>;
77
- condition?: DynamoExpression;
78
- }
79
- interface DynamoUpdateOperation {
80
- type: "update";
81
- key: PrimaryKeyWithoutExpression;
82
- update: DynamoExpression;
83
- condition?: DynamoExpression;
84
- }
85
- interface DynamoQueryOperation {
86
- type: "query";
87
- keyCondition?: DynamoExpression;
88
- filter?: DynamoExpression;
89
- limit?: number;
90
- indexName?: string;
91
- }
92
- interface DynamoDeleteOperation {
93
- type: "delete";
94
- key: PrimaryKeyWithoutExpression;
95
- condition?: DynamoExpression;
96
- }
97
- interface DynamoBatchWriteOperation {
98
- type: "batchWrite";
99
- operations: DynamoBatchWriteItem[];
100
- }
101
- interface DynamoTransactOperation {
102
- type: "transactWrite";
103
- operations: Array<{
104
- put?: {
105
- item: Record<string, unknown>;
106
- condition?: DynamoExpression;
107
- };
108
- delete?: {
109
- key: PrimaryKeyWithoutExpression;
110
- condition?: DynamoExpression;
111
- };
112
- update?: {
113
- key: PrimaryKeyWithoutExpression;
114
- update: DynamoExpression;
115
- condition?: DynamoExpression;
116
- };
117
- }>;
118
- }
119
- type DynamoOperation = DynamoPutOperation | DynamoUpdateOperation | DynamoQueryOperation | DynamoDeleteOperation | DynamoBatchWriteOperation | DynamoTransactOperation;
120
- type PrimaryKeyWithoutExpression = {
121
- pk: string;
122
- sk?: string;
123
- };
124
- type BatchWriteOperation = {
125
- type: "put";
126
- item: Record<string, unknown>;
127
- } | {
128
- type: "delete";
129
- key: PrimaryKeyWithoutExpression;
130
- };
131
-
132
- declare abstract class OperationBuilder<T extends DynamoOperation> {
133
- protected expressionBuilder: IExpressionBuilder;
134
- protected conditions: Array<{
135
- field: string;
136
- operator: ConditionOperator;
137
- value?: unknown;
138
- }>;
139
- constructor(expressionBuilder: IExpressionBuilder);
140
- where(field: string, operator: FilterOperator, value: unknown): this;
141
- whereExists(field: string): this;
142
- whereNotExists(field: string): this;
143
- whereEquals(field: string, value: unknown): this;
144
- whereBetween(field: string, start: unknown, end: unknown): this;
145
- whereIn(field: string, values: unknown[]): this;
146
- protected buildConditionExpression(): ExpressionResult;
147
- abstract build(): T;
148
- }
149
-
150
- declare class PutBuilder extends OperationBuilder<DynamoPutOperation> {
151
- private readonly item;
152
- private readonly onBuild;
153
- constructor(item: Record<string, unknown>, expressionBuilder: IExpressionBuilder, onBuild: (operation: DynamoPutOperation) => Promise<void>);
154
- build(): DynamoPutOperation;
155
- execute(): Promise<void>;
156
- }
157
-
158
- declare class QueryBuilder extends OperationBuilder<DynamoQueryOperation> {
159
- private readonly key;
160
- private readonly indexConfig;
161
- private readonly onBuild;
162
- private limitValue?;
163
- private indexNameValue?;
164
- constructor(key: PrimaryKey, indexConfig: TableIndexConfig, expressionBuilder: IExpressionBuilder, onBuild: (operation: DynamoQueryOperation) => Promise<{
165
- Items?: Record<string, unknown>[];
166
- Count?: number;
167
- ScannedCount?: number;
168
- LastEvaluatedKey?: Record<string, unknown>;
169
- }>);
170
- limit(value: number): this;
171
- useIndex(indexName: string): this;
172
- build(): DynamoQueryOperation;
173
- execute(): Promise<{
174
- Items?: Record<string, unknown>[];
175
- Count?: number;
176
- ScannedCount?: number;
177
- LastEvaluatedKey?: Record<string, unknown>;
178
- }>;
179
- }
180
-
181
- declare class UpdateBuilder extends OperationBuilder<DynamoUpdateOperation> {
182
- private readonly key;
183
- private readonly onBuild;
184
- private updates;
185
- constructor(key: PrimaryKeyWithoutExpression, expressionBuilder: IExpressionBuilder, onBuild: (operation: DynamoUpdateOperation) => Promise<{
186
- Attributes?: Record<string, unknown>;
187
- }>);
188
- set(field: string, value: unknown): this;
189
- setMany(attribtues: Record<string, unknown>): this;
190
- remove(...fields: string[]): this;
191
- increment(field: string, by?: number): this;
192
- build(): DynamoUpdateOperation;
193
- execute(): Promise<{
194
- Attributes?: Record<string, unknown>;
195
- }>;
196
- }
197
-
198
- declare class Table {
199
- private readonly dynamoService;
200
- private readonly expressionBuilder;
201
- private readonly indexes;
202
- constructor({ client, tableName, tableIndexes, expressionBuilder, }: {
203
- client: DynamoDBDocument;
204
- tableName: string;
205
- tableIndexes: Record<string, TableIndexConfig>;
206
- expressionBuilder?: ExpressionBuilder;
207
- });
208
- getIndexConfig(indexName?: string): TableIndexConfig;
209
- put(item: Record<string, unknown>): PutBuilder;
210
- update(key: PrimaryKeyWithoutExpression, data?: Record<string, unknown>): UpdateBuilder;
211
- query(key: PrimaryKey): QueryBuilder;
212
- get(key: PrimaryKeyWithoutExpression, options?: {
213
- indexName?: string;
214
- }): Promise<Record<string, any> | undefined>;
215
- delete(key: PrimaryKeyWithoutExpression): Promise<unknown>;
216
- scan(filters?: FilterCondition[], options?: {
217
- limit?: number;
218
- pageKey?: Record<string, unknown>;
219
- indexName?: string;
220
- }): Promise<_aws_sdk_lib_dynamodb.ScanCommandOutput>;
221
- batchWrite(operations: BatchWriteOperation[]): Promise<unknown>;
222
- transactWrite(operations: Array<{
223
- put?: {
224
- item: Record<string, unknown>;
225
- condition?: {
226
- expression: string;
227
- names?: Record<string, string>;
228
- values?: Record<string, unknown>;
229
- };
230
- };
231
- delete?: {
232
- key: PrimaryKeyWithoutExpression;
233
- condition?: {
234
- expression: string;
235
- names?: Record<string, string>;
236
- values?: Record<string, unknown>;
237
- };
238
- };
239
- update?: {
240
- key: PrimaryKeyWithoutExpression;
241
- update: {
242
- expression: string;
243
- names?: Record<string, string>;
244
- values?: Record<string, unknown>;
245
- };
246
- condition?: {
247
- expression: string;
248
- names?: Record<string, string>;
249
- values?: Record<string, unknown>;
250
- };
251
- };
252
- }>): Promise<unknown>;
253
- private executeOperation;
254
- private buildKeyFromIndex;
255
- private validateKey;
256
- }
257
-
258
- type InferZodSchema<T extends z.ZodType> = z.infer<T>;
259
- declare abstract class BaseRepository<TSchema extends z.ZodType> {
260
- protected readonly table: Table;
261
- protected readonly schema: TSchema;
262
- constructor(table: Table, schema: TSchema);
263
- protected abstract createPrimaryKey(data: InferZodSchema<TSchema>): PrimaryKeyWithoutExpression;
264
- protected abstract getIndexKeys(): {
265
- pk: string;
266
- sk?: string;
267
- };
268
- /**
269
- * Default attribute applied to ALL records that get stored in DDB
270
- */
271
- protected abstract getType(): string;
272
- protected abstract getTypeAttributeName(): string;
273
- protected beforeInsert(data: InferZodSchema<TSchema>): InferZodSchema<TSchema>;
274
- protected beforeUpdate(data: InferZodSchema<TSchema>): InferZodSchema<TSchema>;
275
- create(data: InferZodSchema<TSchema>): Promise<InferZodSchema<TSchema>>;
276
- update(key: PrimaryKeyWithoutExpression, updates: Partial<InferZodSchema<TSchema>>): Promise<InferZodSchema<TSchema>>;
277
- delete(key: PrimaryKeyWithoutExpression): Promise<void>;
278
- findOne(key: PrimaryKeyWithoutExpression): Promise<InferZodSchema<TSchema> | null>;
279
- findOrFail(key: PrimaryKeyWithoutExpression): Promise<InferZodSchema<TSchema>>;
280
- protected query(key: PrimaryKeyWithoutExpression): QueryBuilder;
281
- }
282
-
283
- interface RetryStrategy {
284
- maxAttempts: number;
285
- baseDelay: number;
286
- /**
287
- * Check if the error should be retried
288
- * @param error The error that was thrown
289
- * @param attempt The amount of attempts this action has made to run the action
290
- * @returns Whether the action should be retried
291
- */
292
- shouldRetry: (error: unknown, attempt: number) => boolean;
293
- /**
294
- * Get the delay in milliseconds for the next retry attempt
295
- * @param attempt The amount of attempts this action has made to run the action
296
- * @returns The delay in milliseconds
297
- */
298
- getDelay: (attempt: number) => number;
299
- }
300
-
301
- declare class ExponentialBackoffStrategy implements RetryStrategy {
302
- maxAttempts: number;
303
- baseDelay: number;
304
- private maxDelay;
305
- private jitter;
306
- constructor(maxAttempts?: number, baseDelay?: number, maxDelay?: number, jitter?: boolean);
307
- shouldRetry(error: unknown, attempt: number): boolean;
308
- getDelay(attempt: number): number;
309
- }
310
-
311
- declare class DynamoError extends Error {
312
- readonly originalError: Error;
313
- readonly context?: Record<string, unknown> | undefined;
314
- constructor(message: string, originalError: Error, context?: Record<string, unknown> | undefined);
315
- }
316
- declare class ConditionalCheckFailedError extends DynamoError {
317
- constructor(message: string, originalError: Error, context?: Record<string, unknown>);
318
- }
319
- declare class ResourceNotFoundError extends DynamoError {
320
- constructor(message: string, originalError: Error, context?: Record<string, unknown>);
321
- }
322
-
323
- export { BaseRepository, ConditionalCheckFailedError, DynamoError, ExponentialBackoffStrategy, type FilterCondition, type FilterOperator, type PrimaryKey, ResourceNotFoundError, type RetryStrategy, Table, type TableIndexConfig };
package/dist/index.mjs DELETED
@@ -1,1051 +0,0 @@
1
- // src/builders/expression-builder.ts
2
- var ExpressionBuilder = class {
3
- nameCount = 0;
4
- valueCount = 0;
5
- generateAlias(type, prefix = type === "name" ? "n" : "v") {
6
- const count = type === "name" ? this.nameCount++ : this.valueCount++;
7
- const symbol = type === "name" ? "#" : ":";
8
- return `${symbol}${prefix}${count}`;
9
- }
10
- reset() {
11
- this.nameCount = 0;
12
- this.valueCount = 0;
13
- }
14
- createAttributePath(path) {
15
- const parts = path.split(".");
16
- const aliases = parts.map(() => this.generateAlias("name"));
17
- return {
18
- path: aliases.join("."),
19
- names: Object.fromEntries(parts.map((part, i) => [aliases[i], part]))
20
- };
21
- }
22
- addValue(attributes, value, prefix) {
23
- const alias = this.generateAlias("value", prefix);
24
- attributes.values[alias] = value;
25
- return alias;
26
- }
27
- buildComparison(path, operator, value, attributes, prefix) {
28
- const simpleOperators = ["=", "<>", "<", "<=", ">", ">="];
29
- if (simpleOperators.includes(operator)) {
30
- const valueAlias = this.addValue(attributes, value, prefix);
31
- return `${path} ${operator} ${valueAlias}`;
32
- }
33
- switch (operator) {
34
- case "attribute_exists":
35
- case "attribute_not_exists":
36
- return `${operator}(${path})`;
37
- case "begins_with":
38
- case "contains":
39
- case "attribute_type":
40
- return `${operator}(${path}, ${this.addValue(attributes, value, prefix)})`;
41
- case "not_contains":
42
- return `NOT contains(${path}, ${this.addValue(attributes, value, prefix)})`;
43
- case "size": {
44
- const { compare, value: sizeValue } = value;
45
- return `size(${path}) ${compare} ${this.addValue(attributes, sizeValue, prefix)}`;
46
- }
47
- case "BETWEEN": {
48
- const valueAlias = this.addValue(attributes, value, prefix);
49
- return `${path} BETWEEN ${valueAlias}[0] AND ${valueAlias}[1]`;
50
- }
51
- case "IN":
52
- return `${path} IN (${this.addValue(attributes, value, prefix)})`;
53
- default:
54
- throw new Error(`Unsupported operator: ${operator}`);
55
- }
56
- }
57
- createExpression(conditions) {
58
- this.reset();
59
- const attributes = { names: {}, values: {} };
60
- const expressions = conditions.map(({ field, operator, value }) => {
61
- const { path, names } = this.createAttributePath(field);
62
- Object.assign(attributes.names, names);
63
- return this.buildComparison(path, operator, value, attributes);
64
- });
65
- return {
66
- expression: expressions.length ? expressions.join(" AND ") : void 0,
67
- attributes: this.formatAttributes(attributes)
68
- };
69
- }
70
- formatAttributes({
71
- names,
72
- values
73
- }) {
74
- return {
75
- ...Object.keys(names).length && { names },
76
- ...Object.keys(values).length && { values }
77
- };
78
- }
79
- buildKeyCondition(key, indexConfig) {
80
- this.reset();
81
- const attributes = { names: {}, values: {} };
82
- const conditions = [];
83
- const pkName = this.generateAlias("name", "pk");
84
- attributes.names[pkName] = indexConfig.pkName;
85
- conditions.push(`${pkName} = ${this.addValue(attributes, key.pk, "pk")}`);
86
- if (key.sk && indexConfig.skName) {
87
- const skName = this.generateAlias("name", "sk");
88
- attributes.names[skName] = indexConfig.skName;
89
- if (typeof key.sk === "string") {
90
- conditions.push(
91
- `${skName} = ${this.addValue(attributes, key.sk, "sk")}`
92
- );
93
- } else {
94
- conditions.push(
95
- this.buildComparison(
96
- skName,
97
- key.sk.operator,
98
- key.sk.value,
99
- attributes
100
- )
101
- );
102
- }
103
- }
104
- return {
105
- expression: conditions.join(" AND "),
106
- attributes: this.formatAttributes(attributes)
107
- };
108
- }
109
- buildUpdateExpression(updates) {
110
- this.reset();
111
- const attributes = { names: {}, values: {} };
112
- const operations = { sets: [], removes: [] };
113
- for (const [key, value] of Object.entries(updates)) {
114
- if (key === "") {
115
- throw new Error("Empty key provided");
116
- }
117
- const nameAlias = this.generateAlias("name", "u");
118
- attributes.names[nameAlias] = key;
119
- if (value == null) {
120
- operations.removes.push(nameAlias);
121
- } else {
122
- const valueAlias = this.addValue(attributes, value, "u");
123
- operations.sets.push(`${nameAlias} = ${valueAlias}`);
124
- }
125
- }
126
- const expression = [
127
- operations.sets.length && `SET ${operations.sets.join(", ")}`,
128
- operations.removes.length && `REMOVE ${operations.removes.join(", ")}`
129
- ].filter(Boolean).join(" ");
130
- return {
131
- expression,
132
- attributes: this.formatAttributes(attributes)
133
- };
134
- }
135
- };
136
-
137
- // src/builders/operation-builder.ts
138
- var OperationBuilder = class {
139
- constructor(expressionBuilder) {
140
- this.expressionBuilder = expressionBuilder;
141
- }
142
- conditions = [];
143
- where(field, operator, value) {
144
- this.conditions.push({ field, operator, value });
145
- return this;
146
- }
147
- whereExists(field) {
148
- this.conditions.push({ field, operator: "attribute_exists" });
149
- return this;
150
- }
151
- whereNotExists(field) {
152
- this.conditions.push({ field, operator: "attribute_not_exists" });
153
- return this;
154
- }
155
- whereEquals(field, value) {
156
- return this.where(field, "=", value);
157
- }
158
- whereBetween(field, start, end) {
159
- return this.where(field, "BETWEEN", [start, end]);
160
- }
161
- whereIn(field, values) {
162
- return this.where(field, "IN", values);
163
- }
164
- buildConditionExpression() {
165
- return this.expressionBuilder.createExpression(this.conditions);
166
- }
167
- };
168
-
169
- // src/builders/put-builder.ts
170
- var PutBuilder = class extends OperationBuilder {
171
- constructor(item, expressionBuilder, onBuild) {
172
- super(expressionBuilder);
173
- this.item = item;
174
- this.onBuild = onBuild;
175
- }
176
- build() {
177
- const { expression, attributes } = this.buildConditionExpression();
178
- return {
179
- type: "put",
180
- item: this.item,
181
- condition: expression ? {
182
- expression,
183
- names: attributes.names,
184
- values: attributes.values
185
- } : void 0
186
- };
187
- }
188
- async execute() {
189
- return this.onBuild(this.build());
190
- }
191
- };
192
-
193
- // src/builders/query-builder.ts
194
- var QueryBuilder = class extends OperationBuilder {
195
- constructor(key, indexConfig, expressionBuilder, onBuild) {
196
- super(expressionBuilder);
197
- this.key = key;
198
- this.indexConfig = indexConfig;
199
- this.onBuild = onBuild;
200
- }
201
- limitValue;
202
- indexNameValue;
203
- limit(value) {
204
- this.limitValue = value;
205
- return this;
206
- }
207
- useIndex(indexName) {
208
- this.indexNameValue = indexName;
209
- return this;
210
- }
211
- build() {
212
- const filter = this.buildConditionExpression();
213
- const keyCondition = this.expressionBuilder.buildKeyCondition(
214
- this.key,
215
- this.indexConfig
216
- );
217
- return {
218
- type: "query",
219
- keyCondition: {
220
- expression: keyCondition.expression,
221
- names: keyCondition.attributes.names,
222
- values: keyCondition.attributes.values
223
- },
224
- filter: filter.expression ? {
225
- expression: filter.expression,
226
- names: filter.attributes.names,
227
- values: filter.attributes.values
228
- } : void 0,
229
- limit: this.limitValue,
230
- indexName: this.indexNameValue
231
- };
232
- }
233
- async execute() {
234
- return this.onBuild(this.build());
235
- }
236
- };
237
-
238
- // src/builders/update-builder.ts
239
- var UpdateBuilder = class extends OperationBuilder {
240
- constructor(key, expressionBuilder, onBuild) {
241
- super(expressionBuilder);
242
- this.key = key;
243
- this.onBuild = onBuild;
244
- }
245
- updates = {};
246
- set(field, value) {
247
- this.updates[field] = value;
248
- return this;
249
- }
250
- setMany(attribtues) {
251
- this.updates = { ...this.updates, ...attribtues };
252
- return this;
253
- }
254
- remove(...fields) {
255
- for (const field of fields) {
256
- this.updates[field] = null;
257
- }
258
- return this;
259
- }
260
- increment(field, by = 1) {
261
- this.updates[field] = { $add: by };
262
- return this;
263
- }
264
- build() {
265
- const condition = this.buildConditionExpression();
266
- const update = this.expressionBuilder.buildUpdateExpression(this.updates);
267
- return {
268
- type: "update",
269
- key: this.key,
270
- update: {
271
- expression: update.expression,
272
- names: update.attributes.names,
273
- values: update.attributes.values
274
- },
275
- condition: condition.expression ? {
276
- expression: condition.expression,
277
- names: condition.attributes.names,
278
- values: condition.attributes.values
279
- } : void 0
280
- };
281
- }
282
- async execute() {
283
- return this.onBuild(this.build());
284
- }
285
- };
286
-
287
- // src/errors/dynamo-error.ts
288
- var DynamoError = class _DynamoError extends Error {
289
- constructor(message, originalError, context) {
290
- super(message);
291
- this.originalError = originalError;
292
- this.context = context;
293
- this.name = "DynamoError";
294
- if (Error.captureStackTrace) {
295
- Error.captureStackTrace(this, _DynamoError);
296
- }
297
- }
298
- };
299
- var ConditionalCheckFailedError = class extends DynamoError {
300
- constructor(message, originalError, context) {
301
- super(message, originalError, context);
302
- this.name = "ConditionalCheckFailedError";
303
- }
304
- };
305
- var ResourceNotFoundError = class extends DynamoError {
306
- constructor(message, originalError, context) {
307
- super(message, originalError, context);
308
- this.name = "ResourceNotFoundError";
309
- }
310
- };
311
-
312
- // src/errors/error-handler.ts
313
- function translateExpression(expression, attributes) {
314
- if (!expression || !attributes) return expression || "";
315
- let translated = expression;
316
- if (attributes.names) {
317
- for (const [alias, name] of Object.entries(attributes.names)) {
318
- translated = translated.replace(new RegExp(alias, "g"), name);
319
- }
320
- }
321
- if (attributes.values) {
322
- for (const [alias, value] of Object.entries(attributes.values)) {
323
- translated = translated.replace(
324
- new RegExp(alias, "g"),
325
- typeof value === "string" ? `"${value}"` : String(value)
326
- );
327
- }
328
- }
329
- return translated;
330
- }
331
- function buildErrorMessage(context, error) {
332
- const parts = [`DynamoDB ${context.operation} operation failed`];
333
- if (context.tableName) {
334
- parts.push(`
335
- Table: ${context.tableName}`);
336
- }
337
- if (context.key) {
338
- parts.push(`
339
- Key: ${JSON.stringify(context.key, null, 2)}`);
340
- }
341
- if (context.expression) {
342
- const { condition, update, filter, keyCondition } = context.expression;
343
- if (condition) {
344
- parts.push(
345
- `
346
- Condition: ${translateExpression(condition, context.attributes)}`
347
- );
348
- }
349
- if (update) {
350
- parts.push(
351
- `
352
- Update: ${translateExpression(update, context.attributes)}`
353
- );
354
- }
355
- if (filter) {
356
- parts.push(
357
- `
358
- Filter: ${translateExpression(filter, context.attributes)}`
359
- );
360
- }
361
- if (keyCondition) {
362
- parts.push(
363
- `
364
- Key Condition: ${translateExpression(keyCondition, context.attributes)}`
365
- );
366
- }
367
- }
368
- parts.push(`
369
- Original Error: ${error.message}`);
370
- return parts.join("");
371
- }
372
- function handleDynamoError(error, context) {
373
- if (!(error instanceof Error)) {
374
- throw error;
375
- }
376
- const errorMessage = buildErrorMessage(context, error);
377
- switch (error.name) {
378
- case "ConditionalCheckFailedException":
379
- throw new ConditionalCheckFailedError(errorMessage, error, context);
380
- case "ResourceNotFoundException":
381
- throw new ResourceNotFoundError(errorMessage, error, context);
382
- default:
383
- throw new DynamoError(errorMessage, error, context);
384
- }
385
- }
386
-
387
- // src/retry/retry-strategy.ts
388
- var RETRYABLE_ERRORS = /* @__PURE__ */ new Set([
389
- "ProvisionedThroughputExceededException",
390
- "ThrottlingException",
391
- "RequestLimitExceeded",
392
- "InternalServerError",
393
- "ServiceUnavailable"
394
- ]);
395
- var isRetryableError = (error) => {
396
- if (!error || typeof error !== "object") return false;
397
- return "name" in error && RETRYABLE_ERRORS.has(error.name);
398
- };
399
-
400
- // src/retry/exponential-backoff-strategy.ts
401
- var ExponentialBackoffStrategy = class {
402
- constructor(maxAttempts = 3, baseDelay = 100, maxDelay = 5e3, jitter = true) {
403
- this.maxAttempts = maxAttempts;
404
- this.baseDelay = baseDelay;
405
- this.maxDelay = maxDelay;
406
- this.jitter = jitter;
407
- }
408
- shouldRetry(error, attempt) {
409
- return attempt < this.maxAttempts && isRetryableError(error);
410
- }
411
- getDelay(attempt) {
412
- const delay = Math.min(this.baseDelay * attempt ** 2, this.maxDelay);
413
- if (!this.jitter) return delay;
414
- return delay * (0.5 + Math.random());
415
- }
416
- };
417
-
418
- // src/dynamo/dynamo-converter.ts
419
- var DynamoConverter = class {
420
- constructor(tableName) {
421
- this.tableName = tableName;
422
- }
423
- /**
424
- * Converts our expression format to DynamoDB expression format
425
- */
426
- convertExpression(expr) {
427
- if (!expr) return {};
428
- return {
429
- ...expr.expression && { Expression: expr.expression },
430
- ...expr.names && { ExpressionAttributeNames: expr.names },
431
- ...expr.values && { ExpressionAttributeValues: expr.values }
432
- };
433
- }
434
- /**
435
- * Convert our format to DynamoDB put command input
436
- */
437
- toPutCommand(options) {
438
- return {
439
- TableName: this.tableName,
440
- Item: options.item,
441
- ...options.condition && {
442
- ConditionExpression: options.condition.expression,
443
- ExpressionAttributeNames: options.condition.names,
444
- ExpressionAttributeValues: options.condition.values
445
- }
446
- };
447
- }
448
- /**
449
- * Convert our format to DynamoDB get command input
450
- */
451
- toGetCommand(options) {
452
- return {
453
- TableName: this.tableName,
454
- Key: options.key,
455
- ...options.indexName && { IndexName: options.indexName }
456
- };
457
- }
458
- /**
459
- * Convert our format to DynamoDB update command input
460
- */
461
- toUpdateCommand(options) {
462
- return {
463
- TableName: this.tableName,
464
- Key: options.key,
465
- UpdateExpression: options.update.expression,
466
- ExpressionAttributeNames: {
467
- ...options.update.names,
468
- ...options.condition?.names
469
- },
470
- ExpressionAttributeValues: {
471
- ...options.update.values,
472
- ...options.condition?.values
473
- },
474
- ...options.condition && {
475
- ConditionExpression: options.condition.expression
476
- },
477
- ...options.returnValues && {
478
- ReturnValues: options.returnValues
479
- }
480
- };
481
- }
482
- /**
483
- * Convert our format to DynamoDB delete command input
484
- */
485
- toDeleteCommand(options) {
486
- return {
487
- TableName: this.tableName,
488
- Key: options.key,
489
- ...options.condition && {
490
- ConditionExpression: options.condition.expression,
491
- ExpressionAttributeNames: options.condition.names,
492
- ExpressionAttributeValues: options.condition.values
493
- }
494
- };
495
- }
496
- /**
497
- * Convert our format to DynamoDB query command input
498
- */
499
- toQueryCommand(options) {
500
- return {
501
- TableName: this.tableName,
502
- ...options.keyCondition && {
503
- KeyConditionExpression: options.keyCondition.expression,
504
- ExpressionAttributeNames: {
505
- ...options.keyCondition.names,
506
- ...options.filter?.names
507
- },
508
- ExpressionAttributeValues: {
509
- ...options.keyCondition.values,
510
- ...options.filter?.values
511
- }
512
- },
513
- ...options.filter && {
514
- FilterExpression: options.filter.expression
515
- },
516
- IndexName: options.indexName,
517
- Limit: options.limit,
518
- ExclusiveStartKey: options.pageKey,
519
- ConsistentRead: options.consistentRead
520
- };
521
- }
522
- /**
523
- * Convert our format to DynamoDB scan command input
524
- */
525
- toScanCommand(options) {
526
- return {
527
- TableName: this.tableName,
528
- ...options.filter && {
529
- FilterExpression: options.filter.expression,
530
- ExpressionAttributeNames: options.filter.names,
531
- ExpressionAttributeValues: options.filter.values
532
- },
533
- IndexName: options.indexName,
534
- Limit: options.limit,
535
- ExclusiveStartKey: options.pageKey
536
- };
537
- }
538
- /**
539
- * Convert our format to DynamoDB batch write command input
540
- */
541
- toBatchWriteCommand(items) {
542
- const requests = items.map((item) => {
543
- if (item.put) {
544
- return {
545
- PutRequest: {
546
- Item: item.put
547
- }
548
- };
549
- }
550
- if (item.delete) {
551
- return {
552
- DeleteRequest: {
553
- Key: item.delete
554
- }
555
- };
556
- }
557
- throw new Error("Invalid batch write item");
558
- });
559
- return {
560
- RequestItems: {
561
- [this.tableName]: requests
562
- }
563
- };
564
- }
565
- /**
566
- * Convert our format to DynamoDB transact write command input
567
- */
568
- toTransactWriteCommand(items) {
569
- return {
570
- TransactItems: items.map((item) => {
571
- if (item.put) {
572
- return {
573
- Put: {
574
- TableName: this.tableName,
575
- Item: item.put.item,
576
- ...item.put.condition && {
577
- ConditionExpression: item.put.condition.expression,
578
- ExpressionAttributeNames: item.put.condition.names,
579
- ExpressionAttributeValues: item.put.condition.values
580
- }
581
- }
582
- };
583
- }
584
- if (item.delete) {
585
- return {
586
- Delete: {
587
- TableName: this.tableName,
588
- Key: item.delete.key,
589
- ...item.delete.condition && {
590
- ConditionExpression: item.delete.condition.expression,
591
- ExpressionAttributeNames: item.delete.condition.names,
592
- ExpressionAttributeValues: item.delete.condition.values
593
- }
594
- }
595
- };
596
- }
597
- if (item.update) {
598
- return {
599
- Update: {
600
- TableName: this.tableName,
601
- Key: item.update.key,
602
- UpdateExpression: item.update.update.expression,
603
- ...item.update.condition && {
604
- ConditionExpression: item.update.condition.expression,
605
- ExpressionAttributeNames: {
606
- ...item.update.update.names,
607
- ...item.update.condition.names
608
- },
609
- ExpressionAttributeValues: {
610
- ...item.update.update.values,
611
- ...item.update.condition.values
612
- }
613
- }
614
- }
615
- };
616
- }
617
- throw new Error("Invalid transaction item");
618
- })
619
- };
620
- }
621
- /**
622
- * Convert DynamoDB batch write response to our format
623
- */
624
- fromBatchWriteResponse(response) {
625
- return response.map((item) => {
626
- if ("PutRequest" in item) {
627
- return {
628
- put: item.PutRequest.Item
629
- };
630
- }
631
- if ("DeleteRequest" in item) {
632
- return {
633
- delete: item.DeleteRequest.Key
634
- };
635
- }
636
- throw new Error("Invalid batch write response item");
637
- });
638
- }
639
- };
640
-
641
- // src/dynamo/dynamo-service.ts
642
- var BATCH_WRITE_LIMIT = 25;
643
- var TRANSACTION_LIMIT = 100;
644
- var DynamoService = class {
645
- constructor(client, tableName) {
646
- this.client = client;
647
- this.tableName = tableName;
648
- this.converter = new DynamoConverter(tableName);
649
- }
650
- converter;
651
- async put(options) {
652
- try {
653
- const params = this.converter.toPutCommand(options);
654
- return await this.withRetry(() => this.client.put(params));
655
- } catch (error) {
656
- handleDynamoError(error, {
657
- operation: "PUT",
658
- tableName: this.tableName,
659
- key: options.item,
660
- expression: {
661
- condition: options.condition?.expression
662
- }
663
- });
664
- }
665
- }
666
- async update(options) {
667
- try {
668
- const params = this.converter.toUpdateCommand(options);
669
- return await this.withRetry(() => this.client.update(params));
670
- } catch (error) {
671
- handleDynamoError(error, {
672
- operation: "UPDATE",
673
- tableName: this.tableName,
674
- key: options.key,
675
- expression: {
676
- update: options.update.expression,
677
- condition: options.condition?.expression
678
- }
679
- });
680
- }
681
- }
682
- async delete(options) {
683
- const params = this.converter.toDeleteCommand(options);
684
- try {
685
- return await this.withRetry(() => this.client.delete(params));
686
- } catch (error) {
687
- handleDynamoError(error, {
688
- operation: "DELETE",
689
- tableName: this.tableName,
690
- key: options.key,
691
- expression: {
692
- condition: params.ConditionExpression
693
- }
694
- });
695
- }
696
- }
697
- async get(key, options) {
698
- try {
699
- const params = this.converter.toGetCommand({ key, ...options });
700
- return await this.withRetry(() => this.client.get(params));
701
- } catch (error) {
702
- handleDynamoError(error, {
703
- operation: "GET",
704
- tableName: this.tableName,
705
- key
706
- });
707
- }
708
- }
709
- async query(options) {
710
- try {
711
- if (options.autoPaginate) {
712
- return await this.executeWithAutoPagination(options);
713
- }
714
- const params = this.converter.toQueryCommand(options);
715
- return await this.withRetry(() => this.client.query(params));
716
- } catch (error) {
717
- handleDynamoError(error, {
718
- operation: "QUERY",
719
- tableName: this.tableName,
720
- expression: {
721
- keyCondition: options.keyCondition?.expression,
722
- filter: options.filter?.expression
723
- }
724
- });
725
- }
726
- }
727
- async scan(options) {
728
- try {
729
- const params = this.converter.toScanCommand(options);
730
- return await this.withRetry(() => this.client.scan(params));
731
- } catch (error) {
732
- handleDynamoError(error, {
733
- operation: "SCAN",
734
- tableName: this.tableName,
735
- expression: {
736
- filter: options.filter?.expression
737
- }
738
- });
739
- }
740
- }
741
- async batchWrite(items) {
742
- try {
743
- const chunks = this.chunkArray(items, BATCH_WRITE_LIMIT);
744
- return await Promise.all(
745
- chunks.map((chunk) => this.processBatchWrite(chunk))
746
- );
747
- } catch (error) {
748
- handleDynamoError(error, {
749
- operation: "BATCH_WRITE",
750
- tableName: this.tableName
751
- });
752
- }
753
- }
754
- async transactWrite(items) {
755
- if (items.length > TRANSACTION_LIMIT) {
756
- throw new Error(
757
- `Transaction limit exceeded. Maximum is ${TRANSACTION_LIMIT} items, got ${items.length}`
758
- );
759
- }
760
- try {
761
- const params = this.converter.toTransactWriteCommand(items);
762
- return await this.withRetry(() => this.client.transactWrite(params));
763
- } catch (error) {
764
- handleDynamoError(error, {
765
- operation: "TRANSACT_WRITE",
766
- tableName: this.tableName
767
- });
768
- }
769
- }
770
- async executeWithAutoPagination(options) {
771
- const allItems = [];
772
- let lastEvaluatedKey;
773
- do {
774
- const result = await this.query({
775
- ...options,
776
- pageKey: lastEvaluatedKey,
777
- autoPaginate: false
778
- });
779
- if (result.Items) {
780
- allItems.push(...result.Items);
781
- }
782
- lastEvaluatedKey = result.LastEvaluatedKey;
783
- } while (lastEvaluatedKey);
784
- return {
785
- Items: allItems,
786
- Count: allItems.length,
787
- ScannedCount: allItems.length,
788
- LastEvaluatedKey: void 0
789
- };
790
- }
791
- async processBatchWrite(items) {
792
- const processUnprocessedItems = async (unprocessedItems2) => {
793
- const params2 = this.converter.toBatchWriteCommand(unprocessedItems2);
794
- const result = await this.client.batchWrite(params2);
795
- if (result.UnprocessedItems?.[this.tableName]?.length) {
796
- const remainingItems = this.converter.fromBatchWriteResponse(
797
- result.UnprocessedItems[this.tableName]
798
- );
799
- throw {
800
- name: "UnprocessedItemsError",
801
- unprocessedItems: remainingItems
802
- };
803
- }
804
- return result;
805
- };
806
- const params = this.converter.toBatchWriteCommand(items);
807
- const initialResult = await this.client.batchWrite(params);
808
- if (!initialResult.UnprocessedItems?.[this.tableName]?.length) {
809
- return initialResult;
810
- }
811
- const unprocessedItems = this.converter.fromBatchWriteResponse(
812
- initialResult.UnprocessedItems[this.tableName]
813
- );
814
- return this.withRetry(() => processUnprocessedItems(unprocessedItems));
815
- }
816
- async withRetry(operation, strategy = new ExponentialBackoffStrategy()) {
817
- let attempt = 0;
818
- while (true) {
819
- try {
820
- return await operation();
821
- } catch (error) {
822
- if (!strategy.shouldRetry(error, attempt)) {
823
- throw error;
824
- }
825
- await new Promise(
826
- (resolve) => setTimeout(resolve, strategy.getDelay(attempt))
827
- );
828
- attempt++;
829
- }
830
- }
831
- }
832
- chunkArray(array, size) {
833
- return Array.from(
834
- { length: Math.ceil(array.length / size) },
835
- (_, index) => array.slice(index * size, (index + 1) * size)
836
- );
837
- }
838
- };
839
-
840
- // src/table.ts
841
- var Table = class {
842
- dynamoService;
843
- expressionBuilder;
844
- indexes;
845
- constructor({
846
- client,
847
- tableName,
848
- tableIndexes,
849
- expressionBuilder
850
- }) {
851
- this.dynamoService = new DynamoService(client, tableName);
852
- this.expressionBuilder = expressionBuilder ?? new ExpressionBuilder();
853
- this.indexes = tableIndexes;
854
- }
855
- getIndexConfig(indexName) {
856
- if (!indexName) {
857
- return this.indexes.primary;
858
- }
859
- if (this.indexes[indexName]) {
860
- return this.indexes[indexName];
861
- }
862
- throw new Error(`Index ${indexName} does not exist`);
863
- }
864
- put(item) {
865
- return new PutBuilder(
866
- item,
867
- this.expressionBuilder,
868
- (operation) => this.executeOperation(operation)
869
- );
870
- }
871
- update(key, data) {
872
- const builder = new UpdateBuilder(
873
- key,
874
- this.expressionBuilder,
875
- (operation) => this.executeOperation(operation)
876
- );
877
- if (data) {
878
- builder.setMany(data);
879
- }
880
- return builder;
881
- }
882
- query(key) {
883
- return new QueryBuilder(
884
- key,
885
- this.getIndexConfig(),
886
- this.expressionBuilder,
887
- (operation) => this.executeOperation(operation)
888
- );
889
- }
890
- async get(key, options) {
891
- const indexConfig = this.getIndexConfig(options?.indexName);
892
- const keyObject = this.buildKeyFromIndex(key, indexConfig);
893
- const result = await this.dynamoService.get(keyObject, options);
894
- return result.Item;
895
- }
896
- async delete(key) {
897
- const operation = {
898
- type: "delete",
899
- key
900
- };
901
- return this.executeOperation(operation);
902
- }
903
- async scan(filters, options) {
904
- let filter = void 0;
905
- if (filters?.length) {
906
- const filterResult = this.expressionBuilder.createExpression(filters);
907
- filter = {
908
- expression: filterResult.expression,
909
- names: filterResult.attributes.names,
910
- values: filterResult.attributes.values
911
- };
912
- }
913
- return this.dynamoService.scan({
914
- filter,
915
- limit: options?.limit,
916
- pageKey: options?.pageKey,
917
- indexName: options?.indexName
918
- });
919
- }
920
- async batchWrite(operations) {
921
- const batchOperation = {
922
- type: "batchWrite",
923
- operations: operations.map((op) => {
924
- if (op.type === "put") {
925
- return { put: op.item };
926
- }
927
- return { delete: op.key };
928
- })
929
- };
930
- return this.executeOperation(batchOperation);
931
- }
932
- async transactWrite(operations) {
933
- const transactOperation = {
934
- type: "transactWrite",
935
- operations
936
- };
937
- return this.executeOperation(transactOperation);
938
- }
939
- async executeOperation(operation) {
940
- switch (operation.type) {
941
- case "put":
942
- return this.dynamoService.put({
943
- item: operation.item,
944
- condition: operation.condition
945
- });
946
- case "update":
947
- return this.dynamoService.update({
948
- key: operation.key,
949
- update: operation.update,
950
- condition: operation.condition,
951
- returnValues: "ALL_NEW"
952
- });
953
- case "query":
954
- return this.dynamoService.query({
955
- keyCondition: operation.keyCondition,
956
- filter: operation.filter,
957
- limit: operation.limit,
958
- indexName: operation.indexName
959
- });
960
- case "delete":
961
- return this.dynamoService.delete({
962
- key: operation.key
963
- });
964
- case "batchWrite":
965
- return this.dynamoService.batchWrite(operation.operations);
966
- case "transactWrite":
967
- return this.dynamoService.transactWrite(operation.operations);
968
- default:
969
- throw new Error("Unknown operation type");
970
- }
971
- }
972
- buildKeyFromIndex(key, indexConfig) {
973
- this.validateKey(key, indexConfig);
974
- const keyObject = {
975
- [indexConfig.pkName]: key.pk
976
- };
977
- if (indexConfig.skName && key.sk) {
978
- keyObject[indexConfig.skName] = key.sk;
979
- }
980
- return keyObject;
981
- }
982
- validateKey(key, indexConfig) {
983
- if (!key.pk) {
984
- throw new Error("Partition key is required");
985
- }
986
- if (key.sk && !indexConfig.skName) {
987
- throw new Error("Sort key provided but index does not support sort keys");
988
- }
989
- if (!key.sk && indexConfig.skName) {
990
- throw new Error("Index requires a sort key but none was provided");
991
- }
992
- }
993
- };
994
-
995
- // src/repository/base-repository.ts
996
- var BaseRepository = class {
997
- constructor(table, schema) {
998
- this.table = table;
999
- this.schema = schema;
1000
- }
1001
- beforeInsert(data) {
1002
- return data;
1003
- }
1004
- beforeUpdate(data) {
1005
- return data;
1006
- }
1007
- async create(data) {
1008
- const parsed = this.schema.parse(data);
1009
- const key = this.createPrimaryKey(parsed);
1010
- const item = {
1011
- ...parsed,
1012
- ...key
1013
- };
1014
- const indexConfig = this.table.getIndexConfig();
1015
- await this.table.put(item).whereNotExists(indexConfig.pkName).execute();
1016
- return parsed;
1017
- }
1018
- async update(key, updates) {
1019
- const parsed = this.schema.parse(updates);
1020
- const result = await this.table.update(key).setMany(parsed).execute();
1021
- return result.Attributes ? this.schema.parse(result.Attributes) : null;
1022
- }
1023
- async delete(key) {
1024
- await this.table.delete(key);
1025
- }
1026
- async findOne(key) {
1027
- const item = await this.table.query(key).where(this.getTypeAttributeName(), "=", this.getType()).execute();
1028
- if (!item) {
1029
- return null;
1030
- }
1031
- return this.schema.parse(item);
1032
- }
1033
- async findOrFail(key) {
1034
- const result = await this.findOne(key);
1035
- if (!result) {
1036
- throw new Error("Item not found");
1037
- }
1038
- return this.schema.parse(result);
1039
- }
1040
- query(key) {
1041
- return this.table.query(key).where(this.getTypeAttributeName(), "=", this.getType());
1042
- }
1043
- };
1044
- export {
1045
- BaseRepository,
1046
- ConditionalCheckFailedError,
1047
- DynamoError,
1048
- ExponentialBackoffStrategy,
1049
- ResourceNotFoundError,
1050
- Table
1051
- };