@eresearchqut/ddb-repository 1.1.0 → 1.3.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/.github/workflows/release.yml +1 -1
- package/CHANGELOG.md +14 -0
- package/dist/DynamoDbRepository.js +25 -4
- package/package.json +2 -1
- package/src/DynamoDbRepository.ts +47 -3
- package/test/repository.test.ts +19 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/eresearchqut/ddb-repository/compare/v1.2.0...v1.3.0) (2025-11-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Add test for the tracking of the consumed capacity ([4a1b23c](https://github.com/eresearchqut/ddb-repository/commit/4a1b23cadadbbb02d78507ca9d34715219fc952c))
|
|
7
|
+
|
|
8
|
+
# [1.2.0](https://github.com/eresearchqut/ddb-repository/compare/v1.1.0...v1.2.0) (2025-11-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* return the consumed capacity for each operation ([3910953](https://github.com/eresearchqut/ddb-repository/commit/391095398381a950c3393f9103d97d355c119538))
|
|
14
|
+
|
|
1
15
|
# [1.1.0](https://github.com/eresearchqut/ddb-repository/compare/v1.0.2...v1.1.0) (2025-11-19)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -27,7 +27,7 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
|
27
27
|
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
28
28
|
};
|
|
29
29
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
-
exports.DynamoDbRepository = exports.mapFilterExpressions = exports.mapFilterExpression = exports.FilterOperator = void 0;
|
|
30
|
+
exports.DynamoDbRepository = exports.consumedCapacityMiddleware = exports.mapFilterExpressions = exports.mapFilterExpression = exports.FilterOperator = void 0;
|
|
31
31
|
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
32
32
|
const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
|
|
33
33
|
const lodash_1 = require("lodash");
|
|
@@ -79,17 +79,34 @@ const paginate = (array, pageSize) => {
|
|
|
79
79
|
return acc;
|
|
80
80
|
}, []);
|
|
81
81
|
};
|
|
82
|
+
const consumedCapacityMiddleware = (consumedCapacityMiddlewareConfig) => (next, context) => (args) => __awaiter(void 0, void 0, void 0, function* () {
|
|
83
|
+
try {
|
|
84
|
+
const { input } = args;
|
|
85
|
+
const { ReturnConsumedCapacity } = input;
|
|
86
|
+
const response = yield next(args);
|
|
87
|
+
const { output } = response;
|
|
88
|
+
const consumedCapacity = (0, lodash_1.get)(output, "ConsumedCapacity");
|
|
89
|
+
yield consumedCapacityMiddlewareConfig.onConsumedCapacity({ ReturnConsumedCapacity, ConsumedCapacity: consumedCapacity });
|
|
90
|
+
return response;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
exports.consumedCapacityMiddleware = consumedCapacityMiddleware;
|
|
82
97
|
class DynamoDbRepository {
|
|
83
|
-
constructor(dynamoDBClient, tableName, hashKey, rangKey) {
|
|
98
|
+
constructor(dynamoDBClient, tableName, hashKey, rangKey, returnConsumedCapacity = client_dynamodb_1.ReturnConsumedCapacity.TOTAL) {
|
|
84
99
|
this.dynamoDBClient = dynamoDBClient;
|
|
85
100
|
this.tableName = tableName;
|
|
86
101
|
this.hashKey = hashKey;
|
|
87
102
|
this.rangKey = rangKey;
|
|
103
|
+
this.returnConsumedCapacity = returnConsumedCapacity;
|
|
88
104
|
this.getItem = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
89
105
|
return this.dynamoDBClient
|
|
90
106
|
.send(new client_dynamodb_1.GetItemCommand({
|
|
91
107
|
TableName: this.tableName,
|
|
92
108
|
Key: (0, util_dynamodb_1.marshall)(key, { removeUndefinedValues: true }),
|
|
109
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
93
110
|
}))
|
|
94
111
|
.then((result) => result.Item ? (0, util_dynamodb_1.unmarshall)(result.Item) : undefined);
|
|
95
112
|
});
|
|
@@ -98,6 +115,7 @@ class DynamoDbRepository {
|
|
|
98
115
|
return this.dynamoDBClient
|
|
99
116
|
.send(new client_dynamodb_1.PutItemCommand({
|
|
100
117
|
TableName: this.tableName,
|
|
118
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
101
119
|
Item,
|
|
102
120
|
}))
|
|
103
121
|
.then(() => this.getItem(key));
|
|
@@ -129,6 +147,7 @@ class DynamoDbRepository {
|
|
|
129
147
|
.filter(([, value]) => value !== undefined)
|
|
130
148
|
.reduce((acc, [key]) => (Object.assign(Object.assign({}, acc), { [`#${expressionAttributeKey(key)}`]: key })), Object.assign(removeAttributeNames)),
|
|
131
149
|
ExpressionAttributeValues: hasUpdates ? (0, util_dynamodb_1.marshall)(Object.entries(updates).reduce((acc, [key, value]) => (Object.assign(Object.assign({}, acc), { [`:${expressionAttributeKey(key)}`]: value })), Object.assign({})), { removeUndefinedValues: true }) : undefined,
|
|
150
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
132
151
|
};
|
|
133
152
|
return this.dynamoDBClient
|
|
134
153
|
.send(new client_dynamodb_1.UpdateItemCommand(updateItemCommandInput))
|
|
@@ -160,6 +179,7 @@ class DynamoDbRepository {
|
|
|
160
179
|
: {};
|
|
161
180
|
const queryCommandInput = {
|
|
162
181
|
TableName: this.tableName,
|
|
182
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
163
183
|
IndexName: index,
|
|
164
184
|
KeyConditionExpression,
|
|
165
185
|
FilterExpression,
|
|
@@ -226,9 +246,10 @@ class DynamoDbRepository {
|
|
|
226
246
|
[this.tableName]: {
|
|
227
247
|
Keys: keyPage.map((key) => ((0, util_dynamodb_1.marshall)(key))),
|
|
228
248
|
ProjectionExpression,
|
|
229
|
-
ExpressionAttributeNames
|
|
249
|
+
ExpressionAttributeNames,
|
|
230
250
|
}
|
|
231
|
-
}
|
|
251
|
+
},
|
|
252
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
232
253
|
};
|
|
233
254
|
return this.dynamoDBClient.send(new client_dynamodb_1.BatchGetItemCommand(batchRequest)).then(result => { var _a; return (_a = result.Responses) === null || _a === void 0 ? void 0 : _a[this.tableName].map((item) => (0, util_dynamodb_1.unmarshall)(item)); });
|
|
234
255
|
}))))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eresearchqut/ddb-repository",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@semantic-release/git": "^10.0.1",
|
|
24
24
|
"@semantic-release/github": "^12.0.2",
|
|
25
25
|
"@semantic-release/npm": "^13.1.2",
|
|
26
|
+
"@smithy/types": "^4.9.0",
|
|
26
27
|
"@testcontainers/localstack": "^10.15.0",
|
|
27
28
|
"@types/jest": "^29.5.0",
|
|
28
29
|
"@types/lodash": "^4.17.20",
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BatchGetItemCommand,
|
|
3
3
|
BatchGetItemCommandInput,
|
|
4
|
+
ConsumedCapacity,
|
|
4
5
|
DeleteItemCommand,
|
|
5
6
|
DynamoDBClient,
|
|
6
7
|
GetItemCommand,
|
|
7
8
|
paginateQuery,
|
|
8
9
|
PutItemCommand,
|
|
9
10
|
QueryCommandInput,
|
|
11
|
+
ReturnConsumedCapacity,
|
|
10
12
|
UpdateItemCommand,
|
|
11
13
|
} from "@aws-sdk/client-dynamodb";
|
|
12
14
|
import {marshall, unmarshall} from "@aws-sdk/util-dynamodb";
|
|
13
|
-
import {replace, uniqWith, isEqual, pickBy} from "lodash";
|
|
15
|
+
import {replace, uniqWith, isEqual, pickBy, get} from "lodash";
|
|
16
|
+
import {
|
|
17
|
+
HandlerExecutionContext,
|
|
18
|
+
InitializeHandler,
|
|
19
|
+
InitializeHandlerArguments,
|
|
20
|
+
InitializeHandlerOutput,
|
|
21
|
+
MetadataBearer
|
|
22
|
+
} from "@smithy/types";
|
|
14
23
|
|
|
15
24
|
const expressionAttributeKey = (key: string) => replace(key, /-/g, "_");
|
|
16
25
|
|
|
@@ -118,6 +127,35 @@ const paginate = <T>(array: Array<T>, pageSize: number) => {
|
|
|
118
127
|
}, [] as Array<Array<T>>);
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
export interface ConsumedCapacityDetail {
|
|
131
|
+
ReturnConsumedCapacity: ReturnConsumedCapacity | undefined
|
|
132
|
+
ConsumedCapacity: ConsumedCapacity | undefined
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ConsumedCapacityMiddlewareConfig {
|
|
136
|
+
onConsumedCapacity: (consumedCapacity: ConsumedCapacityDetail) => Promise<unknown>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const consumedCapacityMiddleware =
|
|
140
|
+
(consumedCapacityMiddlewareConfig: ConsumedCapacityMiddlewareConfig) =>
|
|
141
|
+
<Output extends MetadataBearer = MetadataBearer>(
|
|
142
|
+
next: InitializeHandler<any, Output>,
|
|
143
|
+
context: HandlerExecutionContext
|
|
144
|
+
): InitializeHandler<any, Output> =>
|
|
145
|
+
async (args: InitializeHandlerArguments<any>): Promise<InitializeHandlerOutput<Output>> => {
|
|
146
|
+
try {
|
|
147
|
+
const {input} = args;
|
|
148
|
+
const {ReturnConsumedCapacity} = input;
|
|
149
|
+
const response = await next(args);
|
|
150
|
+
const {output} = response;
|
|
151
|
+
const consumedCapacity = get(output, "ConsumedCapacity") as ConsumedCapacity | undefined;
|
|
152
|
+
await consumedCapacityMiddlewareConfig.onConsumedCapacity({ReturnConsumedCapacity, ConsumedCapacity: consumedCapacity});
|
|
153
|
+
return response;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
121
159
|
export class DynamoDbRepository<K, T> {
|
|
122
160
|
|
|
123
161
|
constructor(
|
|
@@ -125,6 +163,7 @@ export class DynamoDbRepository<K, T> {
|
|
|
125
163
|
private readonly tableName: string,
|
|
126
164
|
private readonly hashKey: string,
|
|
127
165
|
private readonly rangKey?: string,
|
|
166
|
+
private readonly returnConsumedCapacity: ReturnConsumedCapacity | undefined = ReturnConsumedCapacity.TOTAL,
|
|
128
167
|
) {
|
|
129
168
|
|
|
130
169
|
}
|
|
@@ -135,6 +174,7 @@ export class DynamoDbRepository<K, T> {
|
|
|
135
174
|
new GetItemCommand({
|
|
136
175
|
TableName: this.tableName,
|
|
137
176
|
Key: marshall(key, {removeUndefinedValues: true}),
|
|
177
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
138
178
|
}),
|
|
139
179
|
)
|
|
140
180
|
.then((result) =>
|
|
@@ -148,6 +188,7 @@ export class DynamoDbRepository<K, T> {
|
|
|
148
188
|
.send(
|
|
149
189
|
new PutItemCommand({
|
|
150
190
|
TableName: this.tableName,
|
|
191
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
151
192
|
Item,
|
|
152
193
|
}),
|
|
153
194
|
)
|
|
@@ -215,6 +256,7 @@ export class DynamoDbRepository<K, T> {
|
|
|
215
256
|
),
|
|
216
257
|
{removeUndefinedValues: true},
|
|
217
258
|
) : undefined,
|
|
259
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
218
260
|
};
|
|
219
261
|
return this.dynamoDBClient
|
|
220
262
|
.send(new UpdateItemCommand(updateItemCommandInput))
|
|
@@ -275,6 +317,7 @@ export class DynamoDbRepository<K, T> {
|
|
|
275
317
|
: {};
|
|
276
318
|
const queryCommandInput: QueryCommandInput = {
|
|
277
319
|
TableName: this.tableName,
|
|
320
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
278
321
|
IndexName: index,
|
|
279
322
|
KeyConditionExpression,
|
|
280
323
|
FilterExpression,
|
|
@@ -348,9 +391,10 @@ export class DynamoDbRepository<K, T> {
|
|
|
348
391
|
[this.tableName]: {
|
|
349
392
|
Keys: keyPage.map((key) => (marshall(key))),
|
|
350
393
|
ProjectionExpression,
|
|
351
|
-
ExpressionAttributeNames
|
|
394
|
+
ExpressionAttributeNames,
|
|
352
395
|
}
|
|
353
|
-
}
|
|
396
|
+
},
|
|
397
|
+
ReturnConsumedCapacity: this.returnConsumedCapacity,
|
|
354
398
|
}
|
|
355
399
|
return this.dynamoDBClient.send(new BatchGetItemCommand(batchRequest)).then(result =>
|
|
356
400
|
result.Responses?.[this.tableName].map((item) => unmarshall(item) as T));
|
package/test/repository.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import { DynamoDBClient, CreateTableCommand, DescribeTableCommand } from "@aws-sdk/client-dynamodb";
|
|
3
3
|
import { LocalstackContainer, StartedLocalStackContainer } from "@testcontainers/localstack";
|
|
4
|
-
import {DynamoDbRepository, FilterOperator} from "../src";
|
|
4
|
+
import {ConsumedCapacityDetail, consumedCapacityMiddleware, DynamoDbRepository, FilterOperator} from "../src";
|
|
5
5
|
|
|
6
6
|
describe('DynamoDbRepository Integration Tests', () => {
|
|
7
7
|
let container: StartedLocalStackContainer;
|
|
@@ -12,6 +12,11 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
12
12
|
const tableName = 'test-table';
|
|
13
13
|
const compositeTableName = 'test-composite-table';
|
|
14
14
|
const gsiTableName = 'test-gsi-table';
|
|
15
|
+
const consumedCapacityRegister = new Array<ConsumedCapacityDetail>() ;
|
|
16
|
+
|
|
17
|
+
const sumConsumedCapacity = () =>
|
|
18
|
+
consumedCapacityRegister.reduce((total, value) =>
|
|
19
|
+
total + (value.ConsumedCapacity?.CapacityUnits || 0), 0);
|
|
15
20
|
|
|
16
21
|
beforeAll(async () => {
|
|
17
22
|
// Start LocalStack container with DynamoDB
|
|
@@ -28,7 +33,10 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
28
33
|
},
|
|
29
34
|
});
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
dynamoDBClient.middlewareStack
|
|
37
|
+
.add(consumedCapacityMiddleware({onConsumedCapacity: async (consumedCapacity) => consumedCapacityRegister.push(consumedCapacity)}));
|
|
38
|
+
|
|
39
|
+
// Create the test table with a simple key
|
|
32
40
|
await dynamoDBClient.send(
|
|
33
41
|
new CreateTableCommand({
|
|
34
42
|
TableName: tableName,
|
|
@@ -42,7 +50,7 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
42
50
|
})
|
|
43
51
|
);
|
|
44
52
|
|
|
45
|
-
// Create the test table with composite key (partition key + sort key)
|
|
53
|
+
// Create the test table with a composite key (partition key + sort key)
|
|
46
54
|
await dynamoDBClient.send(
|
|
47
55
|
new CreateTableCommand({
|
|
48
56
|
TableName: compositeTableName,
|
|
@@ -108,6 +116,10 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
108
116
|
gsiRepository = new DynamoDbRepository(dynamoDBClient, gsiTableName, "userId", "itemId");
|
|
109
117
|
});
|
|
110
118
|
|
|
119
|
+
afterEach(async () => {
|
|
120
|
+
consumedCapacityRegister.splice(0, consumedCapacityRegister.length);
|
|
121
|
+
});
|
|
122
|
+
|
|
111
123
|
afterAll(async () => {
|
|
112
124
|
// Clean up
|
|
113
125
|
if (dynamoDBClient) {
|
|
@@ -122,6 +134,7 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
122
134
|
it('should return undefined when item does not exist', async () => {
|
|
123
135
|
const result = await repository.getItem({ id: 'non-existent' });
|
|
124
136
|
expect(result).toBeUndefined();
|
|
137
|
+
expect(sumConsumedCapacity()).toEqual(0.5);
|
|
125
138
|
});
|
|
126
139
|
|
|
127
140
|
it('should return the item when it exists', async () => {
|
|
@@ -132,6 +145,7 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
132
145
|
const result = await repository.getItem(key);
|
|
133
146
|
|
|
134
147
|
expect(result).toEqual(record);
|
|
148
|
+
expect(sumConsumedCapacity()).toEqual(2);
|
|
135
149
|
});
|
|
136
150
|
});
|
|
137
151
|
|
|
@@ -143,6 +157,7 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
143
157
|
const result = await repository.putItem(key, record);
|
|
144
158
|
|
|
145
159
|
expect(result).toEqual(record);
|
|
160
|
+
expect(sumConsumedCapacity()).toEqual(1.5);
|
|
146
161
|
});
|
|
147
162
|
});
|
|
148
163
|
|
|
@@ -155,6 +170,7 @@ describe('DynamoDbRepository Integration Tests', () => {
|
|
|
155
170
|
await repository.deleteItem(key);
|
|
156
171
|
const afterDelete = await repository.getItem(key);
|
|
157
172
|
expect(afterDelete).toBeUndefined();
|
|
173
|
+
expect(sumConsumedCapacity()).toEqual(2);
|
|
158
174
|
});
|
|
159
175
|
});
|
|
160
176
|
|