@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.
@@ -1,4 +1,4 @@
1
- name: Publish
1
+ name: Release
2
2
 
3
3
  on:
4
4
  push:
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.1.0",
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));
@@ -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
- // Create the test table with simple key
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