@opble/repository-dynamodb 1.0.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.cjs ADDED
@@ -0,0 +1,324 @@
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
+ AbstractDynamoDBRepository: () => AbstractDynamoDBRepository,
24
+ QueryAllSchema: () => QueryAllSchema,
25
+ QueryWithPaginationSchema: () => QueryWithPaginationSchema,
26
+ QueryWithSortingSchema: () => QueryWithSortingSchema,
27
+ ScanFilterSchema: () => ScanFilterSchema,
28
+ TableKeychema: () => TableKeychema,
29
+ TableSpecSchema: () => TableSpecSchema
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/repository.ts
34
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
35
+ var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
36
+ var import_debug = require("@opble/debug");
37
+ var import_entity = require("@opble/entity");
38
+ var debug = (0, import_debug.createDebug)("opble:repository-dynamodb");
39
+ var SORT_ASCENDING = "asc";
40
+ var BATCH_SIZE = 50;
41
+ function now() {
42
+ return Math.floor(Date.now() / 1e3);
43
+ }
44
+ var AbstractDynamoDBRepository = class {
45
+ constructor(table, factory) {
46
+ this.table = table;
47
+ this.factory = factory;
48
+ this.client = import_lib_dynamodb.DynamoDBDocumentClient.from(new import_client_dynamodb.DynamoDBClient({}));
49
+ }
50
+ client;
51
+ getKey(id) {
52
+ return {
53
+ [this.table.keys.hashKey]: id.hashKey,
54
+ ...this.table.keys.sortKey ? { [this.table.keys.sortKey]: id.sortKey } : {}
55
+ };
56
+ }
57
+ async query(hashKey, query, pagination, sorting) {
58
+ let lastEvaluatedKey;
59
+ let currentPage = 1;
60
+ let resultItems = [];
61
+ const filterReturnedItemsExcludeSortKey = (items) => {
62
+ return query?.excludeSortKey && this.table.keys.sortKey ? items.filter(
63
+ (item) => item[this.table.keys.sortKey] !== query.excludeSortKey
64
+ ) : items;
65
+ };
66
+ do {
67
+ const command = new import_lib_dynamodb.QueryCommand({
68
+ TableName: this.table.name,
69
+ KeyConditionExpression: `#${this.table.keys.hashKey} = :hashKeyValue`,
70
+ ExpressionAttributeNames: {
71
+ [`#${this.table.keys.hashKey}`]: this.table.keys.hashKey
72
+ },
73
+ ExpressionAttributeValues: {
74
+ ":hashKeyValue": hashKey
75
+ },
76
+ ExclusiveStartKey: lastEvaluatedKey
77
+ });
78
+ if (pagination) {
79
+ command.input.Limit = pagination.limit;
80
+ }
81
+ if (sorting) {
82
+ command.input.ScanIndexForward = sorting.sortOrder === "asc";
83
+ }
84
+ if (query) {
85
+ if (query.index) {
86
+ command.input.IndexName = query.index.name;
87
+ command.input.KeyConditionExpression = `#${query.index.keys.hashKey} = :hashKeyValue`;
88
+ command.input.ExpressionAttributeNames = {
89
+ [`#${query.index.keys.hashKey}`]: query.index.keys.hashKey
90
+ };
91
+ }
92
+ const sortKey = query.index?.keys.sortKey ?? this.table.keys.sortKey;
93
+ if (query.sortKeyBeginsWith && sortKey) {
94
+ command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND begins_with(#${sortKey}, :sortKeyBeginsWith)`;
95
+ command.input.ExpressionAttributeNames ??= {};
96
+ command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
97
+ command.input.ExpressionAttributeValues ??= {};
98
+ command.input.ExpressionAttributeValues[":sortKeyBeginsWith"] = query.sortKeyBeginsWith;
99
+ } else if (query.sortKey && sortKey) {
100
+ command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} = :sortKeyValue`;
101
+ command.input.ExpressionAttributeNames ??= {};
102
+ command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
103
+ command.input.ExpressionAttributeValues ??= {};
104
+ command.input.ExpressionAttributeValues[":sortKeyValue"] = query.sortKey;
105
+ } else if (query.sortKeyBetween && sortKey) {
106
+ command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} BETWEEN :sortKeyStart AND :sortKeyEnd`;
107
+ command.input.ExpressionAttributeNames ??= {};
108
+ command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
109
+ command.input.ExpressionAttributeValues ??= {};
110
+ command.input.ExpressionAttributeValues[":sortKeyStart"] = query.sortKeyBetween.start;
111
+ command.input.ExpressionAttributeValues[":sortKeyEnd"] = query.sortKeyBetween.end;
112
+ }
113
+ }
114
+ debug("command#input", command.input);
115
+ const result = await this.client.send(command);
116
+ lastEvaluatedKey = result.LastEvaluatedKey;
117
+ if (!pagination) {
118
+ resultItems.push(
119
+ ...filterReturnedItemsExcludeSortKey(result.Items ?? [])
120
+ );
121
+ } else if (currentPage === pagination.page) {
122
+ resultItems = filterReturnedItemsExcludeSortKey(result.Items ?? []);
123
+ break;
124
+ }
125
+ currentPage++;
126
+ } while (lastEvaluatedKey);
127
+ const strict = !query || !query.index;
128
+ return resultItems.map((item) => this.factory.create(item, { strict }));
129
+ }
130
+ async queryInBulk(keys, query) {
131
+ const chunks = [];
132
+ for (let i = 0; i < keys.length; i += BATCH_SIZE) {
133
+ chunks.push(keys.slice(i, i + BATCH_SIZE));
134
+ }
135
+ const results = await Promise.all(
136
+ chunks.map(async (chunk) => {
137
+ const command = new import_lib_dynamodb.BatchGetCommand({
138
+ RequestItems: {
139
+ [this.table.name]: { Keys: chunk }
140
+ }
141
+ });
142
+ const response = await this.client.send(command);
143
+ return response.Responses?.[this.table.name] ?? [];
144
+ })
145
+ );
146
+ const mergedItems = results.flat();
147
+ const items = mergedItems.map((item) => this.factory.create(item));
148
+ if (query && query.sortBy) {
149
+ const sortOrder = query.sortOrder ?? SORT_ASCENDING;
150
+ items.sort((a, b) => {
151
+ if (a[query.sortBy] < b[query.sortBy])
152
+ return sortOrder === SORT_ASCENDING ? -1 : 1;
153
+ if (a[query.sortBy] > b[query.sortBy])
154
+ return sortOrder === SORT_ASCENDING ? 1 : -1;
155
+ return 0;
156
+ });
157
+ }
158
+ return items;
159
+ }
160
+ async scan(pagination, filter) {
161
+ let lastEvaluatedKey;
162
+ let currentPage = 1;
163
+ let resultItems = [];
164
+ do {
165
+ const command = new import_lib_dynamodb.ScanCommand({
166
+ TableName: this.table.name,
167
+ ExclusiveStartKey: lastEvaluatedKey
168
+ });
169
+ if (pagination) {
170
+ command.input.Limit = pagination.limit;
171
+ }
172
+ if (filter) {
173
+ command.input.FilterExpression = filter.filterExpression;
174
+ command.input.ExpressionAttributeNames = filter.expressionAttributeNames;
175
+ command.input.ExpressionAttributeValues = filter.expressionAttributeValues;
176
+ }
177
+ const result = await this.client.send(command);
178
+ lastEvaluatedKey = result.LastEvaluatedKey;
179
+ if (!pagination) {
180
+ resultItems.push(...result.Items ?? []);
181
+ } else if (currentPage === pagination.page) {
182
+ resultItems = result.Items ?? [];
183
+ break;
184
+ }
185
+ currentPage++;
186
+ } while (lastEvaluatedKey);
187
+ return resultItems.map((item) => this.factory.create(item));
188
+ }
189
+ async deleteAll(hashKey) {
190
+ const items = await this.query(hashKey);
191
+ const keys = items.map((item) => {
192
+ if (this.table.keys.sortKey) {
193
+ return {
194
+ [this.table.keys.hashKey]: hashKey,
195
+ [this.table.keys.sortKey]: item[this.table.keys.sortKey]
196
+ };
197
+ } else {
198
+ return {
199
+ [this.table.keys.hashKey]: hashKey
200
+ };
201
+ }
202
+ });
203
+ await this.deleteInBulk(keys);
204
+ }
205
+ async deleteInBulk(keys) {
206
+ const batches = [];
207
+ while (keys.length > 0) {
208
+ batches.push(keys.splice(0, BATCH_SIZE));
209
+ }
210
+ for (const batch of batches) {
211
+ const deleteRequests = batch.map((key) => ({
212
+ DeleteRequest: { Key: key }
213
+ }));
214
+ const command = new import_lib_dynamodb.BatchWriteCommand({
215
+ RequestItems: {
216
+ [this.table.name]: deleteRequests
217
+ }
218
+ });
219
+ try {
220
+ const response = await this.client.send(command);
221
+ debug("[INFO] Batch delete response:", response);
222
+ if (response.UnprocessedItems && response.UnprocessedItems[this.table.name]) {
223
+ debug("[WARN] Unprocessed items found. Retrying...");
224
+ const unprocessedItems = response.UnprocessedItems[this.table.name];
225
+ if (unprocessedItems === void 0) {
226
+ continue;
227
+ }
228
+ const unprocessedKeys = unprocessedItems.map((item) => item.DeleteRequest?.Key).filter((key) => key != null);
229
+ await this.deleteInBulk(unprocessedKeys);
230
+ }
231
+ } catch (error) {
232
+ debug("[ERROR] Error deleting items in batch:", error);
233
+ }
234
+ }
235
+ }
236
+ async get(id) {
237
+ const command = new import_lib_dynamodb.GetCommand({
238
+ TableName: this.table.name,
239
+ Key: this.getKey(id)
240
+ });
241
+ const result = await this.client.send(command);
242
+ return result.Item ? this.factory.create(result.Item) : null;
243
+ }
244
+ /**
245
+ * Allow to manipulate the data before saving
246
+ * @param item
247
+ * @returns
248
+ */
249
+ beforeSave(item) {
250
+ if ((0, import_entity.isTimestamps)(item)) {
251
+ item.updatedAt = now();
252
+ } else if ((0, import_entity.isTimestamp)(item)) {
253
+ item.timestamp = now();
254
+ }
255
+ return item;
256
+ }
257
+ async save(item) {
258
+ item = this.beforeSave(item);
259
+ const command = new import_lib_dynamodb.PutCommand({
260
+ TableName: this.table.name,
261
+ Item: { ...item }
262
+ });
263
+ await this.client.send(command);
264
+ return item;
265
+ }
266
+ async delete(id) {
267
+ await this.client.send(
268
+ new import_lib_dynamodb.DeleteCommand({
269
+ TableName: this.table.name,
270
+ Key: this.getKey(id)
271
+ })
272
+ );
273
+ }
274
+ };
275
+
276
+ // src/types.ts
277
+ var import_zod = require("zod");
278
+ var TableKeychema = import_zod.z.object({
279
+ hashKey: import_zod.z.string(),
280
+ sortKey: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional()
281
+ });
282
+ var TableSpecSchema = import_zod.z.object({
283
+ name: import_zod.z.string(),
284
+ keys: import_zod.z.object({
285
+ hashKey: import_zod.z.string(),
286
+ sortKey: import_zod.z.string().optional()
287
+ })
288
+ });
289
+ var QueryWithPaginationSchema = import_zod.z.object({
290
+ page: import_zod.z.string().optional().transform((val) => val ? parseInt(val, 10) : 1).refine((val) => val >= 1, { message: "Page must be at least 1" }),
291
+ limit: import_zod.z.string().optional().transform((val) => val ? parseInt(val, 10) : 25).refine((val) => val >= 5 && val <= 100, {
292
+ message: "Limit must be between 5 and 100"
293
+ })
294
+ });
295
+ var QueryWithSortingSchema = import_zod.z.object({
296
+ sortBy: import_zod.z.string().optional(),
297
+ sortOrder: import_zod.z.enum(["asc", "desc"]).optional().transform((val) => val ?? "asc")
298
+ });
299
+ var QueryAllSchema = import_zod.z.object({
300
+ excludeSortKey: import_zod.z.string().optional(),
301
+ sortKeyBetween: import_zod.z.object({
302
+ start: import_zod.z.string(),
303
+ end: import_zod.z.string()
304
+ }).optional(),
305
+ sortKeyBeginsWith: import_zod.z.string().optional(),
306
+ sortKey: import_zod.z.any().optional(),
307
+ index: TableSpecSchema.optional()
308
+ });
309
+ var ScanFilterSchema = import_zod.z.object({
310
+ filterExpression: import_zod.z.string(),
311
+ expressionAttributeNames: import_zod.z.record(import_zod.z.string(), import_zod.z.string()),
312
+ expressionAttributeValues: import_zod.z.record(import_zod.z.string(), import_zod.z.any())
313
+ });
314
+ // Annotate the CommonJS export names for ESM import in node:
315
+ 0 && (module.exports = {
316
+ AbstractDynamoDBRepository,
317
+ QueryAllSchema,
318
+ QueryWithPaginationSchema,
319
+ QueryWithSortingSchema,
320
+ ScanFilterSchema,
321
+ TableKeychema,
322
+ TableSpecSchema
323
+ });
324
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/repository.ts","../src/types.ts"],"sourcesContent":["export * from './repository';\nexport * from './types';\n","import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n BatchGetCommand,\n BatchWriteCommand,\n DeleteCommand,\n DynamoDBDocumentClient,\n GetCommand,\n PutCommand,\n QueryCommand,\n ScanCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport { createDebug } from '@opble/debug';\nimport { Factory, HashMap } from '@opble/types';\nimport { isTimestamp, isTimestamps } from '@opble/entity';\nimport {\n DynamoDBRepository,\n QueryAll,\n QueryWithPagination,\n QueryWithSorting,\n ScanFilter,\n TableKey,\n TableSpec,\n} from './types';\n\nconst debug = createDebug('opble:repository-dynamodb');\nconst SORT_ASCENDING = 'asc';\nconst BATCH_SIZE = 50;\n\nfunction now(): number {\n return Math.floor(Date.now() / 1000);\n}\n\nexport class AbstractDynamoDBRepository<\n T extends HashMap,\n ID extends TableKey = TableKey,\n> implements DynamoDBRepository<T, ID> {\n protected client: DynamoDBDocumentClient;\n\n constructor(\n protected table: TableSpec,\n protected factory: Factory<T>\n ) {\n this.client = DynamoDBDocumentClient.from(new DynamoDBClient({}));\n }\n\n protected getKey(id: ID): HashMap {\n return {\n [this.table.keys.hashKey]: id.hashKey,\n ...(this.table.keys.sortKey\n ? { [this.table.keys.sortKey]: id.sortKey }\n : {}),\n };\n }\n\n async query(\n hashKey: unknown,\n query?: QueryAll,\n pagination?: QueryWithPagination,\n sorting?: Pick<QueryWithSorting, 'sortOrder'>\n ): Promise<T[]> {\n /**\n * Internal state for pagination\n * - lastEvaluatedKey tells DynamoDB where to resume from\n * - currentPage helps us skip earlier pages\n * - resultItems will store the final page’s items\n */\n let lastEvaluatedKey: Record<string, unknown> | undefined;\n let currentPage = 1;\n let resultItems: HashMap[] = [];\n const filterReturnedItemsExcludeSortKey = (items: HashMap[]) => {\n /**\n * Business rule: Exclude a specific sortKey (if requested)\n * This is used to skip “parent” or “header” records within the same partition.\n */\n return query?.excludeSortKey && this.table.keys.sortKey\n ? items.filter(\n (item) => item[this.table.keys.sortKey!] !== query.excludeSortKey\n )\n : items;\n };\n\n do {\n /**\n * Prepare the QueryCommand\n * - Basic condition: match all items with the given hash key\n * - Add pagination via Limit + ExclusiveStartKey\n */\n const command = new QueryCommand({\n TableName: this.table.name,\n KeyConditionExpression: `#${this.table.keys.hashKey} = :hashKeyValue`,\n ExpressionAttributeNames: {\n [`#${this.table.keys.hashKey}`]: this.table.keys.hashKey,\n },\n ExpressionAttributeValues: {\n ':hashKeyValue': hashKey,\n },\n ExclusiveStartKey: lastEvaluatedKey,\n });\n\n if (pagination) {\n command.input.Limit = pagination.limit;\n }\n if (sorting) {\n command.input.ScanIndexForward = sorting.sortOrder === 'asc';\n }\n\n /**\n * Apply optional filters if provided\n */\n if (query) {\n // If querying a secondary index\n if (query.index) {\n command.input.IndexName = query.index.name;\n command.input.KeyConditionExpression = `#${query.index.keys.hashKey} = :hashKeyValue`;\n command.input.ExpressionAttributeNames = {\n [`#${query.index.keys.hashKey}`]: query.index.keys.hashKey,\n };\n }\n\n // If we want to match only sort keys starting with a specific prefix\n const sortKey = query.index?.keys.sortKey ?? this.table.keys.sortKey;\n if (query.sortKeyBeginsWith && sortKey) {\n command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND begins_with(#${sortKey}, :sortKeyBeginsWith)`;\n command.input.ExpressionAttributeNames ??= {};\n command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;\n command.input.ExpressionAttributeValues ??= {};\n command.input.ExpressionAttributeValues[':sortKeyBeginsWith'] =\n query.sortKeyBeginsWith;\n } else if (query.sortKey && sortKey) {\n command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} = :sortKeyValue`;\n command.input.ExpressionAttributeNames ??= {};\n command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;\n command.input.ExpressionAttributeValues ??= {};\n command.input.ExpressionAttributeValues[':sortKeyValue'] =\n query.sortKey;\n } else if (query.sortKeyBetween && sortKey) {\n command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} BETWEEN :sortKeyStart AND :sortKeyEnd`;\n command.input.ExpressionAttributeNames ??= {};\n command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;\n command.input.ExpressionAttributeValues ??= {};\n command.input.ExpressionAttributeValues[':sortKeyStart'] =\n query.sortKeyBetween.start;\n command.input.ExpressionAttributeValues[':sortKeyEnd'] =\n query.sortKeyBetween.end;\n }\n }\n debug('command#input', command.input);\n\n // Execute the query\n const result = await this.client.send(command);\n\n // Keep the pagination pointer (if DynamoDB says there’s more data)\n lastEvaluatedKey = result.LastEvaluatedKey;\n\n if (!pagination) {\n // pagination is disabled, fetch until no results found\n resultItems.push(\n ...filterReturnedItemsExcludeSortKey(result.Items ?? [])\n );\n } else if (currentPage === pagination.page) {\n /**\n * Once we reach the desired page, capture those items.\n * DynamoDB doesn’t support direct page jumps — we simulate it by looping\n * until we reach the correct page number.\n */\n\n resultItems = filterReturnedItemsExcludeSortKey(result.Items ?? []);\n break; // Stop — we’ve collected the requested page\n }\n\n currentPage++; // Move to next page and continue querying\n } while (lastEvaluatedKey);\n\n /**\n * Map each DynamoDB item into an instance of your model class (T)\n * When querrying with index, turn strict to false\n */\n const strict = !query || !query.index;\n return resultItems.map((item) => this.factory.create(item, { strict }));\n }\n\n async queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]> {\n // Split keys into chunks of BATCH_SIZE (DynamoDB BatchGet limit)\n const chunks: HashMap[][] = [];\n for (let i = 0; i < keys.length; i += BATCH_SIZE) {\n chunks.push(keys.slice(i, i + BATCH_SIZE));\n }\n\n // Execute all batch requests concurrently\n const results = await Promise.all(\n chunks.map(async (chunk) => {\n const command = new BatchGetCommand({\n RequestItems: {\n [this.table.name]: { Keys: chunk },\n },\n });\n const response = await this.client.send(command);\n return response.Responses?.[this.table.name] ?? [];\n })\n );\n\n // Merge all items from all responses\n const mergedItems = results.flat();\n\n // Convert each item to T using factory\n const items = mergedItems.map((item) => this.factory.create(item));\n\n // Sort if query.sortBy is specified\n if (query && query.sortBy) {\n const sortOrder = query.sortOrder ?? SORT_ASCENDING;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n items.sort((a: any, b: any) => {\n if (a[query.sortBy!] < b[query.sortBy!])\n return sortOrder === SORT_ASCENDING ? -1 : 1;\n if (a[query.sortBy!] > b[query.sortBy!])\n return sortOrder === SORT_ASCENDING ? 1 : -1;\n return 0;\n });\n }\n\n return items;\n }\n\n async scan(\n pagination?: QueryWithPagination,\n filter?: ScanFilter\n ): Promise<T[]> {\n /**\n * Internal state for pagination\n * - lastEvaluatedKey tells DynamoDB where to resume from\n * - currentPage helps us skip earlier pages\n * - resultItems will store the final page’s items\n */\n let lastEvaluatedKey: Record<string, unknown> | undefined;\n let currentPage = 1;\n let resultItems: HashMap[] = [];\n\n do {\n /**\n * Prepare the ScanCommand\n * - Add pagination via Limit + ExclusiveStartKey\n */\n const command = new ScanCommand({\n TableName: this.table.name,\n ExclusiveStartKey: lastEvaluatedKey,\n });\n\n if (pagination) {\n command.input.Limit = pagination.limit;\n }\n\n if (filter) {\n command.input.FilterExpression = filter.filterExpression;\n command.input.ExpressionAttributeNames =\n filter.expressionAttributeNames;\n command.input.ExpressionAttributeValues =\n filter.expressionAttributeValues;\n }\n\n /**\n * Execute the scan\n */\n const result = await this.client.send(command);\n\n // Keep the pagination pointer (if DynamoDB says there’s more data)\n lastEvaluatedKey = result.LastEvaluatedKey;\n\n if (!pagination) {\n // pagination is disabled, fetch until no results found\n resultItems.push(...(result.Items ?? []));\n } else if (currentPage === pagination.page) {\n /**\n * Once we reach the desired page, capture those items.\n * DynamoDB doesn’t support direct page jumps — we simulate it by looping\n * until we reach the correct page number.\n */\n\n resultItems = result.Items ?? [];\n break; // Stop — we’ve collected the requested page\n }\n\n currentPage++; // Move to next page and continue querying\n } while (lastEvaluatedKey);\n\n /**\n * Map each DynamoDB item into an instance of your model class (T)\n */\n return resultItems.map((item) => this.factory.create(item));\n }\n\n async deleteAll(hashKey: unknown): Promise<void> {\n const items = await this.query(hashKey);\n const keys = items.map((item) => {\n if (this.table.keys.sortKey) {\n return {\n [this.table.keys.hashKey]: hashKey,\n [this.table.keys.sortKey]:\n item[this.table.keys.sortKey as keyof typeof item],\n };\n } else {\n return {\n [this.table.keys.hashKey]: hashKey,\n };\n }\n });\n\n await this.deleteInBulk(keys);\n }\n\n async deleteInBulk(keys: HashMap[]): Promise<void> {\n const batches = [];\n while (keys.length > 0) {\n batches.push(keys.splice(0, BATCH_SIZE));\n }\n\n for (const batch of batches) {\n // Create delete requests for each item in the batch\n const deleteRequests = batch.map((key) => ({\n DeleteRequest: { Key: key },\n }));\n\n // Execute the BatchWriteCommand\n const command = new BatchWriteCommand({\n RequestItems: {\n [this.table.name]: deleteRequests,\n },\n });\n\n try {\n const response = await this.client.send(command);\n debug('[INFO] Batch delete response:', response);\n\n // Check if there are unprocessed items and retry them if necessary\n if (\n response.UnprocessedItems &&\n response.UnprocessedItems[this.table.name]\n ) {\n debug('[WARN] Unprocessed items found. Retrying...');\n const unprocessedItems = response.UnprocessedItems[this.table.name];\n if (unprocessedItems === undefined) {\n continue;\n }\n const unprocessedKeys = unprocessedItems\n .map((item) => item.DeleteRequest?.Key)\n .filter((key): key is Record<string, unknown> => key != null); // Filter out undefined keys\n\n await this.deleteInBulk(unprocessedKeys);\n }\n } catch (error) {\n debug('[ERROR] Error deleting items in batch:', error);\n }\n }\n }\n\n async get(id: ID): Promise<T | null> {\n const command = new GetCommand({\n TableName: this.table.name,\n Key: this.getKey(id),\n });\n\n const result = await this.client.send(command);\n return result.Item ? this.factory.create(result.Item) : null;\n }\n\n /**\n * Allow to manipulate the data before saving\n * @param item\n * @returns\n */\n protected beforeSave(item: T): T {\n if (isTimestamps(item)) {\n item.updatedAt = now();\n } else if (isTimestamp(item)) {\n item.timestamp = now();\n }\n\n return item;\n }\n\n async save(item: T): Promise<T> {\n item = this.beforeSave(item);\n const command = new PutCommand({\n TableName: this.table.name,\n Item: { ...item },\n });\n await this.client.send(command);\n\n return item;\n }\n\n async delete(id: ID): Promise<void | T> {\n await this.client.send(\n new DeleteCommand({\n TableName: this.table.name,\n Key: this.getKey(id),\n })\n );\n }\n}\n","import { z } from 'zod';\nimport {\n DeletableRepository,\n HashMap,\n ReadableRepository,\n SavableRepository,\n} from '@opble/types';\n\nexport const TableKeychema = z.object({\n hashKey: z.string(),\n sortKey: z.union([z.string(), z.number()]).optional(),\n});\n\nexport const TableSpecSchema = z.object({\n name: z.string(),\n keys: z.object({\n hashKey: z.string(),\n sortKey: z.string().optional(),\n }),\n});\n\nexport const QueryWithPaginationSchema = z.object({\n page: z\n .string()\n .optional()\n .transform((val) => (val ? parseInt(val, 10) : 1)) // default 1\n .refine((val) => val >= 1, { message: 'Page must be at least 1' }),\n\n limit: z\n .string()\n .optional()\n .transform((val) => (val ? parseInt(val, 10) : 25)) // default 25\n .refine((val) => val >= 5 && val <= 100, {\n message: 'Limit must be between 5 and 100',\n }),\n});\n\nexport const QueryWithSortingSchema = z.object({\n sortBy: z.string().optional(),\n sortOrder: z\n .enum(['asc', 'desc'])\n .optional()\n .transform((val) => val ?? 'asc'),\n});\n\nexport const QueryAllSchema = z.object({\n excludeSortKey: z.string().optional(),\n sortKeyBetween: z\n .object({\n start: z.string(),\n end: z.string(),\n })\n .optional(),\n sortKeyBeginsWith: z.string().optional(),\n sortKey: z.any().optional(),\n index: TableSpecSchema.optional(),\n});\n\nexport const ScanFilterSchema = z.object({\n filterExpression: z.string(),\n expressionAttributeNames: z.record(z.string(), z.string()),\n expressionAttributeValues: z.record(z.string(), z.any()),\n});\n\nexport type TableKey = z.infer<typeof TableKeychema>;\nexport type TableSpec = z.infer<typeof TableSpecSchema>;\n\nexport type QueryWithPagination = z.infer<typeof QueryWithPaginationSchema>;\nexport type QueryWithSorting = z.infer<typeof QueryWithSortingSchema>;\nexport type QueryAll = z.infer<typeof QueryAllSchema>;\nexport type ScanFilter = z.infer<typeof ScanFilterSchema>;\n\n/**\n * A generic repository interface specifically designed for DynamoDB operations.\n * It extends the basic CRUD repositories (Readable, Savable, Deletable) and adds\n * DynamoDB-specific methods for efficient querying, scanning, bulk operations,\n * and deletion patterns commonly used with DynamoDB's key structure.\n *\n * @template T - The type of the entity/document stored in DynamoDB (must be a HashMap / Record<string, unknown>)\n * @template ID - The type used to identify items. Defaults to `TableKey` (hash + optional sort key).\n */\nexport interface DynamoDBRepository<T extends HashMap, ID = TableKey>\n extends\n ReadableRepository<T, ID>,\n SavableRepository<T>,\n DeletableRepository<T, ID> {\n /**\n * Queries items using a partition key (hashKey) and optional sort key conditions.\n * This is the most common and efficient way to read data from a DynamoDB table\n * when you know the partition key.\n *\n * @param hashKey - The value of the partition key (hash key)\n * @param query - Optional advanced query conditions for sort key (begins_with, between, specific value, etc.)\n * @param pagination - Optional pagination parameters (page & limit)\n * @param sorting - Optional sort direction (only 'sortOrder' is used)\n * @returns Promise of array of matching items (T[])\n *\n * @example\n * repo.query(\"user#123\", { sortKeyBeginsWith: \"order#\" }, { limit: 20 }, { sortOrder: \"desc\" })\n */\n query(\n hashKey: unknown,\n query?: QueryAll,\n pagination?: QueryWithPagination,\n sorting?: Pick<QueryWithSorting, 'sortOrder'>\n ): Promise<T[]>;\n\n /**\n * Batch retrieves multiple items using their full composite keys (hash + sort).\n * This method uses `BatchGetItem` under the hood — much more efficient than individual gets\n * when fetching many items by primary key.\n *\n * @param keys - Array of objects containing at least `{ hashKey, sortKey? }`\n * @param query - Optional sorting preferences (rarely used in batch get)\n * @returns Promise of array of found items (in the order requested, missing items are omitted)\n *\n * @example\n * repo.queryInBulk([\n * { hashKey: \"user#abc\", sortKey: \"profile\" },\n * { hashKey: \"user#abc\", sortKey: \"settings\" }\n * ])\n */\n queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]>;\n\n /**\n * Performs a full table or index scan with optional filtering and pagination.\n * Scans are less efficient than queries — use only when you cannot use a partition key.\n *\n * @param pagination - Optional page & limit controls\n * @param filter - Optional raw filter expression (FilterExpression + attribute names/values)\n * @returns Promise of array of matching items\n *\n * @warning Scans can be expensive and slow on large tables — prefer query() when possible\n */\n scan(pagination?: QueryWithPagination, filter?: ScanFilter): Promise<T[]>;\n\n /**\n * Deletes **all items** that share the same partition key (hashKey).\n * Useful for cleaning up all records related to a specific entity (e.g. all orders of a user).\n *\n * @param hashKey - The partition key value whose items should be deleted\n * @returns Promise that resolves when deletion is complete\n *\n * @warning This operation may consume many write capacity units if the partition is large.\n * Consider using Query + BatchWriteItem in production for very large partitions.\n */\n deleteAll(hashKey: unknown): Promise<void>;\n\n /**\n * Deletes multiple items in a single BatchWriteItem call using their full keys.\n * Most efficient way to delete many known items at once.\n *\n * @param keys - Array of objects with hashKey and sortKey\n * @returns Promise that resolves when all deletions are complete\n *\n * @example\n * repo.deleteInBulk([\n * { hashKey: \"user#123\", sortKey: \"session#abc123\" },\n * { hashKey: \"user#123\", sortKey: \"session#def456\" }\n * ])\n */\n deleteInBulk(keys: HashMap[]): Promise<void>;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,155 @@
1
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
2
+ import { HashMap, ReadableRepository, SavableRepository, DeletableRepository, Factory } from '@opble/types';
3
+ import { z } from 'zod';
4
+
5
+ declare const TableKeychema: z.ZodObject<{
6
+ hashKey: z.ZodString;
7
+ sortKey: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
8
+ }, z.core.$strip>;
9
+ declare const TableSpecSchema: z.ZodObject<{
10
+ name: z.ZodString;
11
+ keys: z.ZodObject<{
12
+ hashKey: z.ZodString;
13
+ sortKey: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$strip>;
15
+ }, z.core.$strip>;
16
+ declare const QueryWithPaginationSchema: z.ZodObject<{
17
+ page: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<number, string | undefined>>;
18
+ limit: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<number, string | undefined>>;
19
+ }, z.core.$strip>;
20
+ declare const QueryWithSortingSchema: z.ZodObject<{
21
+ sortBy: z.ZodOptional<z.ZodString>;
22
+ sortOrder: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
23
+ asc: "asc";
24
+ desc: "desc";
25
+ }>>, z.ZodTransform<"asc" | "desc", "asc" | "desc" | undefined>>;
26
+ }, z.core.$strip>;
27
+ declare const QueryAllSchema: z.ZodObject<{
28
+ excludeSortKey: z.ZodOptional<z.ZodString>;
29
+ sortKeyBetween: z.ZodOptional<z.ZodObject<{
30
+ start: z.ZodString;
31
+ end: z.ZodString;
32
+ }, z.core.$strip>>;
33
+ sortKeyBeginsWith: z.ZodOptional<z.ZodString>;
34
+ sortKey: z.ZodOptional<z.ZodAny>;
35
+ index: z.ZodOptional<z.ZodObject<{
36
+ name: z.ZodString;
37
+ keys: z.ZodObject<{
38
+ hashKey: z.ZodString;
39
+ sortKey: z.ZodOptional<z.ZodString>;
40
+ }, z.core.$strip>;
41
+ }, z.core.$strip>>;
42
+ }, z.core.$strip>;
43
+ declare const ScanFilterSchema: z.ZodObject<{
44
+ filterExpression: z.ZodString;
45
+ expressionAttributeNames: z.ZodRecord<z.ZodString, z.ZodString>;
46
+ expressionAttributeValues: z.ZodRecord<z.ZodString, z.ZodAny>;
47
+ }, z.core.$strip>;
48
+ type TableKey = z.infer<typeof TableKeychema>;
49
+ type TableSpec = z.infer<typeof TableSpecSchema>;
50
+ type QueryWithPagination = z.infer<typeof QueryWithPaginationSchema>;
51
+ type QueryWithSorting = z.infer<typeof QueryWithSortingSchema>;
52
+ type QueryAll = z.infer<typeof QueryAllSchema>;
53
+ type ScanFilter = z.infer<typeof ScanFilterSchema>;
54
+ /**
55
+ * A generic repository interface specifically designed for DynamoDB operations.
56
+ * It extends the basic CRUD repositories (Readable, Savable, Deletable) and adds
57
+ * DynamoDB-specific methods for efficient querying, scanning, bulk operations,
58
+ * and deletion patterns commonly used with DynamoDB's key structure.
59
+ *
60
+ * @template T - The type of the entity/document stored in DynamoDB (must be a HashMap / Record<string, unknown>)
61
+ * @template ID - The type used to identify items. Defaults to `TableKey` (hash + optional sort key).
62
+ */
63
+ interface DynamoDBRepository<T extends HashMap, ID = TableKey> extends ReadableRepository<T, ID>, SavableRepository<T>, DeletableRepository<T, ID> {
64
+ /**
65
+ * Queries items using a partition key (hashKey) and optional sort key conditions.
66
+ * This is the most common and efficient way to read data from a DynamoDB table
67
+ * when you know the partition key.
68
+ *
69
+ * @param hashKey - The value of the partition key (hash key)
70
+ * @param query - Optional advanced query conditions for sort key (begins_with, between, specific value, etc.)
71
+ * @param pagination - Optional pagination parameters (page & limit)
72
+ * @param sorting - Optional sort direction (only 'sortOrder' is used)
73
+ * @returns Promise of array of matching items (T[])
74
+ *
75
+ * @example
76
+ * repo.query("user#123", { sortKeyBeginsWith: "order#" }, { limit: 20 }, { sortOrder: "desc" })
77
+ */
78
+ query(hashKey: unknown, query?: QueryAll, pagination?: QueryWithPagination, sorting?: Pick<QueryWithSorting, 'sortOrder'>): Promise<T[]>;
79
+ /**
80
+ * Batch retrieves multiple items using their full composite keys (hash + sort).
81
+ * This method uses `BatchGetItem` under the hood — much more efficient than individual gets
82
+ * when fetching many items by primary key.
83
+ *
84
+ * @param keys - Array of objects containing at least `{ hashKey, sortKey? }`
85
+ * @param query - Optional sorting preferences (rarely used in batch get)
86
+ * @returns Promise of array of found items (in the order requested, missing items are omitted)
87
+ *
88
+ * @example
89
+ * repo.queryInBulk([
90
+ * { hashKey: "user#abc", sortKey: "profile" },
91
+ * { hashKey: "user#abc", sortKey: "settings" }
92
+ * ])
93
+ */
94
+ queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]>;
95
+ /**
96
+ * Performs a full table or index scan with optional filtering and pagination.
97
+ * Scans are less efficient than queries — use only when you cannot use a partition key.
98
+ *
99
+ * @param pagination - Optional page & limit controls
100
+ * @param filter - Optional raw filter expression (FilterExpression + attribute names/values)
101
+ * @returns Promise of array of matching items
102
+ *
103
+ * @warning Scans can be expensive and slow on large tables — prefer query() when possible
104
+ */
105
+ scan(pagination?: QueryWithPagination, filter?: ScanFilter): Promise<T[]>;
106
+ /**
107
+ * Deletes **all items** that share the same partition key (hashKey).
108
+ * Useful for cleaning up all records related to a specific entity (e.g. all orders of a user).
109
+ *
110
+ * @param hashKey - The partition key value whose items should be deleted
111
+ * @returns Promise that resolves when deletion is complete
112
+ *
113
+ * @warning This operation may consume many write capacity units if the partition is large.
114
+ * Consider using Query + BatchWriteItem in production for very large partitions.
115
+ */
116
+ deleteAll(hashKey: unknown): Promise<void>;
117
+ /**
118
+ * Deletes multiple items in a single BatchWriteItem call using their full keys.
119
+ * Most efficient way to delete many known items at once.
120
+ *
121
+ * @param keys - Array of objects with hashKey and sortKey
122
+ * @returns Promise that resolves when all deletions are complete
123
+ *
124
+ * @example
125
+ * repo.deleteInBulk([
126
+ * { hashKey: "user#123", sortKey: "session#abc123" },
127
+ * { hashKey: "user#123", sortKey: "session#def456" }
128
+ * ])
129
+ */
130
+ deleteInBulk(keys: HashMap[]): Promise<void>;
131
+ }
132
+
133
+ declare class AbstractDynamoDBRepository<T extends HashMap, ID extends TableKey = TableKey> implements DynamoDBRepository<T, ID> {
134
+ protected table: TableSpec;
135
+ protected factory: Factory<T>;
136
+ protected client: DynamoDBDocumentClient;
137
+ constructor(table: TableSpec, factory: Factory<T>);
138
+ protected getKey(id: ID): HashMap;
139
+ query(hashKey: unknown, query?: QueryAll, pagination?: QueryWithPagination, sorting?: Pick<QueryWithSorting, 'sortOrder'>): Promise<T[]>;
140
+ queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]>;
141
+ scan(pagination?: QueryWithPagination, filter?: ScanFilter): Promise<T[]>;
142
+ deleteAll(hashKey: unknown): Promise<void>;
143
+ deleteInBulk(keys: HashMap[]): Promise<void>;
144
+ get(id: ID): Promise<T | null>;
145
+ /**
146
+ * Allow to manipulate the data before saving
147
+ * @param item
148
+ * @returns
149
+ */
150
+ protected beforeSave(item: T): T;
151
+ save(item: T): Promise<T>;
152
+ delete(id: ID): Promise<void | T>;
153
+ }
154
+
155
+ export { AbstractDynamoDBRepository, type DynamoDBRepository, type QueryAll, QueryAllSchema, type QueryWithPagination, QueryWithPaginationSchema, type QueryWithSorting, QueryWithSortingSchema, type ScanFilter, ScanFilterSchema, type TableKey, TableKeychema, type TableSpec, TableSpecSchema };
@@ -0,0 +1,155 @@
1
+ import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
2
+ import { HashMap, ReadableRepository, SavableRepository, DeletableRepository, Factory } from '@opble/types';
3
+ import { z } from 'zod';
4
+
5
+ declare const TableKeychema: z.ZodObject<{
6
+ hashKey: z.ZodString;
7
+ sortKey: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
8
+ }, z.core.$strip>;
9
+ declare const TableSpecSchema: z.ZodObject<{
10
+ name: z.ZodString;
11
+ keys: z.ZodObject<{
12
+ hashKey: z.ZodString;
13
+ sortKey: z.ZodOptional<z.ZodString>;
14
+ }, z.core.$strip>;
15
+ }, z.core.$strip>;
16
+ declare const QueryWithPaginationSchema: z.ZodObject<{
17
+ page: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<number, string | undefined>>;
18
+ limit: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<number, string | undefined>>;
19
+ }, z.core.$strip>;
20
+ declare const QueryWithSortingSchema: z.ZodObject<{
21
+ sortBy: z.ZodOptional<z.ZodString>;
22
+ sortOrder: z.ZodPipe<z.ZodOptional<z.ZodEnum<{
23
+ asc: "asc";
24
+ desc: "desc";
25
+ }>>, z.ZodTransform<"asc" | "desc", "asc" | "desc" | undefined>>;
26
+ }, z.core.$strip>;
27
+ declare const QueryAllSchema: z.ZodObject<{
28
+ excludeSortKey: z.ZodOptional<z.ZodString>;
29
+ sortKeyBetween: z.ZodOptional<z.ZodObject<{
30
+ start: z.ZodString;
31
+ end: z.ZodString;
32
+ }, z.core.$strip>>;
33
+ sortKeyBeginsWith: z.ZodOptional<z.ZodString>;
34
+ sortKey: z.ZodOptional<z.ZodAny>;
35
+ index: z.ZodOptional<z.ZodObject<{
36
+ name: z.ZodString;
37
+ keys: z.ZodObject<{
38
+ hashKey: z.ZodString;
39
+ sortKey: z.ZodOptional<z.ZodString>;
40
+ }, z.core.$strip>;
41
+ }, z.core.$strip>>;
42
+ }, z.core.$strip>;
43
+ declare const ScanFilterSchema: z.ZodObject<{
44
+ filterExpression: z.ZodString;
45
+ expressionAttributeNames: z.ZodRecord<z.ZodString, z.ZodString>;
46
+ expressionAttributeValues: z.ZodRecord<z.ZodString, z.ZodAny>;
47
+ }, z.core.$strip>;
48
+ type TableKey = z.infer<typeof TableKeychema>;
49
+ type TableSpec = z.infer<typeof TableSpecSchema>;
50
+ type QueryWithPagination = z.infer<typeof QueryWithPaginationSchema>;
51
+ type QueryWithSorting = z.infer<typeof QueryWithSortingSchema>;
52
+ type QueryAll = z.infer<typeof QueryAllSchema>;
53
+ type ScanFilter = z.infer<typeof ScanFilterSchema>;
54
+ /**
55
+ * A generic repository interface specifically designed for DynamoDB operations.
56
+ * It extends the basic CRUD repositories (Readable, Savable, Deletable) and adds
57
+ * DynamoDB-specific methods for efficient querying, scanning, bulk operations,
58
+ * and deletion patterns commonly used with DynamoDB's key structure.
59
+ *
60
+ * @template T - The type of the entity/document stored in DynamoDB (must be a HashMap / Record<string, unknown>)
61
+ * @template ID - The type used to identify items. Defaults to `TableKey` (hash + optional sort key).
62
+ */
63
+ interface DynamoDBRepository<T extends HashMap, ID = TableKey> extends ReadableRepository<T, ID>, SavableRepository<T>, DeletableRepository<T, ID> {
64
+ /**
65
+ * Queries items using a partition key (hashKey) and optional sort key conditions.
66
+ * This is the most common and efficient way to read data from a DynamoDB table
67
+ * when you know the partition key.
68
+ *
69
+ * @param hashKey - The value of the partition key (hash key)
70
+ * @param query - Optional advanced query conditions for sort key (begins_with, between, specific value, etc.)
71
+ * @param pagination - Optional pagination parameters (page & limit)
72
+ * @param sorting - Optional sort direction (only 'sortOrder' is used)
73
+ * @returns Promise of array of matching items (T[])
74
+ *
75
+ * @example
76
+ * repo.query("user#123", { sortKeyBeginsWith: "order#" }, { limit: 20 }, { sortOrder: "desc" })
77
+ */
78
+ query(hashKey: unknown, query?: QueryAll, pagination?: QueryWithPagination, sorting?: Pick<QueryWithSorting, 'sortOrder'>): Promise<T[]>;
79
+ /**
80
+ * Batch retrieves multiple items using their full composite keys (hash + sort).
81
+ * This method uses `BatchGetItem` under the hood — much more efficient than individual gets
82
+ * when fetching many items by primary key.
83
+ *
84
+ * @param keys - Array of objects containing at least `{ hashKey, sortKey? }`
85
+ * @param query - Optional sorting preferences (rarely used in batch get)
86
+ * @returns Promise of array of found items (in the order requested, missing items are omitted)
87
+ *
88
+ * @example
89
+ * repo.queryInBulk([
90
+ * { hashKey: "user#abc", sortKey: "profile" },
91
+ * { hashKey: "user#abc", sortKey: "settings" }
92
+ * ])
93
+ */
94
+ queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]>;
95
+ /**
96
+ * Performs a full table or index scan with optional filtering and pagination.
97
+ * Scans are less efficient than queries — use only when you cannot use a partition key.
98
+ *
99
+ * @param pagination - Optional page & limit controls
100
+ * @param filter - Optional raw filter expression (FilterExpression + attribute names/values)
101
+ * @returns Promise of array of matching items
102
+ *
103
+ * @warning Scans can be expensive and slow on large tables — prefer query() when possible
104
+ */
105
+ scan(pagination?: QueryWithPagination, filter?: ScanFilter): Promise<T[]>;
106
+ /**
107
+ * Deletes **all items** that share the same partition key (hashKey).
108
+ * Useful for cleaning up all records related to a specific entity (e.g. all orders of a user).
109
+ *
110
+ * @param hashKey - The partition key value whose items should be deleted
111
+ * @returns Promise that resolves when deletion is complete
112
+ *
113
+ * @warning This operation may consume many write capacity units if the partition is large.
114
+ * Consider using Query + BatchWriteItem in production for very large partitions.
115
+ */
116
+ deleteAll(hashKey: unknown): Promise<void>;
117
+ /**
118
+ * Deletes multiple items in a single BatchWriteItem call using their full keys.
119
+ * Most efficient way to delete many known items at once.
120
+ *
121
+ * @param keys - Array of objects with hashKey and sortKey
122
+ * @returns Promise that resolves when all deletions are complete
123
+ *
124
+ * @example
125
+ * repo.deleteInBulk([
126
+ * { hashKey: "user#123", sortKey: "session#abc123" },
127
+ * { hashKey: "user#123", sortKey: "session#def456" }
128
+ * ])
129
+ */
130
+ deleteInBulk(keys: HashMap[]): Promise<void>;
131
+ }
132
+
133
+ declare class AbstractDynamoDBRepository<T extends HashMap, ID extends TableKey = TableKey> implements DynamoDBRepository<T, ID> {
134
+ protected table: TableSpec;
135
+ protected factory: Factory<T>;
136
+ protected client: DynamoDBDocumentClient;
137
+ constructor(table: TableSpec, factory: Factory<T>);
138
+ protected getKey(id: ID): HashMap;
139
+ query(hashKey: unknown, query?: QueryAll, pagination?: QueryWithPagination, sorting?: Pick<QueryWithSorting, 'sortOrder'>): Promise<T[]>;
140
+ queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]>;
141
+ scan(pagination?: QueryWithPagination, filter?: ScanFilter): Promise<T[]>;
142
+ deleteAll(hashKey: unknown): Promise<void>;
143
+ deleteInBulk(keys: HashMap[]): Promise<void>;
144
+ get(id: ID): Promise<T | null>;
145
+ /**
146
+ * Allow to manipulate the data before saving
147
+ * @param item
148
+ * @returns
149
+ */
150
+ protected beforeSave(item: T): T;
151
+ save(item: T): Promise<T>;
152
+ delete(id: ID): Promise<void | T>;
153
+ }
154
+
155
+ export { AbstractDynamoDBRepository, type DynamoDBRepository, type QueryAll, QueryAllSchema, type QueryWithPagination, QueryWithPaginationSchema, type QueryWithSorting, QueryWithSortingSchema, type ScanFilter, ScanFilterSchema, type TableKey, TableKeychema, type TableSpec, TableSpecSchema };
package/dist/index.js ADDED
@@ -0,0 +1,300 @@
1
+ // src/repository.ts
2
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
3
+ import {
4
+ BatchGetCommand,
5
+ BatchWriteCommand,
6
+ DeleteCommand,
7
+ DynamoDBDocumentClient,
8
+ GetCommand,
9
+ PutCommand,
10
+ QueryCommand,
11
+ ScanCommand
12
+ } from "@aws-sdk/lib-dynamodb";
13
+ import { createDebug } from "@opble/debug";
14
+ import { isTimestamp, isTimestamps } from "@opble/entity";
15
+ var debug = createDebug("opble:repository-dynamodb");
16
+ var SORT_ASCENDING = "asc";
17
+ var BATCH_SIZE = 50;
18
+ function now() {
19
+ return Math.floor(Date.now() / 1e3);
20
+ }
21
+ var AbstractDynamoDBRepository = class {
22
+ constructor(table, factory) {
23
+ this.table = table;
24
+ this.factory = factory;
25
+ this.client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
26
+ }
27
+ client;
28
+ getKey(id) {
29
+ return {
30
+ [this.table.keys.hashKey]: id.hashKey,
31
+ ...this.table.keys.sortKey ? { [this.table.keys.sortKey]: id.sortKey } : {}
32
+ };
33
+ }
34
+ async query(hashKey, query, pagination, sorting) {
35
+ let lastEvaluatedKey;
36
+ let currentPage = 1;
37
+ let resultItems = [];
38
+ const filterReturnedItemsExcludeSortKey = (items) => {
39
+ return query?.excludeSortKey && this.table.keys.sortKey ? items.filter(
40
+ (item) => item[this.table.keys.sortKey] !== query.excludeSortKey
41
+ ) : items;
42
+ };
43
+ do {
44
+ const command = new QueryCommand({
45
+ TableName: this.table.name,
46
+ KeyConditionExpression: `#${this.table.keys.hashKey} = :hashKeyValue`,
47
+ ExpressionAttributeNames: {
48
+ [`#${this.table.keys.hashKey}`]: this.table.keys.hashKey
49
+ },
50
+ ExpressionAttributeValues: {
51
+ ":hashKeyValue": hashKey
52
+ },
53
+ ExclusiveStartKey: lastEvaluatedKey
54
+ });
55
+ if (pagination) {
56
+ command.input.Limit = pagination.limit;
57
+ }
58
+ if (sorting) {
59
+ command.input.ScanIndexForward = sorting.sortOrder === "asc";
60
+ }
61
+ if (query) {
62
+ if (query.index) {
63
+ command.input.IndexName = query.index.name;
64
+ command.input.KeyConditionExpression = `#${query.index.keys.hashKey} = :hashKeyValue`;
65
+ command.input.ExpressionAttributeNames = {
66
+ [`#${query.index.keys.hashKey}`]: query.index.keys.hashKey
67
+ };
68
+ }
69
+ const sortKey = query.index?.keys.sortKey ?? this.table.keys.sortKey;
70
+ if (query.sortKeyBeginsWith && sortKey) {
71
+ command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND begins_with(#${sortKey}, :sortKeyBeginsWith)`;
72
+ command.input.ExpressionAttributeNames ??= {};
73
+ command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
74
+ command.input.ExpressionAttributeValues ??= {};
75
+ command.input.ExpressionAttributeValues[":sortKeyBeginsWith"] = query.sortKeyBeginsWith;
76
+ } else if (query.sortKey && sortKey) {
77
+ command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} = :sortKeyValue`;
78
+ command.input.ExpressionAttributeNames ??= {};
79
+ command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
80
+ command.input.ExpressionAttributeValues ??= {};
81
+ command.input.ExpressionAttributeValues[":sortKeyValue"] = query.sortKey;
82
+ } else if (query.sortKeyBetween && sortKey) {
83
+ command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} BETWEEN :sortKeyStart AND :sortKeyEnd`;
84
+ command.input.ExpressionAttributeNames ??= {};
85
+ command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
86
+ command.input.ExpressionAttributeValues ??= {};
87
+ command.input.ExpressionAttributeValues[":sortKeyStart"] = query.sortKeyBetween.start;
88
+ command.input.ExpressionAttributeValues[":sortKeyEnd"] = query.sortKeyBetween.end;
89
+ }
90
+ }
91
+ debug("command#input", command.input);
92
+ const result = await this.client.send(command);
93
+ lastEvaluatedKey = result.LastEvaluatedKey;
94
+ if (!pagination) {
95
+ resultItems.push(
96
+ ...filterReturnedItemsExcludeSortKey(result.Items ?? [])
97
+ );
98
+ } else if (currentPage === pagination.page) {
99
+ resultItems = filterReturnedItemsExcludeSortKey(result.Items ?? []);
100
+ break;
101
+ }
102
+ currentPage++;
103
+ } while (lastEvaluatedKey);
104
+ const strict = !query || !query.index;
105
+ return resultItems.map((item) => this.factory.create(item, { strict }));
106
+ }
107
+ async queryInBulk(keys, query) {
108
+ const chunks = [];
109
+ for (let i = 0; i < keys.length; i += BATCH_SIZE) {
110
+ chunks.push(keys.slice(i, i + BATCH_SIZE));
111
+ }
112
+ const results = await Promise.all(
113
+ chunks.map(async (chunk) => {
114
+ const command = new BatchGetCommand({
115
+ RequestItems: {
116
+ [this.table.name]: { Keys: chunk }
117
+ }
118
+ });
119
+ const response = await this.client.send(command);
120
+ return response.Responses?.[this.table.name] ?? [];
121
+ })
122
+ );
123
+ const mergedItems = results.flat();
124
+ const items = mergedItems.map((item) => this.factory.create(item));
125
+ if (query && query.sortBy) {
126
+ const sortOrder = query.sortOrder ?? SORT_ASCENDING;
127
+ items.sort((a, b) => {
128
+ if (a[query.sortBy] < b[query.sortBy])
129
+ return sortOrder === SORT_ASCENDING ? -1 : 1;
130
+ if (a[query.sortBy] > b[query.sortBy])
131
+ return sortOrder === SORT_ASCENDING ? 1 : -1;
132
+ return 0;
133
+ });
134
+ }
135
+ return items;
136
+ }
137
+ async scan(pagination, filter) {
138
+ let lastEvaluatedKey;
139
+ let currentPage = 1;
140
+ let resultItems = [];
141
+ do {
142
+ const command = new ScanCommand({
143
+ TableName: this.table.name,
144
+ ExclusiveStartKey: lastEvaluatedKey
145
+ });
146
+ if (pagination) {
147
+ command.input.Limit = pagination.limit;
148
+ }
149
+ if (filter) {
150
+ command.input.FilterExpression = filter.filterExpression;
151
+ command.input.ExpressionAttributeNames = filter.expressionAttributeNames;
152
+ command.input.ExpressionAttributeValues = filter.expressionAttributeValues;
153
+ }
154
+ const result = await this.client.send(command);
155
+ lastEvaluatedKey = result.LastEvaluatedKey;
156
+ if (!pagination) {
157
+ resultItems.push(...result.Items ?? []);
158
+ } else if (currentPage === pagination.page) {
159
+ resultItems = result.Items ?? [];
160
+ break;
161
+ }
162
+ currentPage++;
163
+ } while (lastEvaluatedKey);
164
+ return resultItems.map((item) => this.factory.create(item));
165
+ }
166
+ async deleteAll(hashKey) {
167
+ const items = await this.query(hashKey);
168
+ const keys = items.map((item) => {
169
+ if (this.table.keys.sortKey) {
170
+ return {
171
+ [this.table.keys.hashKey]: hashKey,
172
+ [this.table.keys.sortKey]: item[this.table.keys.sortKey]
173
+ };
174
+ } else {
175
+ return {
176
+ [this.table.keys.hashKey]: hashKey
177
+ };
178
+ }
179
+ });
180
+ await this.deleteInBulk(keys);
181
+ }
182
+ async deleteInBulk(keys) {
183
+ const batches = [];
184
+ while (keys.length > 0) {
185
+ batches.push(keys.splice(0, BATCH_SIZE));
186
+ }
187
+ for (const batch of batches) {
188
+ const deleteRequests = batch.map((key) => ({
189
+ DeleteRequest: { Key: key }
190
+ }));
191
+ const command = new BatchWriteCommand({
192
+ RequestItems: {
193
+ [this.table.name]: deleteRequests
194
+ }
195
+ });
196
+ try {
197
+ const response = await this.client.send(command);
198
+ debug("[INFO] Batch delete response:", response);
199
+ if (response.UnprocessedItems && response.UnprocessedItems[this.table.name]) {
200
+ debug("[WARN] Unprocessed items found. Retrying...");
201
+ const unprocessedItems = response.UnprocessedItems[this.table.name];
202
+ if (unprocessedItems === void 0) {
203
+ continue;
204
+ }
205
+ const unprocessedKeys = unprocessedItems.map((item) => item.DeleteRequest?.Key).filter((key) => key != null);
206
+ await this.deleteInBulk(unprocessedKeys);
207
+ }
208
+ } catch (error) {
209
+ debug("[ERROR] Error deleting items in batch:", error);
210
+ }
211
+ }
212
+ }
213
+ async get(id) {
214
+ const command = new GetCommand({
215
+ TableName: this.table.name,
216
+ Key: this.getKey(id)
217
+ });
218
+ const result = await this.client.send(command);
219
+ return result.Item ? this.factory.create(result.Item) : null;
220
+ }
221
+ /**
222
+ * Allow to manipulate the data before saving
223
+ * @param item
224
+ * @returns
225
+ */
226
+ beforeSave(item) {
227
+ if (isTimestamps(item)) {
228
+ item.updatedAt = now();
229
+ } else if (isTimestamp(item)) {
230
+ item.timestamp = now();
231
+ }
232
+ return item;
233
+ }
234
+ async save(item) {
235
+ item = this.beforeSave(item);
236
+ const command = new PutCommand({
237
+ TableName: this.table.name,
238
+ Item: { ...item }
239
+ });
240
+ await this.client.send(command);
241
+ return item;
242
+ }
243
+ async delete(id) {
244
+ await this.client.send(
245
+ new DeleteCommand({
246
+ TableName: this.table.name,
247
+ Key: this.getKey(id)
248
+ })
249
+ );
250
+ }
251
+ };
252
+
253
+ // src/types.ts
254
+ import { z } from "zod";
255
+ var TableKeychema = z.object({
256
+ hashKey: z.string(),
257
+ sortKey: z.union([z.string(), z.number()]).optional()
258
+ });
259
+ var TableSpecSchema = z.object({
260
+ name: z.string(),
261
+ keys: z.object({
262
+ hashKey: z.string(),
263
+ sortKey: z.string().optional()
264
+ })
265
+ });
266
+ var QueryWithPaginationSchema = z.object({
267
+ page: z.string().optional().transform((val) => val ? parseInt(val, 10) : 1).refine((val) => val >= 1, { message: "Page must be at least 1" }),
268
+ limit: z.string().optional().transform((val) => val ? parseInt(val, 10) : 25).refine((val) => val >= 5 && val <= 100, {
269
+ message: "Limit must be between 5 and 100"
270
+ })
271
+ });
272
+ var QueryWithSortingSchema = z.object({
273
+ sortBy: z.string().optional(),
274
+ sortOrder: z.enum(["asc", "desc"]).optional().transform((val) => val ?? "asc")
275
+ });
276
+ var QueryAllSchema = z.object({
277
+ excludeSortKey: z.string().optional(),
278
+ sortKeyBetween: z.object({
279
+ start: z.string(),
280
+ end: z.string()
281
+ }).optional(),
282
+ sortKeyBeginsWith: z.string().optional(),
283
+ sortKey: z.any().optional(),
284
+ index: TableSpecSchema.optional()
285
+ });
286
+ var ScanFilterSchema = z.object({
287
+ filterExpression: z.string(),
288
+ expressionAttributeNames: z.record(z.string(), z.string()),
289
+ expressionAttributeValues: z.record(z.string(), z.any())
290
+ });
291
+ export {
292
+ AbstractDynamoDBRepository,
293
+ QueryAllSchema,
294
+ QueryWithPaginationSchema,
295
+ QueryWithSortingSchema,
296
+ ScanFilterSchema,
297
+ TableKeychema,
298
+ TableSpecSchema
299
+ };
300
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/repository.ts","../src/types.ts"],"sourcesContent":["import { DynamoDBClient } from '@aws-sdk/client-dynamodb';\nimport {\n BatchGetCommand,\n BatchWriteCommand,\n DeleteCommand,\n DynamoDBDocumentClient,\n GetCommand,\n PutCommand,\n QueryCommand,\n ScanCommand,\n} from '@aws-sdk/lib-dynamodb';\nimport { createDebug } from '@opble/debug';\nimport { Factory, HashMap } from '@opble/types';\nimport { isTimestamp, isTimestamps } from '@opble/entity';\nimport {\n DynamoDBRepository,\n QueryAll,\n QueryWithPagination,\n QueryWithSorting,\n ScanFilter,\n TableKey,\n TableSpec,\n} from './types';\n\nconst debug = createDebug('opble:repository-dynamodb');\nconst SORT_ASCENDING = 'asc';\nconst BATCH_SIZE = 50;\n\nfunction now(): number {\n return Math.floor(Date.now() / 1000);\n}\n\nexport class AbstractDynamoDBRepository<\n T extends HashMap,\n ID extends TableKey = TableKey,\n> implements DynamoDBRepository<T, ID> {\n protected client: DynamoDBDocumentClient;\n\n constructor(\n protected table: TableSpec,\n protected factory: Factory<T>\n ) {\n this.client = DynamoDBDocumentClient.from(new DynamoDBClient({}));\n }\n\n protected getKey(id: ID): HashMap {\n return {\n [this.table.keys.hashKey]: id.hashKey,\n ...(this.table.keys.sortKey\n ? { [this.table.keys.sortKey]: id.sortKey }\n : {}),\n };\n }\n\n async query(\n hashKey: unknown,\n query?: QueryAll,\n pagination?: QueryWithPagination,\n sorting?: Pick<QueryWithSorting, 'sortOrder'>\n ): Promise<T[]> {\n /**\n * Internal state for pagination\n * - lastEvaluatedKey tells DynamoDB where to resume from\n * - currentPage helps us skip earlier pages\n * - resultItems will store the final page’s items\n */\n let lastEvaluatedKey: Record<string, unknown> | undefined;\n let currentPage = 1;\n let resultItems: HashMap[] = [];\n const filterReturnedItemsExcludeSortKey = (items: HashMap[]) => {\n /**\n * Business rule: Exclude a specific sortKey (if requested)\n * This is used to skip “parent” or “header” records within the same partition.\n */\n return query?.excludeSortKey && this.table.keys.sortKey\n ? items.filter(\n (item) => item[this.table.keys.sortKey!] !== query.excludeSortKey\n )\n : items;\n };\n\n do {\n /**\n * Prepare the QueryCommand\n * - Basic condition: match all items with the given hash key\n * - Add pagination via Limit + ExclusiveStartKey\n */\n const command = new QueryCommand({\n TableName: this.table.name,\n KeyConditionExpression: `#${this.table.keys.hashKey} = :hashKeyValue`,\n ExpressionAttributeNames: {\n [`#${this.table.keys.hashKey}`]: this.table.keys.hashKey,\n },\n ExpressionAttributeValues: {\n ':hashKeyValue': hashKey,\n },\n ExclusiveStartKey: lastEvaluatedKey,\n });\n\n if (pagination) {\n command.input.Limit = pagination.limit;\n }\n if (sorting) {\n command.input.ScanIndexForward = sorting.sortOrder === 'asc';\n }\n\n /**\n * Apply optional filters if provided\n */\n if (query) {\n // If querying a secondary index\n if (query.index) {\n command.input.IndexName = query.index.name;\n command.input.KeyConditionExpression = `#${query.index.keys.hashKey} = :hashKeyValue`;\n command.input.ExpressionAttributeNames = {\n [`#${query.index.keys.hashKey}`]: query.index.keys.hashKey,\n };\n }\n\n // If we want to match only sort keys starting with a specific prefix\n const sortKey = query.index?.keys.sortKey ?? this.table.keys.sortKey;\n if (query.sortKeyBeginsWith && sortKey) {\n command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND begins_with(#${sortKey}, :sortKeyBeginsWith)`;\n command.input.ExpressionAttributeNames ??= {};\n command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;\n command.input.ExpressionAttributeValues ??= {};\n command.input.ExpressionAttributeValues[':sortKeyBeginsWith'] =\n query.sortKeyBeginsWith;\n } else if (query.sortKey && sortKey) {\n command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} = :sortKeyValue`;\n command.input.ExpressionAttributeNames ??= {};\n command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;\n command.input.ExpressionAttributeValues ??= {};\n command.input.ExpressionAttributeValues[':sortKeyValue'] =\n query.sortKey;\n } else if (query.sortKeyBetween && sortKey) {\n command.input.KeyConditionExpression = `${command.input.KeyConditionExpression} AND #${sortKey} BETWEEN :sortKeyStart AND :sortKeyEnd`;\n command.input.ExpressionAttributeNames ??= {};\n command.input.ExpressionAttributeNames[`#${sortKey}`] = sortKey;\n command.input.ExpressionAttributeValues ??= {};\n command.input.ExpressionAttributeValues[':sortKeyStart'] =\n query.sortKeyBetween.start;\n command.input.ExpressionAttributeValues[':sortKeyEnd'] =\n query.sortKeyBetween.end;\n }\n }\n debug('command#input', command.input);\n\n // Execute the query\n const result = await this.client.send(command);\n\n // Keep the pagination pointer (if DynamoDB says there’s more data)\n lastEvaluatedKey = result.LastEvaluatedKey;\n\n if (!pagination) {\n // pagination is disabled, fetch until no results found\n resultItems.push(\n ...filterReturnedItemsExcludeSortKey(result.Items ?? [])\n );\n } else if (currentPage === pagination.page) {\n /**\n * Once we reach the desired page, capture those items.\n * DynamoDB doesn’t support direct page jumps — we simulate it by looping\n * until we reach the correct page number.\n */\n\n resultItems = filterReturnedItemsExcludeSortKey(result.Items ?? []);\n break; // Stop — we’ve collected the requested page\n }\n\n currentPage++; // Move to next page and continue querying\n } while (lastEvaluatedKey);\n\n /**\n * Map each DynamoDB item into an instance of your model class (T)\n * When querrying with index, turn strict to false\n */\n const strict = !query || !query.index;\n return resultItems.map((item) => this.factory.create(item, { strict }));\n }\n\n async queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]> {\n // Split keys into chunks of BATCH_SIZE (DynamoDB BatchGet limit)\n const chunks: HashMap[][] = [];\n for (let i = 0; i < keys.length; i += BATCH_SIZE) {\n chunks.push(keys.slice(i, i + BATCH_SIZE));\n }\n\n // Execute all batch requests concurrently\n const results = await Promise.all(\n chunks.map(async (chunk) => {\n const command = new BatchGetCommand({\n RequestItems: {\n [this.table.name]: { Keys: chunk },\n },\n });\n const response = await this.client.send(command);\n return response.Responses?.[this.table.name] ?? [];\n })\n );\n\n // Merge all items from all responses\n const mergedItems = results.flat();\n\n // Convert each item to T using factory\n const items = mergedItems.map((item) => this.factory.create(item));\n\n // Sort if query.sortBy is specified\n if (query && query.sortBy) {\n const sortOrder = query.sortOrder ?? SORT_ASCENDING;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n items.sort((a: any, b: any) => {\n if (a[query.sortBy!] < b[query.sortBy!])\n return sortOrder === SORT_ASCENDING ? -1 : 1;\n if (a[query.sortBy!] > b[query.sortBy!])\n return sortOrder === SORT_ASCENDING ? 1 : -1;\n return 0;\n });\n }\n\n return items;\n }\n\n async scan(\n pagination?: QueryWithPagination,\n filter?: ScanFilter\n ): Promise<T[]> {\n /**\n * Internal state for pagination\n * - lastEvaluatedKey tells DynamoDB where to resume from\n * - currentPage helps us skip earlier pages\n * - resultItems will store the final page’s items\n */\n let lastEvaluatedKey: Record<string, unknown> | undefined;\n let currentPage = 1;\n let resultItems: HashMap[] = [];\n\n do {\n /**\n * Prepare the ScanCommand\n * - Add pagination via Limit + ExclusiveStartKey\n */\n const command = new ScanCommand({\n TableName: this.table.name,\n ExclusiveStartKey: lastEvaluatedKey,\n });\n\n if (pagination) {\n command.input.Limit = pagination.limit;\n }\n\n if (filter) {\n command.input.FilterExpression = filter.filterExpression;\n command.input.ExpressionAttributeNames =\n filter.expressionAttributeNames;\n command.input.ExpressionAttributeValues =\n filter.expressionAttributeValues;\n }\n\n /**\n * Execute the scan\n */\n const result = await this.client.send(command);\n\n // Keep the pagination pointer (if DynamoDB says there’s more data)\n lastEvaluatedKey = result.LastEvaluatedKey;\n\n if (!pagination) {\n // pagination is disabled, fetch until no results found\n resultItems.push(...(result.Items ?? []));\n } else if (currentPage === pagination.page) {\n /**\n * Once we reach the desired page, capture those items.\n * DynamoDB doesn’t support direct page jumps — we simulate it by looping\n * until we reach the correct page number.\n */\n\n resultItems = result.Items ?? [];\n break; // Stop — we’ve collected the requested page\n }\n\n currentPage++; // Move to next page and continue querying\n } while (lastEvaluatedKey);\n\n /**\n * Map each DynamoDB item into an instance of your model class (T)\n */\n return resultItems.map((item) => this.factory.create(item));\n }\n\n async deleteAll(hashKey: unknown): Promise<void> {\n const items = await this.query(hashKey);\n const keys = items.map((item) => {\n if (this.table.keys.sortKey) {\n return {\n [this.table.keys.hashKey]: hashKey,\n [this.table.keys.sortKey]:\n item[this.table.keys.sortKey as keyof typeof item],\n };\n } else {\n return {\n [this.table.keys.hashKey]: hashKey,\n };\n }\n });\n\n await this.deleteInBulk(keys);\n }\n\n async deleteInBulk(keys: HashMap[]): Promise<void> {\n const batches = [];\n while (keys.length > 0) {\n batches.push(keys.splice(0, BATCH_SIZE));\n }\n\n for (const batch of batches) {\n // Create delete requests for each item in the batch\n const deleteRequests = batch.map((key) => ({\n DeleteRequest: { Key: key },\n }));\n\n // Execute the BatchWriteCommand\n const command = new BatchWriteCommand({\n RequestItems: {\n [this.table.name]: deleteRequests,\n },\n });\n\n try {\n const response = await this.client.send(command);\n debug('[INFO] Batch delete response:', response);\n\n // Check if there are unprocessed items and retry them if necessary\n if (\n response.UnprocessedItems &&\n response.UnprocessedItems[this.table.name]\n ) {\n debug('[WARN] Unprocessed items found. Retrying...');\n const unprocessedItems = response.UnprocessedItems[this.table.name];\n if (unprocessedItems === undefined) {\n continue;\n }\n const unprocessedKeys = unprocessedItems\n .map((item) => item.DeleteRequest?.Key)\n .filter((key): key is Record<string, unknown> => key != null); // Filter out undefined keys\n\n await this.deleteInBulk(unprocessedKeys);\n }\n } catch (error) {\n debug('[ERROR] Error deleting items in batch:', error);\n }\n }\n }\n\n async get(id: ID): Promise<T | null> {\n const command = new GetCommand({\n TableName: this.table.name,\n Key: this.getKey(id),\n });\n\n const result = await this.client.send(command);\n return result.Item ? this.factory.create(result.Item) : null;\n }\n\n /**\n * Allow to manipulate the data before saving\n * @param item\n * @returns\n */\n protected beforeSave(item: T): T {\n if (isTimestamps(item)) {\n item.updatedAt = now();\n } else if (isTimestamp(item)) {\n item.timestamp = now();\n }\n\n return item;\n }\n\n async save(item: T): Promise<T> {\n item = this.beforeSave(item);\n const command = new PutCommand({\n TableName: this.table.name,\n Item: { ...item },\n });\n await this.client.send(command);\n\n return item;\n }\n\n async delete(id: ID): Promise<void | T> {\n await this.client.send(\n new DeleteCommand({\n TableName: this.table.name,\n Key: this.getKey(id),\n })\n );\n }\n}\n","import { z } from 'zod';\nimport {\n DeletableRepository,\n HashMap,\n ReadableRepository,\n SavableRepository,\n} from '@opble/types';\n\nexport const TableKeychema = z.object({\n hashKey: z.string(),\n sortKey: z.union([z.string(), z.number()]).optional(),\n});\n\nexport const TableSpecSchema = z.object({\n name: z.string(),\n keys: z.object({\n hashKey: z.string(),\n sortKey: z.string().optional(),\n }),\n});\n\nexport const QueryWithPaginationSchema = z.object({\n page: z\n .string()\n .optional()\n .transform((val) => (val ? parseInt(val, 10) : 1)) // default 1\n .refine((val) => val >= 1, { message: 'Page must be at least 1' }),\n\n limit: z\n .string()\n .optional()\n .transform((val) => (val ? parseInt(val, 10) : 25)) // default 25\n .refine((val) => val >= 5 && val <= 100, {\n message: 'Limit must be between 5 and 100',\n }),\n});\n\nexport const QueryWithSortingSchema = z.object({\n sortBy: z.string().optional(),\n sortOrder: z\n .enum(['asc', 'desc'])\n .optional()\n .transform((val) => val ?? 'asc'),\n});\n\nexport const QueryAllSchema = z.object({\n excludeSortKey: z.string().optional(),\n sortKeyBetween: z\n .object({\n start: z.string(),\n end: z.string(),\n })\n .optional(),\n sortKeyBeginsWith: z.string().optional(),\n sortKey: z.any().optional(),\n index: TableSpecSchema.optional(),\n});\n\nexport const ScanFilterSchema = z.object({\n filterExpression: z.string(),\n expressionAttributeNames: z.record(z.string(), z.string()),\n expressionAttributeValues: z.record(z.string(), z.any()),\n});\n\nexport type TableKey = z.infer<typeof TableKeychema>;\nexport type TableSpec = z.infer<typeof TableSpecSchema>;\n\nexport type QueryWithPagination = z.infer<typeof QueryWithPaginationSchema>;\nexport type QueryWithSorting = z.infer<typeof QueryWithSortingSchema>;\nexport type QueryAll = z.infer<typeof QueryAllSchema>;\nexport type ScanFilter = z.infer<typeof ScanFilterSchema>;\n\n/**\n * A generic repository interface specifically designed for DynamoDB operations.\n * It extends the basic CRUD repositories (Readable, Savable, Deletable) and adds\n * DynamoDB-specific methods for efficient querying, scanning, bulk operations,\n * and deletion patterns commonly used with DynamoDB's key structure.\n *\n * @template T - The type of the entity/document stored in DynamoDB (must be a HashMap / Record<string, unknown>)\n * @template ID - The type used to identify items. Defaults to `TableKey` (hash + optional sort key).\n */\nexport interface DynamoDBRepository<T extends HashMap, ID = TableKey>\n extends\n ReadableRepository<T, ID>,\n SavableRepository<T>,\n DeletableRepository<T, ID> {\n /**\n * Queries items using a partition key (hashKey) and optional sort key conditions.\n * This is the most common and efficient way to read data from a DynamoDB table\n * when you know the partition key.\n *\n * @param hashKey - The value of the partition key (hash key)\n * @param query - Optional advanced query conditions for sort key (begins_with, between, specific value, etc.)\n * @param pagination - Optional pagination parameters (page & limit)\n * @param sorting - Optional sort direction (only 'sortOrder' is used)\n * @returns Promise of array of matching items (T[])\n *\n * @example\n * repo.query(\"user#123\", { sortKeyBeginsWith: \"order#\" }, { limit: 20 }, { sortOrder: \"desc\" })\n */\n query(\n hashKey: unknown,\n query?: QueryAll,\n pagination?: QueryWithPagination,\n sorting?: Pick<QueryWithSorting, 'sortOrder'>\n ): Promise<T[]>;\n\n /**\n * Batch retrieves multiple items using their full composite keys (hash + sort).\n * This method uses `BatchGetItem` under the hood — much more efficient than individual gets\n * when fetching many items by primary key.\n *\n * @param keys - Array of objects containing at least `{ hashKey, sortKey? }`\n * @param query - Optional sorting preferences (rarely used in batch get)\n * @returns Promise of array of found items (in the order requested, missing items are omitted)\n *\n * @example\n * repo.queryInBulk([\n * { hashKey: \"user#abc\", sortKey: \"profile\" },\n * { hashKey: \"user#abc\", sortKey: \"settings\" }\n * ])\n */\n queryInBulk(keys: HashMap[], query?: QueryWithSorting): Promise<T[]>;\n\n /**\n * Performs a full table or index scan with optional filtering and pagination.\n * Scans are less efficient than queries — use only when you cannot use a partition key.\n *\n * @param pagination - Optional page & limit controls\n * @param filter - Optional raw filter expression (FilterExpression + attribute names/values)\n * @returns Promise of array of matching items\n *\n * @warning Scans can be expensive and slow on large tables — prefer query() when possible\n */\n scan(pagination?: QueryWithPagination, filter?: ScanFilter): Promise<T[]>;\n\n /**\n * Deletes **all items** that share the same partition key (hashKey).\n * Useful for cleaning up all records related to a specific entity (e.g. all orders of a user).\n *\n * @param hashKey - The partition key value whose items should be deleted\n * @returns Promise that resolves when deletion is complete\n *\n * @warning This operation may consume many write capacity units if the partition is large.\n * Consider using Query + BatchWriteItem in production for very large partitions.\n */\n deleteAll(hashKey: unknown): Promise<void>;\n\n /**\n * Deletes multiple items in a single BatchWriteItem call using their full keys.\n * Most efficient way to delete many known items at once.\n *\n * @param keys - Array of objects with hashKey and sortKey\n * @returns Promise that resolves when all deletions are complete\n *\n * @example\n * repo.deleteInBulk([\n * { hashKey: \"user#123\", sortKey: \"session#abc123\" },\n * { hashKey: \"user#123\", sortKey: \"session#def456\" }\n * ])\n */\n deleteInBulk(keys: HashMap[]): Promise<void>;\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@opble/repository-dynamodb",
3
+ "description": "Provide DynamoDB repository implementation.",
4
+ "version": "1.0.0",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ "import": "./dist/index.js",
12
+ "require": "./dist/index.cjs"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/opble/node-packages.git",
17
+ "directory": "packages/repository-dynamodb"
18
+ },
19
+ "keywords": [
20
+ "opble",
21
+ "dynamodb",
22
+ "repository"
23
+ ],
24
+ "bugs": {
25
+ "url": "https://github.com/opble/node-packages/issues"
26
+ },
27
+ "homepage": "https://github.com/opble/node-packages#readme",
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": false
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "LICENSE"
36
+ ],
37
+ "devDependencies": {
38
+ "@opble/typescript-config": "0.0.0",
39
+ "@opble/eslint-config": "0.0.0"
40
+ },
41
+ "dependencies": {
42
+ "@aws-sdk/client-dynamodb": "^3.971.0",
43
+ "@aws-sdk/lib-dynamodb": "^3.971.0",
44
+ "@opble/debug": "^1.0.0",
45
+ "@opble/entity": "^1.0.0",
46
+ "@opble/types": "^1.3.0",
47
+ "zod": "^4.3.5"
48
+ },
49
+ "scripts": {
50
+ "build": "tsup",
51
+ "clean": "rm -rf dist",
52
+ "lint": "eslint \"src/**/*.ts*\" --max-warnings 0",
53
+ "test": "true"
54
+ }
55
+ }