@twin.org/entity-storage-connector-dynamodb 0.0.2-next.9 → 0.0.3-next.10
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/README.md +7 -12
- package/dist/{esm/index.mjs → es/dynamoDbEntityStorageConnector.js} +314 -80
- package/dist/es/dynamoDbEntityStorageConnector.js.map +1 -0
- package/dist/es/index.js +6 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConfig.js +4 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConfig.js.map +1 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConstructorOptions.js +2 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConstructorOptions.js.map +1 -0
- package/dist/types/dynamoDbEntityStorageConnector.d.ts +46 -12
- package/dist/types/index.d.ts +3 -3
- package/dist/types/models/IDynamoDbEntityStorageConnectorConfig.d.ts +5 -1
- package/dist/types/models/IDynamoDbEntityStorageConnectorConstructorOptions.d.ts +5 -1
- package/docs/changelog.md +230 -36
- package/docs/examples.md +96 -1
- package/docs/reference/classes/DynamoDbEntityStorageConnector.md +162 -30
- package/docs/reference/interfaces/IDynamoDbEntityStorageConnectorConfig.md +19 -11
- package/docs/reference/interfaces/IDynamoDbEntityStorageConnectorConstructorOptions.md +12 -4
- package/locales/en.json +16 -2
- package/package.json +17 -14
- package/dist/cjs/index.cjs +0 -751
|
@@ -1,39 +1,44 @@
|
|
|
1
|
-
import { waitUntilTableExists, DynamoDB, QueryCommand } from '@aws-sdk/client-dynamodb';
|
|
2
|
-
import { GetCommand, PutCommand, DeleteCommand, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
-
import { unmarshall } from '@aws-sdk/util-dynamodb';
|
|
4
|
-
import { Guards, Is, ComponentFactory, BaseError, GeneralError, Coerce, ObjectHelper, Converter } from '@twin.org/core';
|
|
5
|
-
import { EntitySchemaFactory, EntitySchemaHelper, ComparisonOperator, LogicalOperator, SortDirection } from '@twin.org/entity';
|
|
6
|
-
|
|
7
1
|
// Copyright 2024 IOTA Stiftung.
|
|
8
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
|
+
import { DynamoDB, QueryCommand, waitUntilTableExists } from "@aws-sdk/client-dynamodb";
|
|
4
|
+
import { BatchWriteCommand, DeleteCommand, DynamoDBDocumentClient, GetCommand, PutCommand, ScanCommand } from "@aws-sdk/lib-dynamodb";
|
|
5
|
+
import { unmarshall } from "@aws-sdk/util-dynamodb";
|
|
6
|
+
import { ContextIdHelper, ContextIdStore } from "@twin.org/context";
|
|
7
|
+
import { BaseError, Coerce, ComponentFactory, Converter, GeneralError, Guards, HealthStatus, Is, ObjectHelper } from "@twin.org/core";
|
|
8
|
+
import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, LogicalOperator, SortDirection } from "@twin.org/entity";
|
|
9
9
|
/**
|
|
10
10
|
* Class for performing entity storage operations using Dynamo DB.
|
|
11
11
|
*/
|
|
12
|
-
class DynamoDbEntityStorageConnector {
|
|
12
|
+
export class DynamoDbEntityStorageConnector {
|
|
13
|
+
/**
|
|
14
|
+
* Runtime name for the class.
|
|
15
|
+
*/
|
|
16
|
+
static CLASS_NAME = "DynamoDbEntityStorageConnector";
|
|
13
17
|
/**
|
|
14
18
|
* Limit the number of entities when finding.
|
|
15
19
|
* @internal
|
|
16
20
|
*/
|
|
17
|
-
static
|
|
21
|
+
static _DEFAULT_LIMIT = 40;
|
|
18
22
|
/**
|
|
19
23
|
* Partition id field name.
|
|
20
24
|
* @internal
|
|
21
25
|
*/
|
|
22
|
-
static
|
|
26
|
+
static _PARTITION_KEY = "partitionId";
|
|
23
27
|
/**
|
|
24
28
|
* Partition id field value.
|
|
25
29
|
* @internal
|
|
26
30
|
*/
|
|
27
|
-
static
|
|
28
|
-
/**
|
|
29
|
-
* Runtime name for the class.
|
|
30
|
-
*/
|
|
31
|
-
CLASS_NAME = "DynamoDbEntityStorageConnector";
|
|
31
|
+
static _PARTITION_KEY_VALUE = "root";
|
|
32
32
|
/**
|
|
33
33
|
* The schema for the entity.
|
|
34
34
|
* @internal
|
|
35
35
|
*/
|
|
36
36
|
_entitySchema;
|
|
37
|
+
/**
|
|
38
|
+
* The keys to use from the context ids to create partitions.
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
_partitionContextIds;
|
|
37
42
|
/**
|
|
38
43
|
* The primary key.
|
|
39
44
|
* @internal
|
|
@@ -49,23 +54,65 @@ class DynamoDbEntityStorageConnector {
|
|
|
49
54
|
* @param options The options for the connector.
|
|
50
55
|
*/
|
|
51
56
|
constructor(options) {
|
|
52
|
-
Guards.object(
|
|
53
|
-
Guards.stringValue(
|
|
54
|
-
Guards.object(
|
|
57
|
+
Guards.object(DynamoDbEntityStorageConnector.CLASS_NAME, "options", options);
|
|
58
|
+
Guards.stringValue(DynamoDbEntityStorageConnector.CLASS_NAME, "options.entitySchema", options.entitySchema);
|
|
59
|
+
Guards.object(DynamoDbEntityStorageConnector.CLASS_NAME, "options.config", options.config);
|
|
55
60
|
options.config.authMode ??= "credentials";
|
|
56
61
|
if (options.config.authMode === "credentials") {
|
|
57
|
-
Guards.stringValue(
|
|
58
|
-
Guards.stringValue(
|
|
62
|
+
Guards.stringValue(DynamoDbEntityStorageConnector.CLASS_NAME, "options.config.accessKeyId", options.config.accessKeyId);
|
|
63
|
+
Guards.stringValue(DynamoDbEntityStorageConnector.CLASS_NAME, "options.config.secretAccessKey", options.config.secretAccessKey);
|
|
59
64
|
}
|
|
60
|
-
Guards.stringValue(
|
|
61
|
-
Guards.stringValue(
|
|
65
|
+
Guards.stringValue(DynamoDbEntityStorageConnector.CLASS_NAME, "options.config.region", options.config.region);
|
|
66
|
+
Guards.stringValue(DynamoDbEntityStorageConnector.CLASS_NAME, "options.config.tableName", options.config.tableName);
|
|
62
67
|
this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
|
|
68
|
+
this._partitionContextIds = options.partitionContextIds;
|
|
63
69
|
this._primaryKey = EntitySchemaHelper.getPrimaryKey(this._entitySchema);
|
|
64
70
|
this._config = options.config;
|
|
65
71
|
this._config.endpoint = Is.stringValue(this._config.endpoint)
|
|
66
72
|
? this._config.endpoint
|
|
67
73
|
: undefined;
|
|
68
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Returns the class name of the component.
|
|
77
|
+
* @returns The class name of the component.
|
|
78
|
+
*/
|
|
79
|
+
className() {
|
|
80
|
+
return DynamoDbEntityStorageConnector.CLASS_NAME;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns the health status of the component.
|
|
84
|
+
* @returns The health status of the component.
|
|
85
|
+
*/
|
|
86
|
+
async health() {
|
|
87
|
+
try {
|
|
88
|
+
const dbConnection = this.createConnection();
|
|
89
|
+
await dbConnection.describeTable({ TableName: this._config.tableName });
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
93
|
+
status: HealthStatus.Ok,
|
|
94
|
+
description: "healthDescription"
|
|
95
|
+
}
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [
|
|
100
|
+
{
|
|
101
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
102
|
+
status: HealthStatus.Error,
|
|
103
|
+
description: "healthDescription",
|
|
104
|
+
message: "connectionFailed"
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get the schema for the entities.
|
|
111
|
+
* @returns The schema for the entities.
|
|
112
|
+
*/
|
|
113
|
+
getSchema() {
|
|
114
|
+
return this._entitySchema;
|
|
115
|
+
}
|
|
69
116
|
/**
|
|
70
117
|
* Bootstrap the component by creating and initializing any resources it needs.
|
|
71
118
|
* @param nodeLoggingComponentType The node logging component type.
|
|
@@ -76,7 +123,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
76
123
|
if (!(await this.tableExists(this._config.tableName))) {
|
|
77
124
|
await nodeLogging?.log({
|
|
78
125
|
level: "info",
|
|
79
|
-
source:
|
|
126
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
80
127
|
ts: Date.now(),
|
|
81
128
|
message: "tableCreating",
|
|
82
129
|
data: {
|
|
@@ -97,11 +144,11 @@ class DynamoDbEntityStorageConnector {
|
|
|
97
144
|
// We always add a partition key to the table as a non optional hash key
|
|
98
145
|
// is always required when querying using sort parameters
|
|
99
146
|
tableParams.AttributeDefinitions?.push({
|
|
100
|
-
AttributeName: DynamoDbEntityStorageConnector.
|
|
147
|
+
AttributeName: DynamoDbEntityStorageConnector._PARTITION_KEY,
|
|
101
148
|
AttributeType: "S"
|
|
102
149
|
});
|
|
103
150
|
tableParams.KeySchema?.push({
|
|
104
|
-
AttributeName: DynamoDbEntityStorageConnector.
|
|
151
|
+
AttributeName: DynamoDbEntityStorageConnector._PARTITION_KEY,
|
|
105
152
|
KeyType: "HASH"
|
|
106
153
|
});
|
|
107
154
|
const gsi = [];
|
|
@@ -128,7 +175,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
128
175
|
IndexName: `${prop.property}Index`,
|
|
129
176
|
KeySchema: [
|
|
130
177
|
{
|
|
131
|
-
AttributeName: DynamoDbEntityStorageConnector.
|
|
178
|
+
AttributeName: DynamoDbEntityStorageConnector._PARTITION_KEY,
|
|
132
179
|
KeyType: "HASH"
|
|
133
180
|
},
|
|
134
181
|
{
|
|
@@ -160,7 +207,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
160
207
|
});
|
|
161
208
|
await nodeLogging?.log({
|
|
162
209
|
level: "info",
|
|
163
|
-
source:
|
|
210
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
164
211
|
ts: Date.now(),
|
|
165
212
|
message: "tableCreated",
|
|
166
213
|
data: {
|
|
@@ -172,7 +219,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
172
219
|
if (BaseError.isErrorCode(err, "ResourceInUseException")) {
|
|
173
220
|
await nodeLogging?.log({
|
|
174
221
|
level: "info",
|
|
175
|
-
source:
|
|
222
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
176
223
|
ts: Date.now(),
|
|
177
224
|
message: "tableExists",
|
|
178
225
|
data: {
|
|
@@ -183,7 +230,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
183
230
|
else {
|
|
184
231
|
await nodeLogging?.log({
|
|
185
232
|
level: "error",
|
|
186
|
-
source:
|
|
233
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
187
234
|
ts: Date.now(),
|
|
188
235
|
message: "tableCreateFailed",
|
|
189
236
|
error: BaseError.fromError(err),
|
|
@@ -198,7 +245,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
198
245
|
else {
|
|
199
246
|
await nodeLogging?.log({
|
|
200
247
|
level: "info",
|
|
201
|
-
source:
|
|
248
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
202
249
|
ts: Date.now(),
|
|
203
250
|
message: "tableExists",
|
|
204
251
|
data: {
|
|
@@ -208,13 +255,6 @@ class DynamoDbEntityStorageConnector {
|
|
|
208
255
|
}
|
|
209
256
|
return true;
|
|
210
257
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Get the schema for the entities.
|
|
213
|
-
* @returns The schema for the entities.
|
|
214
|
-
*/
|
|
215
|
-
getSchema() {
|
|
216
|
-
return this._entitySchema;
|
|
217
|
-
}
|
|
218
258
|
/**
|
|
219
259
|
* Get an entity.
|
|
220
260
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
@@ -223,19 +263,21 @@ class DynamoDbEntityStorageConnector {
|
|
|
223
263
|
* @returns The object if it can be found or undefined.
|
|
224
264
|
*/
|
|
225
265
|
async get(id, secondaryIndex, conditions) {
|
|
226
|
-
Guards.stringValue(
|
|
266
|
+
Guards.stringValue(DynamoDbEntityStorageConnector.CLASS_NAME, "id", id);
|
|
267
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
268
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
227
269
|
try {
|
|
228
270
|
const docClient = this.createDocClient();
|
|
229
271
|
if (Is.empty(secondaryIndex) && Is.empty(conditions)) {
|
|
230
272
|
const getCommand = new GetCommand({
|
|
231
273
|
TableName: this._config.tableName,
|
|
232
274
|
Key: {
|
|
233
|
-
[DynamoDbEntityStorageConnector.
|
|
275
|
+
[DynamoDbEntityStorageConnector._PARTITION_KEY]: partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
234
276
|
[this._primaryKey.property]: id
|
|
235
277
|
}
|
|
236
278
|
});
|
|
237
279
|
const response = await docClient.send(getCommand);
|
|
238
|
-
delete response.Item?.[DynamoDbEntityStorageConnector.
|
|
280
|
+
delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_KEY];
|
|
239
281
|
return response.Item;
|
|
240
282
|
}
|
|
241
283
|
const finalConditions = {
|
|
@@ -257,16 +299,16 @@ class DynamoDbEntityStorageConnector {
|
|
|
257
299
|
});
|
|
258
300
|
}
|
|
259
301
|
}
|
|
260
|
-
const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex);
|
|
302
|
+
const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex, partitionKey);
|
|
261
303
|
return queryResult.entities[0];
|
|
262
304
|
}
|
|
263
305
|
catch (err) {
|
|
264
306
|
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
265
|
-
throw new GeneralError(
|
|
266
|
-
|
|
307
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "tableDoesNotExist", {
|
|
308
|
+
tableName: this._config.tableName
|
|
267
309
|
}, err);
|
|
268
310
|
}
|
|
269
|
-
throw new GeneralError(
|
|
311
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "getFailed", {
|
|
270
312
|
id
|
|
271
313
|
}, err);
|
|
272
314
|
}
|
|
@@ -278,7 +320,9 @@ class DynamoDbEntityStorageConnector {
|
|
|
278
320
|
* @returns The id of the entity.
|
|
279
321
|
*/
|
|
280
322
|
async set(entity, conditions) {
|
|
281
|
-
Guards.object(
|
|
323
|
+
Guards.object(DynamoDbEntityStorageConnector.CLASS_NAME, "entity", entity);
|
|
324
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
325
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
282
326
|
EntitySchemaHelper.validateEntity(entity, this.getSchema());
|
|
283
327
|
const id = entity[this._primaryKey.property];
|
|
284
328
|
try {
|
|
@@ -287,7 +331,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
287
331
|
const putCommand = new PutCommand({
|
|
288
332
|
TableName: this._config.tableName,
|
|
289
333
|
Item: {
|
|
290
|
-
[DynamoDbEntityStorageConnector.
|
|
334
|
+
[DynamoDbEntityStorageConnector._PARTITION_KEY]: partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
291
335
|
...entity
|
|
292
336
|
},
|
|
293
337
|
// Only set the condition expression if we have conditions to match
|
|
@@ -305,15 +349,100 @@ class DynamoDbEntityStorageConnector {
|
|
|
305
349
|
return;
|
|
306
350
|
}
|
|
307
351
|
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
308
|
-
throw new GeneralError(
|
|
352
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "tableDoesNotExist", {
|
|
309
353
|
tableName: this._config.tableName
|
|
310
354
|
}, err);
|
|
311
355
|
}
|
|
312
|
-
throw new GeneralError(
|
|
356
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "setFailed", {
|
|
313
357
|
id
|
|
314
358
|
}, err);
|
|
315
359
|
}
|
|
316
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Set multiple entities in a batch.
|
|
363
|
+
* @param entities The entities to set.
|
|
364
|
+
* @returns Nothing.
|
|
365
|
+
*/
|
|
366
|
+
async setBatch(entities) {
|
|
367
|
+
Guards.arrayValue(DynamoDbEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
368
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
369
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
370
|
+
for (const entity of entities) {
|
|
371
|
+
EntitySchemaHelper.validateEntity(entity, this.getSchema());
|
|
372
|
+
}
|
|
373
|
+
try {
|
|
374
|
+
const docClient = this.createDocClient();
|
|
375
|
+
const chunkSize = 25;
|
|
376
|
+
for (let i = 0; i < entities.length; i += chunkSize) {
|
|
377
|
+
const chunk = entities.slice(i, i + chunkSize);
|
|
378
|
+
await docClient.send(new BatchWriteCommand({
|
|
379
|
+
RequestItems: {
|
|
380
|
+
[this._config.tableName]: chunk.map(entity => ({
|
|
381
|
+
PutRequest: {
|
|
382
|
+
Item: {
|
|
383
|
+
[DynamoDbEntityStorageConnector._PARTITION_KEY]: partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
384
|
+
...entity
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}))
|
|
388
|
+
}
|
|
389
|
+
}));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
catch (err) {
|
|
393
|
+
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
394
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "tableDoesNotExist", { tableName: this._config.tableName }, err);
|
|
395
|
+
}
|
|
396
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Empty the entity storage.
|
|
401
|
+
* @returns Nothing.
|
|
402
|
+
*/
|
|
403
|
+
async empty() {
|
|
404
|
+
try {
|
|
405
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
406
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
407
|
+
const pKey = partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE;
|
|
408
|
+
const docClient = this.createDocClient();
|
|
409
|
+
const chunkSize = 25;
|
|
410
|
+
let exclusiveStartKey;
|
|
411
|
+
do {
|
|
412
|
+
const scanResult = await docClient.send(new ScanCommand({
|
|
413
|
+
TableName: this._config.tableName,
|
|
414
|
+
FilterExpression: "#partitionId = :partitionId",
|
|
415
|
+
ExpressionAttributeNames: {
|
|
416
|
+
"#partitionId": DynamoDbEntityStorageConnector._PARTITION_KEY
|
|
417
|
+
},
|
|
418
|
+
ExpressionAttributeValues: {
|
|
419
|
+
":partitionId": pKey
|
|
420
|
+
},
|
|
421
|
+
ExclusiveStartKey: exclusiveStartKey
|
|
422
|
+
}));
|
|
423
|
+
const items = scanResult.Items ?? [];
|
|
424
|
+
for (let i = 0; i < items.length; i += chunkSize) {
|
|
425
|
+
const chunk = items.slice(i, i + chunkSize);
|
|
426
|
+
await docClient.send(new BatchWriteCommand({
|
|
427
|
+
RequestItems: {
|
|
428
|
+
[this._config.tableName]: chunk.map((item) => ({
|
|
429
|
+
DeleteRequest: {
|
|
430
|
+
Key: {
|
|
431
|
+
[DynamoDbEntityStorageConnector._PARTITION_KEY]: item[DynamoDbEntityStorageConnector._PARTITION_KEY],
|
|
432
|
+
[this._primaryKey.property]: item[this._primaryKey.property]
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}))
|
|
436
|
+
}
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
exclusiveStartKey = scanResult.LastEvaluatedKey;
|
|
440
|
+
} while (exclusiveStartKey);
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
317
446
|
/**
|
|
318
447
|
* Remove the entity.
|
|
319
448
|
* @param id The id of the entity to remove.
|
|
@@ -321,14 +450,16 @@ class DynamoDbEntityStorageConnector {
|
|
|
321
450
|
* @returns Nothing.
|
|
322
451
|
*/
|
|
323
452
|
async remove(id, conditions) {
|
|
324
|
-
Guards.stringValue(
|
|
453
|
+
Guards.stringValue(DynamoDbEntityStorageConnector.CLASS_NAME, "id", id);
|
|
454
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
455
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
325
456
|
try {
|
|
326
457
|
const docClient = this.createDocClient();
|
|
327
458
|
const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
|
|
328
459
|
const deleteCommand = new DeleteCommand({
|
|
329
460
|
TableName: this._config.tableName,
|
|
330
461
|
Key: {
|
|
331
|
-
[DynamoDbEntityStorageConnector.
|
|
462
|
+
[DynamoDbEntityStorageConnector._PARTITION_KEY]: partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE,
|
|
332
463
|
[this._primaryKey.property]: id
|
|
333
464
|
},
|
|
334
465
|
ConditionExpression: conditionExpression,
|
|
@@ -342,38 +473,134 @@ class DynamoDbEntityStorageConnector {
|
|
|
342
473
|
return;
|
|
343
474
|
}
|
|
344
475
|
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
345
|
-
throw new GeneralError(
|
|
346
|
-
|
|
476
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "tableDoesNotExist", {
|
|
477
|
+
tableName: this._config.tableName
|
|
347
478
|
}, err);
|
|
348
479
|
}
|
|
349
|
-
throw new GeneralError(
|
|
480
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "removeFailed", {
|
|
350
481
|
id
|
|
351
482
|
}, err);
|
|
352
483
|
}
|
|
353
484
|
}
|
|
485
|
+
/**
|
|
486
|
+
* Remove multiple entities by their IDs in a batch.
|
|
487
|
+
* @param ids The ids of the entities to remove.
|
|
488
|
+
* @returns Nothing.
|
|
489
|
+
*/
|
|
490
|
+
async removeBatch(ids) {
|
|
491
|
+
Guards.arrayValue(DynamoDbEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
492
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
493
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
494
|
+
try {
|
|
495
|
+
const docClient = this.createDocClient();
|
|
496
|
+
const chunkSize = 25;
|
|
497
|
+
const primaryKeyProperty = this._primaryKey.property;
|
|
498
|
+
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
499
|
+
const chunk = ids.slice(i, i + chunkSize);
|
|
500
|
+
await docClient.send(new BatchWriteCommand({
|
|
501
|
+
RequestItems: {
|
|
502
|
+
[this._config.tableName]: chunk.map(id => ({
|
|
503
|
+
DeleteRequest: {
|
|
504
|
+
Key: {
|
|
505
|
+
[primaryKeyProperty]: id,
|
|
506
|
+
[DynamoDbEntityStorageConnector._PARTITION_KEY]: partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}))
|
|
510
|
+
}
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Teardown the entity storage by deleting the underlying table.
|
|
520
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
521
|
+
* @returns True if the teardown process was successful.
|
|
522
|
+
*/
|
|
523
|
+
async teardown(nodeLoggingComponentType) {
|
|
524
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
525
|
+
await nodeLogging?.log({
|
|
526
|
+
level: "info",
|
|
527
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
528
|
+
ts: Date.now(),
|
|
529
|
+
message: "tableDeleting",
|
|
530
|
+
data: { tableName: this._config.tableName }
|
|
531
|
+
});
|
|
532
|
+
try {
|
|
533
|
+
const dbConnection = this.createConnection();
|
|
534
|
+
await dbConnection.deleteTable({ TableName: this._config.tableName });
|
|
535
|
+
await nodeLogging?.log({
|
|
536
|
+
level: "info",
|
|
537
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
538
|
+
ts: Date.now(),
|
|
539
|
+
message: "tableDeleted",
|
|
540
|
+
data: { tableName: this._config.tableName }
|
|
541
|
+
});
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
await nodeLogging?.log({
|
|
546
|
+
level: "error",
|
|
547
|
+
source: DynamoDbEntityStorageConnector.CLASS_NAME,
|
|
548
|
+
ts: Date.now(),
|
|
549
|
+
message: "teardownFailed",
|
|
550
|
+
error: BaseError.fromError(err)
|
|
551
|
+
});
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
354
555
|
/**
|
|
355
556
|
* Find all the entities which match the conditions.
|
|
356
557
|
* @param conditions The conditions to match for the entities.
|
|
357
558
|
* @param sortProperties The optional sort order.
|
|
358
559
|
* @param properties The optional properties to return, defaults to all.
|
|
359
|
-
* @param cursor The cursor to request the next
|
|
360
|
-
* @param
|
|
560
|
+
* @param cursor The cursor to request the next chunk of entities.
|
|
561
|
+
* @param limit The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
361
562
|
* @returns All the entities for the storage matching the conditions,
|
|
362
563
|
* and a cursor which can be used to request more entities.
|
|
363
564
|
*/
|
|
364
|
-
async query(conditions, sortProperties, properties, cursor,
|
|
365
|
-
|
|
565
|
+
async query(conditions, sortProperties, properties, cursor, limit) {
|
|
566
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
567
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
568
|
+
return this.internalQuery(conditions, sortProperties, properties, cursor, limit, undefined, partitionKey);
|
|
366
569
|
}
|
|
367
570
|
/**
|
|
368
|
-
*
|
|
369
|
-
* @returns
|
|
571
|
+
* Count all the entities which match the conditions.
|
|
572
|
+
* @returns The total count of entities in the storage.
|
|
370
573
|
*/
|
|
371
|
-
async
|
|
574
|
+
async count() {
|
|
372
575
|
try {
|
|
576
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
577
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
373
578
|
const dbConnection = this.createConnection();
|
|
374
|
-
|
|
579
|
+
let total = 0;
|
|
580
|
+
let exclusiveStartKey;
|
|
581
|
+
do {
|
|
582
|
+
const result = await dbConnection.send(new QueryCommand({
|
|
583
|
+
TableName: this._config.tableName,
|
|
584
|
+
Select: "COUNT",
|
|
585
|
+
KeyConditionExpression: "#partitionId = :partitionId",
|
|
586
|
+
ExpressionAttributeNames: {
|
|
587
|
+
"#partitionId": DynamoDbEntityStorageConnector._PARTITION_KEY
|
|
588
|
+
},
|
|
589
|
+
ExpressionAttributeValues: {
|
|
590
|
+
":partitionId": {
|
|
591
|
+
S: partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
ExclusiveStartKey: exclusiveStartKey
|
|
595
|
+
}));
|
|
596
|
+
total += result.Count ?? 0;
|
|
597
|
+
exclusiveStartKey = result.LastEvaluatedKey;
|
|
598
|
+
} while (exclusiveStartKey);
|
|
599
|
+
return total;
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
|
|
375
603
|
}
|
|
376
|
-
catch { }
|
|
377
604
|
}
|
|
378
605
|
/**
|
|
379
606
|
* Create the parameters for a query.
|
|
@@ -492,7 +719,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
492
719
|
else if (comparator.comparison === ComparisonOperator.In) {
|
|
493
720
|
return `${propName} IN ${attributeName}`;
|
|
494
721
|
}
|
|
495
|
-
throw new GeneralError(
|
|
722
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "comparisonNotSupported", {
|
|
496
723
|
comparison: comparator.comparison
|
|
497
724
|
});
|
|
498
725
|
}
|
|
@@ -529,7 +756,9 @@ class DynamoDbEntityStorageConnector {
|
|
|
529
756
|
else if (operator === LogicalOperator.Or) {
|
|
530
757
|
return "OR";
|
|
531
758
|
}
|
|
532
|
-
throw new GeneralError(
|
|
759
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "conditionalNotSupported", {
|
|
760
|
+
operator
|
|
761
|
+
});
|
|
533
762
|
}
|
|
534
763
|
/**
|
|
535
764
|
* Format a value to insert into DB.
|
|
@@ -585,6 +814,9 @@ class DynamoDbEntityStorageConnector {
|
|
|
585
814
|
* @internal
|
|
586
815
|
*/
|
|
587
816
|
createConnectionConfig() {
|
|
817
|
+
const requestHandler = {
|
|
818
|
+
requestTimeout: this._config.connectionTimeoutMs
|
|
819
|
+
};
|
|
588
820
|
if (Is.stringValue(this._config.secretAccessKey) &&
|
|
589
821
|
Is.stringValue(this._config.accessKeyId) &&
|
|
590
822
|
this._config.authMode === "credentials") {
|
|
@@ -594,12 +826,14 @@ class DynamoDbEntityStorageConnector {
|
|
|
594
826
|
secretAccessKey: this._config.secretAccessKey
|
|
595
827
|
},
|
|
596
828
|
endpoint: this._config.endpoint,
|
|
597
|
-
region: this._config.region
|
|
829
|
+
region: this._config.region,
|
|
830
|
+
requestHandler
|
|
598
831
|
};
|
|
599
832
|
}
|
|
600
833
|
return {
|
|
601
834
|
endpoint: this._config.endpoint,
|
|
602
|
-
region: this._config.region
|
|
835
|
+
region: this._config.region,
|
|
836
|
+
requestHandler
|
|
603
837
|
};
|
|
604
838
|
}
|
|
605
839
|
/**
|
|
@@ -623,16 +857,17 @@ class DynamoDbEntityStorageConnector {
|
|
|
623
857
|
* @param conditions The conditions to match for the entities.
|
|
624
858
|
* @param sortProperties The optional sort order.
|
|
625
859
|
* @param properties The optional properties to return, defaults to all.
|
|
626
|
-
* @param cursor The cursor to request the next
|
|
627
|
-
* @param
|
|
860
|
+
* @param cursor The cursor to request the next chunk of entities.
|
|
861
|
+
* @param limit The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
628
862
|
* @param secondaryIndex The secondary index to use for the query.
|
|
863
|
+
* @param partitionKey The partition key to use for the query.
|
|
629
864
|
* @returns All the entities for the storage matching the conditions,
|
|
630
865
|
* and a cursor which can be used to request more entities.
|
|
631
866
|
* @internal
|
|
632
867
|
*/
|
|
633
|
-
async internalQuery(conditions, sortProperties, properties, cursor,
|
|
868
|
+
async internalQuery(conditions, sortProperties, properties, cursor, limit, secondaryIndex, partitionKey) {
|
|
634
869
|
try {
|
|
635
|
-
const returnSize =
|
|
870
|
+
const returnSize = limit ?? DynamoDbEntityStorageConnector._DEFAULT_LIMIT;
|
|
636
871
|
let indexName = Is.stringValue(secondaryIndex)
|
|
637
872
|
? `${secondaryIndex}Index`
|
|
638
873
|
: undefined;
|
|
@@ -641,7 +876,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
641
876
|
let scanAscending = true;
|
|
642
877
|
if (Is.arrayValue(sortProperties)) {
|
|
643
878
|
if (sortProperties.length > 1) {
|
|
644
|
-
throw new GeneralError(
|
|
879
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "sortSingle");
|
|
645
880
|
}
|
|
646
881
|
for (const sortProperty of sortProperties) {
|
|
647
882
|
const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
|
|
@@ -649,7 +884,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
649
884
|
(!propertySchema.isPrimary &&
|
|
650
885
|
!propertySchema.isSecondary &&
|
|
651
886
|
Is.empty(propertySchema.sortDirection))) {
|
|
652
|
-
throw new GeneralError(
|
|
887
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "sortNotIndexed", {
|
|
653
888
|
property: sortProperty.property
|
|
654
889
|
});
|
|
655
890
|
}
|
|
@@ -661,8 +896,8 @@ class DynamoDbEntityStorageConnector {
|
|
|
661
896
|
}
|
|
662
897
|
const attributeNames = { "#partitionId": "partitionId" };
|
|
663
898
|
const attributeValues = {
|
|
664
|
-
[`:${DynamoDbEntityStorageConnector.
|
|
665
|
-
S: DynamoDbEntityStorageConnector.
|
|
899
|
+
[`:${DynamoDbEntityStorageConnector._PARTITION_KEY}`]: {
|
|
900
|
+
S: partitionKey ?? DynamoDbEntityStorageConnector._PARTITION_KEY_VALUE
|
|
666
901
|
}
|
|
667
902
|
};
|
|
668
903
|
const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues, secondaryIndex);
|
|
@@ -692,7 +927,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
692
927
|
if (Is.arrayValue(results.Items)) {
|
|
693
928
|
entities = results.Items.map(item => {
|
|
694
929
|
const unmarshalled = unmarshall(item);
|
|
695
|
-
delete unmarshalled[DynamoDbEntityStorageConnector.
|
|
930
|
+
delete unmarshalled[DynamoDbEntityStorageConnector._PARTITION_KEY];
|
|
696
931
|
return unmarshalled;
|
|
697
932
|
});
|
|
698
933
|
}
|
|
@@ -705,11 +940,11 @@ class DynamoDbEntityStorageConnector {
|
|
|
705
940
|
}
|
|
706
941
|
catch (err) {
|
|
707
942
|
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
708
|
-
throw new GeneralError(
|
|
709
|
-
|
|
943
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "tableDoesNotExist", {
|
|
944
|
+
tableName: this._config.tableName
|
|
710
945
|
}, err);
|
|
711
946
|
}
|
|
712
|
-
throw new GeneralError(
|
|
947
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "queryFailed", undefined, err);
|
|
713
948
|
}
|
|
714
949
|
}
|
|
715
950
|
/**
|
|
@@ -728,7 +963,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
728
963
|
for (const c of conditions) {
|
|
729
964
|
const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
|
|
730
965
|
if (Is.undefined(schemaProp)) {
|
|
731
|
-
throw new GeneralError(
|
|
966
|
+
throw new GeneralError(DynamoDbEntityStorageConnector.CLASS_NAME, "propertyNotFound", {
|
|
732
967
|
property: c.property
|
|
733
968
|
});
|
|
734
969
|
}
|
|
@@ -745,5 +980,4 @@ class DynamoDbEntityStorageConnector {
|
|
|
745
980
|
return { conditionExpression, attributeNames, attributeValues };
|
|
746
981
|
}
|
|
747
982
|
}
|
|
748
|
-
|
|
749
|
-
export { DynamoDbEntityStorageConnector };
|
|
983
|
+
//# sourceMappingURL=dynamoDbEntityStorageConnector.js.map
|