@twin.org/entity-storage-connector-dynamodb 0.0.1-next.2 → 0.0.1-next.21
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 +1 -1
- package/dist/cjs/index.cjs +193 -109
- package/dist/esm/index.mjs +196 -112
- package/dist/types/dynamoDbEntityStorageConnector.d.ts +23 -13
- package/dist/types/index.d.ts +1 -0
- package/dist/types/models/IDynamoDbEntityStorageConnectorConstructorOptions.d.ts +18 -0
- package/docs/changelog.md +1 -1
- package/docs/reference/classes/DynamoDbEntityStorageConnector.md +74 -40
- package/docs/reference/index.md +1 -0
- package/docs/reference/interfaces/IDynamoDbEntityStorageConnectorConstructorOptions.md +27 -0
- package/locales/en.json +1 -1
- package/package.json +9 -39
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ npm install @twin.org/entity-storage-connector-dynamodb
|
|
|
13
13
|
The tests developed are functional tests and need an instance of DynamoDB up and running. To run DynamoDB locally:
|
|
14
14
|
|
|
15
15
|
```sh
|
|
16
|
-
docker run -p 8000:8000 --name dynamodb --hostname dynamodb -d amazon/dynamodb-local
|
|
16
|
+
docker run -p 8000:8000 --name twin-entity-storage-dynamodb --hostname dynamodb -d amazon/dynamodb-local
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Afterwards you can run the tests as follows:
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -50,9 +50,6 @@ class DynamoDbEntityStorageConnector {
|
|
|
50
50
|
/**
|
|
51
51
|
* Create a new instance of DynamoDbEntityStorageConnector.
|
|
52
52
|
* @param options The options for the connector.
|
|
53
|
-
* @param options.entitySchema The schema for the entity.
|
|
54
|
-
* @param options.loggingConnectorType The type of logging connector to use, defaults to no logging.
|
|
55
|
-
* @param options.config The configuration for the connector.
|
|
56
53
|
*/
|
|
57
54
|
constructor(options) {
|
|
58
55
|
core.Guards.object(this.CLASS_NAME, "options", options);
|
|
@@ -214,17 +211,25 @@ class DynamoDbEntityStorageConnector {
|
|
|
214
211
|
}
|
|
215
212
|
return true;
|
|
216
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Get the schema for the entities.
|
|
216
|
+
* @returns The schema for the entities.
|
|
217
|
+
*/
|
|
218
|
+
getSchema() {
|
|
219
|
+
return this._entitySchema;
|
|
220
|
+
}
|
|
217
221
|
/**
|
|
218
222
|
* Get an entity.
|
|
219
223
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
220
224
|
* @param secondaryIndex Get the item using a secondary index.
|
|
225
|
+
* @param conditions The optional conditions to match for the entities.
|
|
221
226
|
* @returns The object if it can be found or undefined.
|
|
222
227
|
*/
|
|
223
|
-
async get(id, secondaryIndex) {
|
|
228
|
+
async get(id, secondaryIndex, conditions) {
|
|
224
229
|
core.Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
225
230
|
try {
|
|
226
231
|
const docClient = this.createDocClient();
|
|
227
|
-
if (core.Is.
|
|
232
|
+
if (core.Is.empty(secondaryIndex) && core.Is.empty(conditions)) {
|
|
228
233
|
const getCommand = new libDynamodb.GetCommand({
|
|
229
234
|
TableName: this._config.tableName,
|
|
230
235
|
Key: {
|
|
@@ -236,27 +241,27 @@ class DynamoDbEntityStorageConnector {
|
|
|
236
241
|
delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
|
|
237
242
|
return response.Item;
|
|
238
243
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
244
|
+
const finalConditions = {
|
|
245
|
+
conditions: []
|
|
246
|
+
};
|
|
247
|
+
if (core.Is.stringValue(secondaryIndex)) {
|
|
248
|
+
finalConditions.conditions.push({
|
|
249
|
+
property: secondaryIndex,
|
|
250
|
+
comparison: entity.ComparisonOperator.Equals,
|
|
251
|
+
value: id
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
if (core.Is.arrayValue(conditions)) {
|
|
255
|
+
for (const c of conditions) {
|
|
256
|
+
finalConditions.conditions.push({
|
|
257
|
+
property: c.property,
|
|
258
|
+
comparison: entity.ComparisonOperator.Equals,
|
|
259
|
+
value: c.value
|
|
260
|
+
});
|
|
254
261
|
}
|
|
255
|
-
});
|
|
256
|
-
const response = await docClient.send(queryCommand);
|
|
257
|
-
if (response.Items?.length === 1) {
|
|
258
|
-
return utilDynamodb.unmarshall(response.Items[0]);
|
|
259
262
|
}
|
|
263
|
+
const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex);
|
|
264
|
+
return queryResult.entities[0];
|
|
260
265
|
}
|
|
261
266
|
catch (err) {
|
|
262
267
|
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
@@ -268,28 +273,39 @@ class DynamoDbEntityStorageConnector {
|
|
|
268
273
|
id
|
|
269
274
|
}, err);
|
|
270
275
|
}
|
|
271
|
-
return undefined;
|
|
272
276
|
}
|
|
273
277
|
/**
|
|
274
278
|
* Set an entity.
|
|
275
279
|
* @param entity The entity to set.
|
|
280
|
+
* @param conditions The optional conditions to match for the entities.
|
|
276
281
|
* @returns The id of the entity.
|
|
277
282
|
*/
|
|
278
|
-
async set(entity) {
|
|
283
|
+
async set(entity, conditions) {
|
|
279
284
|
core.Guards.object(this.CLASS_NAME, "entity", entity);
|
|
280
285
|
const id = entity[this._primaryKey.property];
|
|
281
286
|
try {
|
|
282
287
|
const docClient = this.createDocClient();
|
|
288
|
+
const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
|
|
283
289
|
const putCommand = new libDynamodb.PutCommand({
|
|
284
290
|
TableName: this._config.tableName,
|
|
285
291
|
Item: {
|
|
286
292
|
[DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
287
293
|
...entity
|
|
288
|
-
}
|
|
294
|
+
},
|
|
295
|
+
// Only set the condition expression if we have conditions to match
|
|
296
|
+
// and the primary key exists, otherwise we are creating a new object
|
|
297
|
+
ConditionExpression: core.Is.stringValue(conditionExpression)
|
|
298
|
+
? `(attribute_exists(${this._primaryKey.property}) AND ${conditionExpression}) OR attribute_not_exists(${this._primaryKey.property})`
|
|
299
|
+
: undefined,
|
|
300
|
+
ExpressionAttributeNames: attributeNames,
|
|
301
|
+
ExpressionAttributeValues: attributeValues
|
|
289
302
|
});
|
|
290
303
|
await docClient.send(putCommand);
|
|
291
304
|
}
|
|
292
305
|
catch (err) {
|
|
306
|
+
if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
293
309
|
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
294
310
|
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
295
311
|
tableName: this._config.tableName
|
|
@@ -303,22 +319,30 @@ class DynamoDbEntityStorageConnector {
|
|
|
303
319
|
/**
|
|
304
320
|
* Remove the entity.
|
|
305
321
|
* @param id The id of the entity to remove.
|
|
322
|
+
* @param conditions The optional conditions to match for the entities.
|
|
306
323
|
* @returns Nothing.
|
|
307
324
|
*/
|
|
308
|
-
async remove(id) {
|
|
325
|
+
async remove(id, conditions) {
|
|
309
326
|
core.Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
310
327
|
try {
|
|
311
328
|
const docClient = this.createDocClient();
|
|
329
|
+
const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
|
|
312
330
|
const deleteCommand = new libDynamodb.DeleteCommand({
|
|
313
331
|
TableName: this._config.tableName,
|
|
314
332
|
Key: {
|
|
315
333
|
[DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
316
334
|
[this._primaryKey.property]: id
|
|
317
|
-
}
|
|
335
|
+
},
|
|
336
|
+
ConditionExpression: conditionExpression,
|
|
337
|
+
ExpressionAttributeNames: attributeNames,
|
|
338
|
+
ExpressionAttributeValues: attributeValues
|
|
318
339
|
});
|
|
319
340
|
await docClient.send(deleteCommand);
|
|
320
341
|
}
|
|
321
342
|
catch (err) {
|
|
343
|
+
if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
322
346
|
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
323
347
|
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
324
348
|
table: this._config.tableName
|
|
@@ -340,80 +364,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
340
364
|
* and a cursor which can be used to request more entities.
|
|
341
365
|
*/
|
|
342
366
|
async query(conditions, sortProperties, properties, cursor, pageSize) {
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
|
|
346
|
-
let indexName;
|
|
347
|
-
// If we have a sortable property defined in the descriptor then we must use
|
|
348
|
-
// the secondary index for the query
|
|
349
|
-
if (core.Is.arrayValue(sortProperties)) {
|
|
350
|
-
if (sortProperties.length > 1) {
|
|
351
|
-
throw new core.GeneralError(this.CLASS_NAME, "sortSingle");
|
|
352
|
-
}
|
|
353
|
-
for (const sortProperty of sortProperties) {
|
|
354
|
-
const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
|
|
355
|
-
if (core.Is.undefined(propertySchema) ||
|
|
356
|
-
(!propertySchema.isPrimary &&
|
|
357
|
-
!propertySchema.isSecondary &&
|
|
358
|
-
core.Is.empty(propertySchema.sortDirection))) {
|
|
359
|
-
throw new core.GeneralError(this.CLASS_NAME, "sortNotIndexed", {
|
|
360
|
-
property: sortProperty.property
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
indexName = propertySchema.isPrimary
|
|
364
|
-
? undefined
|
|
365
|
-
: `${sortProperty.property}Index`;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const attributeNames = { "#partitionId": "partitionId" };
|
|
369
|
-
const attributeValues = {
|
|
370
|
-
[`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
|
|
371
|
-
S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
|
|
372
|
-
}
|
|
373
|
-
};
|
|
374
|
-
const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues);
|
|
375
|
-
let keyExpression = "#partitionId = :partitionId";
|
|
376
|
-
if (expressions.keyCondition.length > 0) {
|
|
377
|
-
keyExpression += ` AND ${expressions.keyCondition}`;
|
|
378
|
-
}
|
|
379
|
-
const query = new clientDynamodb.QueryCommand({
|
|
380
|
-
TableName: this._config.tableName,
|
|
381
|
-
IndexName: indexName,
|
|
382
|
-
KeyConditionExpression: keyExpression,
|
|
383
|
-
FilterExpression: core.Is.stringValue(expressions.filterCondition)
|
|
384
|
-
? expressions.filterCondition
|
|
385
|
-
: undefined,
|
|
386
|
-
ExpressionAttributeNames: attributeNames,
|
|
387
|
-
ExpressionAttributeValues: attributeValues,
|
|
388
|
-
ProjectionExpression: properties?.map(p => p).join(", "),
|
|
389
|
-
Limit: returnSize,
|
|
390
|
-
ExclusiveStartKey: core.Is.empty(cursor)
|
|
391
|
-
? undefined
|
|
392
|
-
: core.ObjectHelper.fromBytes(core.Converter.base64ToBytes(cursor))
|
|
393
|
-
});
|
|
394
|
-
const connection = this.createDocClient();
|
|
395
|
-
const results = await connection.send(query);
|
|
396
|
-
let entities = [];
|
|
397
|
-
if (core.Is.arrayValue(results.Items)) {
|
|
398
|
-
entities = results.Items.map(item => utilDynamodb.unmarshall(item));
|
|
399
|
-
}
|
|
400
|
-
return {
|
|
401
|
-
entities,
|
|
402
|
-
cursor: core.Is.empty(results.LastEvaluatedKey)
|
|
403
|
-
? undefined
|
|
404
|
-
: core.Converter.bytesToBase64(core.ObjectHelper.toBytes(results.LastEvaluatedKey))
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
catch (err) {
|
|
408
|
-
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
409
|
-
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
410
|
-
table: this._config.tableName
|
|
411
|
-
}, err);
|
|
412
|
-
}
|
|
413
|
-
throw new core.GeneralError(this.CLASS_NAME, "queryFailed", {
|
|
414
|
-
sql
|
|
415
|
-
}, err);
|
|
416
|
-
}
|
|
367
|
+
return this.internalQuery(conditions, sortProperties, properties, cursor, pageSize);
|
|
417
368
|
}
|
|
418
369
|
/**
|
|
419
370
|
* Delete the table.
|
|
@@ -427,7 +378,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
427
378
|
catch { }
|
|
428
379
|
}
|
|
429
380
|
/**
|
|
430
|
-
* Create
|
|
381
|
+
* Create the parameters for a query.
|
|
431
382
|
* @param objectPath The path for the nested object.
|
|
432
383
|
* @param condition The conditions to create the query from.
|
|
433
384
|
* @param attributeNames The attribute names to use in the query.
|
|
@@ -435,7 +386,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
435
386
|
* @returns The condition clause.
|
|
436
387
|
* @internal
|
|
437
388
|
*/
|
|
438
|
-
buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
|
|
389
|
+
buildQueryParameters(objectPath, condition, attributeNames, attributeValues, secondaryIndex) {
|
|
439
390
|
// If no conditions are defined then return empty string
|
|
440
391
|
if (core.Is.undefined(condition)) {
|
|
441
392
|
return {
|
|
@@ -444,11 +395,21 @@ class DynamoDbEntityStorageConnector {
|
|
|
444
395
|
};
|
|
445
396
|
}
|
|
446
397
|
if ("conditions" in condition) {
|
|
398
|
+
if (condition.conditions.length === 0) {
|
|
399
|
+
return {
|
|
400
|
+
keyCondition: "",
|
|
401
|
+
filterCondition: ""
|
|
402
|
+
};
|
|
403
|
+
}
|
|
447
404
|
// It's a group of comparisons, so check the individual items and combine with the logical operator
|
|
448
|
-
const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues));
|
|
405
|
+
const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues, secondaryIndex));
|
|
449
406
|
const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
|
|
450
|
-
const keyCondition = joinConditions
|
|
407
|
+
const keyCondition = joinConditions
|
|
408
|
+
.filter(j => j.keyCondition.length > 0)
|
|
409
|
+
.map(j => j.keyCondition)
|
|
410
|
+
.join(` ${logicalOperator} `);
|
|
451
411
|
const filterCondition = joinConditions
|
|
412
|
+
.filter(j => j.filterCondition.length > 0)
|
|
452
413
|
.map(j => j.filterCondition)
|
|
453
414
|
.join(` ${logicalOperator} `);
|
|
454
415
|
return {
|
|
@@ -459,9 +420,10 @@ class DynamoDbEntityStorageConnector {
|
|
|
459
420
|
const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
|
|
460
421
|
// It's a single value so just create the property comparison for the condition
|
|
461
422
|
const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
|
|
423
|
+
const isKey = schemaProp?.isPrimary || (schemaProp?.isSecondary && schemaProp?.property === secondaryIndex);
|
|
462
424
|
return {
|
|
463
|
-
keyCondition:
|
|
464
|
-
filterCondition:
|
|
425
|
+
keyCondition: isKey ? comparison : "",
|
|
426
|
+
filterCondition: !isKey ? comparison : ""
|
|
465
427
|
};
|
|
466
428
|
}
|
|
467
429
|
/**
|
|
@@ -642,6 +604,128 @@ class DynamoDbEntityStorageConnector {
|
|
|
642
604
|
return false;
|
|
643
605
|
}
|
|
644
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Find all the entities which match the conditions.
|
|
609
|
+
* @param conditions The conditions to match for the entities.
|
|
610
|
+
* @param sortProperties The optional sort order.
|
|
611
|
+
* @param properties The optional properties to return, defaults to all.
|
|
612
|
+
* @param cursor The cursor to request the next page of entities.
|
|
613
|
+
* @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
614
|
+
* @param secondaryIndex The secondary index to use for the query.
|
|
615
|
+
* @returns All the entities for the storage matching the conditions,
|
|
616
|
+
* and a cursor which can be used to request more entities.
|
|
617
|
+
* @internal
|
|
618
|
+
*/
|
|
619
|
+
async internalQuery(conditions, sortProperties, properties, cursor, pageSize, secondaryIndex) {
|
|
620
|
+
try {
|
|
621
|
+
const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
|
|
622
|
+
let indexName = core.Is.stringValue(secondaryIndex)
|
|
623
|
+
? `${secondaryIndex}Index`
|
|
624
|
+
: undefined;
|
|
625
|
+
// If we have a sortable property defined in the descriptor then we must use
|
|
626
|
+
// the secondary index for the query
|
|
627
|
+
let scanAscending = true;
|
|
628
|
+
if (core.Is.arrayValue(sortProperties)) {
|
|
629
|
+
if (sortProperties.length > 1) {
|
|
630
|
+
throw new core.GeneralError(this.CLASS_NAME, "sortSingle");
|
|
631
|
+
}
|
|
632
|
+
for (const sortProperty of sortProperties) {
|
|
633
|
+
const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
|
|
634
|
+
if (core.Is.undefined(propertySchema) ||
|
|
635
|
+
(!propertySchema.isPrimary &&
|
|
636
|
+
!propertySchema.isSecondary &&
|
|
637
|
+
core.Is.empty(propertySchema.sortDirection))) {
|
|
638
|
+
throw new core.GeneralError(this.CLASS_NAME, "sortNotIndexed", {
|
|
639
|
+
property: sortProperty.property
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
indexName = propertySchema.isPrimary
|
|
643
|
+
? undefined
|
|
644
|
+
: `${sortProperty.property}Index`;
|
|
645
|
+
scanAscending = sortProperty.sortDirection === entity.SortDirection.Ascending;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const attributeNames = { "#partitionId": "partitionId" };
|
|
649
|
+
const attributeValues = {
|
|
650
|
+
[`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
|
|
651
|
+
S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues, secondaryIndex);
|
|
655
|
+
let keyExpression = "#partitionId = :partitionId";
|
|
656
|
+
if (expressions.keyCondition.length > 0) {
|
|
657
|
+
keyExpression += ` AND ${expressions.keyCondition}`;
|
|
658
|
+
}
|
|
659
|
+
const query = new clientDynamodb.QueryCommand({
|
|
660
|
+
TableName: this._config.tableName,
|
|
661
|
+
IndexName: indexName,
|
|
662
|
+
KeyConditionExpression: keyExpression,
|
|
663
|
+
FilterExpression: core.Is.stringValue(expressions.filterCondition)
|
|
664
|
+
? expressions.filterCondition
|
|
665
|
+
: undefined,
|
|
666
|
+
ExpressionAttributeNames: attributeNames,
|
|
667
|
+
ExpressionAttributeValues: attributeValues,
|
|
668
|
+
ProjectionExpression: properties?.map(p => p).join(", "),
|
|
669
|
+
Limit: returnSize,
|
|
670
|
+
ScanIndexForward: scanAscending,
|
|
671
|
+
ExclusiveStartKey: core.Is.empty(cursor)
|
|
672
|
+
? undefined
|
|
673
|
+
: core.ObjectHelper.fromBytes(core.Converter.base64ToBytes(cursor))
|
|
674
|
+
});
|
|
675
|
+
const connection = this.createDocClient();
|
|
676
|
+
const results = await connection.send(query);
|
|
677
|
+
let entities = [];
|
|
678
|
+
if (core.Is.arrayValue(results.Items)) {
|
|
679
|
+
entities = results.Items.map(item => utilDynamodb.unmarshall(item));
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
entities,
|
|
683
|
+
cursor: core.Is.empty(results.LastEvaluatedKey)
|
|
684
|
+
? undefined
|
|
685
|
+
: core.Converter.bytesToBase64(core.ObjectHelper.toBytes(results.LastEvaluatedKey))
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
catch (err) {
|
|
689
|
+
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
690
|
+
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
691
|
+
table: this._config.tableName
|
|
692
|
+
}, err);
|
|
693
|
+
}
|
|
694
|
+
throw new core.GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Build the condition expression for the query.
|
|
699
|
+
* @param conditions The conditions to build the expression from.
|
|
700
|
+
* @returns The condition expression.
|
|
701
|
+
* @throws GeneralError if the property is not found in the schema.
|
|
702
|
+
* @internal
|
|
703
|
+
*/
|
|
704
|
+
buildConditionExpression(conditions) {
|
|
705
|
+
let conditionExpression;
|
|
706
|
+
let attributeNames;
|
|
707
|
+
let attributeValues;
|
|
708
|
+
if (core.Is.arrayValue(conditions)) {
|
|
709
|
+
const expressions = [];
|
|
710
|
+
for (const c of conditions) {
|
|
711
|
+
const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
|
|
712
|
+
if (core.Is.undefined(schemaProp)) {
|
|
713
|
+
throw new core.GeneralError(this.CLASS_NAME, "propertyNotFound", {
|
|
714
|
+
property: c.property
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
const attributeName = `#${c.property}`;
|
|
718
|
+
const attributeValueName = `:${c.property}`;
|
|
719
|
+
attributeNames ??= {};
|
|
720
|
+
attributeValues ??= {};
|
|
721
|
+
attributeNames[attributeName] = c.property;
|
|
722
|
+
attributeValues[attributeValueName] = c.value;
|
|
723
|
+
expressions.push(`${attributeName} = ${attributeValueName}`);
|
|
724
|
+
}
|
|
725
|
+
conditionExpression = expressions.join(" AND ");
|
|
726
|
+
}
|
|
727
|
+
return { conditionExpression, attributeNames, attributeValues };
|
|
728
|
+
}
|
|
645
729
|
}
|
|
646
730
|
|
|
647
731
|
exports.DynamoDbEntityStorageConnector = DynamoDbEntityStorageConnector;
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { waitUntilTableExists,
|
|
1
|
+
import { waitUntilTableExists, DynamoDB, QueryCommand } from '@aws-sdk/client-dynamodb';
|
|
2
2
|
import { GetCommand, PutCommand, DeleteCommand, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
3
|
import { unmarshall } from '@aws-sdk/util-dynamodb';
|
|
4
|
-
import { Guards, Is, BaseError, GeneralError, ObjectHelper, Converter
|
|
5
|
-
import { EntitySchemaFactory, EntitySchemaHelper, ComparisonOperator, LogicalOperator } from '@twin.org/entity';
|
|
4
|
+
import { Guards, Is, BaseError, GeneralError, Coerce, ObjectHelper, Converter } from '@twin.org/core';
|
|
5
|
+
import { EntitySchemaFactory, EntitySchemaHelper, ComparisonOperator, LogicalOperator, SortDirection } from '@twin.org/entity';
|
|
6
6
|
import { LoggingConnectorFactory } from '@twin.org/logging-models';
|
|
7
7
|
|
|
8
8
|
// Copyright 2024 IOTA Stiftung.
|
|
@@ -48,9 +48,6 @@ class DynamoDbEntityStorageConnector {
|
|
|
48
48
|
/**
|
|
49
49
|
* Create a new instance of DynamoDbEntityStorageConnector.
|
|
50
50
|
* @param options The options for the connector.
|
|
51
|
-
* @param options.entitySchema The schema for the entity.
|
|
52
|
-
* @param options.loggingConnectorType The type of logging connector to use, defaults to no logging.
|
|
53
|
-
* @param options.config The configuration for the connector.
|
|
54
51
|
*/
|
|
55
52
|
constructor(options) {
|
|
56
53
|
Guards.object(this.CLASS_NAME, "options", options);
|
|
@@ -212,17 +209,25 @@ class DynamoDbEntityStorageConnector {
|
|
|
212
209
|
}
|
|
213
210
|
return true;
|
|
214
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Get the schema for the entities.
|
|
214
|
+
* @returns The schema for the entities.
|
|
215
|
+
*/
|
|
216
|
+
getSchema() {
|
|
217
|
+
return this._entitySchema;
|
|
218
|
+
}
|
|
215
219
|
/**
|
|
216
220
|
* Get an entity.
|
|
217
221
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
218
222
|
* @param secondaryIndex Get the item using a secondary index.
|
|
223
|
+
* @param conditions The optional conditions to match for the entities.
|
|
219
224
|
* @returns The object if it can be found or undefined.
|
|
220
225
|
*/
|
|
221
|
-
async get(id, secondaryIndex) {
|
|
226
|
+
async get(id, secondaryIndex, conditions) {
|
|
222
227
|
Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
223
228
|
try {
|
|
224
229
|
const docClient = this.createDocClient();
|
|
225
|
-
if (Is.
|
|
230
|
+
if (Is.empty(secondaryIndex) && Is.empty(conditions)) {
|
|
226
231
|
const getCommand = new GetCommand({
|
|
227
232
|
TableName: this._config.tableName,
|
|
228
233
|
Key: {
|
|
@@ -234,27 +239,27 @@ class DynamoDbEntityStorageConnector {
|
|
|
234
239
|
delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
|
|
235
240
|
return response.Item;
|
|
236
241
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
242
|
+
const finalConditions = {
|
|
243
|
+
conditions: []
|
|
244
|
+
};
|
|
245
|
+
if (Is.stringValue(secondaryIndex)) {
|
|
246
|
+
finalConditions.conditions.push({
|
|
247
|
+
property: secondaryIndex,
|
|
248
|
+
comparison: ComparisonOperator.Equals,
|
|
249
|
+
value: id
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
if (Is.arrayValue(conditions)) {
|
|
253
|
+
for (const c of conditions) {
|
|
254
|
+
finalConditions.conditions.push({
|
|
255
|
+
property: c.property,
|
|
256
|
+
comparison: ComparisonOperator.Equals,
|
|
257
|
+
value: c.value
|
|
258
|
+
});
|
|
252
259
|
}
|
|
253
|
-
});
|
|
254
|
-
const response = await docClient.send(queryCommand);
|
|
255
|
-
if (response.Items?.length === 1) {
|
|
256
|
-
return unmarshall(response.Items[0]);
|
|
257
260
|
}
|
|
261
|
+
const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex);
|
|
262
|
+
return queryResult.entities[0];
|
|
258
263
|
}
|
|
259
264
|
catch (err) {
|
|
260
265
|
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
@@ -266,28 +271,39 @@ class DynamoDbEntityStorageConnector {
|
|
|
266
271
|
id
|
|
267
272
|
}, err);
|
|
268
273
|
}
|
|
269
|
-
return undefined;
|
|
270
274
|
}
|
|
271
275
|
/**
|
|
272
276
|
* Set an entity.
|
|
273
277
|
* @param entity The entity to set.
|
|
278
|
+
* @param conditions The optional conditions to match for the entities.
|
|
274
279
|
* @returns The id of the entity.
|
|
275
280
|
*/
|
|
276
|
-
async set(entity) {
|
|
281
|
+
async set(entity, conditions) {
|
|
277
282
|
Guards.object(this.CLASS_NAME, "entity", entity);
|
|
278
283
|
const id = entity[this._primaryKey.property];
|
|
279
284
|
try {
|
|
280
285
|
const docClient = this.createDocClient();
|
|
286
|
+
const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
|
|
281
287
|
const putCommand = new PutCommand({
|
|
282
288
|
TableName: this._config.tableName,
|
|
283
289
|
Item: {
|
|
284
290
|
[DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
285
291
|
...entity
|
|
286
|
-
}
|
|
292
|
+
},
|
|
293
|
+
// Only set the condition expression if we have conditions to match
|
|
294
|
+
// and the primary key exists, otherwise we are creating a new object
|
|
295
|
+
ConditionExpression: Is.stringValue(conditionExpression)
|
|
296
|
+
? `(attribute_exists(${this._primaryKey.property}) AND ${conditionExpression}) OR attribute_not_exists(${this._primaryKey.property})`
|
|
297
|
+
: undefined,
|
|
298
|
+
ExpressionAttributeNames: attributeNames,
|
|
299
|
+
ExpressionAttributeValues: attributeValues
|
|
287
300
|
});
|
|
288
301
|
await docClient.send(putCommand);
|
|
289
302
|
}
|
|
290
303
|
catch (err) {
|
|
304
|
+
if (BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
291
307
|
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
292
308
|
throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
293
309
|
tableName: this._config.tableName
|
|
@@ -301,22 +317,30 @@ class DynamoDbEntityStorageConnector {
|
|
|
301
317
|
/**
|
|
302
318
|
* Remove the entity.
|
|
303
319
|
* @param id The id of the entity to remove.
|
|
320
|
+
* @param conditions The optional conditions to match for the entities.
|
|
304
321
|
* @returns Nothing.
|
|
305
322
|
*/
|
|
306
|
-
async remove(id) {
|
|
323
|
+
async remove(id, conditions) {
|
|
307
324
|
Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
308
325
|
try {
|
|
309
326
|
const docClient = this.createDocClient();
|
|
327
|
+
const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
|
|
310
328
|
const deleteCommand = new DeleteCommand({
|
|
311
329
|
TableName: this._config.tableName,
|
|
312
330
|
Key: {
|
|
313
331
|
[DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
314
332
|
[this._primaryKey.property]: id
|
|
315
|
-
}
|
|
333
|
+
},
|
|
334
|
+
ConditionExpression: conditionExpression,
|
|
335
|
+
ExpressionAttributeNames: attributeNames,
|
|
336
|
+
ExpressionAttributeValues: attributeValues
|
|
316
337
|
});
|
|
317
338
|
await docClient.send(deleteCommand);
|
|
318
339
|
}
|
|
319
340
|
catch (err) {
|
|
341
|
+
if (BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
320
344
|
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
321
345
|
throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
322
346
|
table: this._config.tableName
|
|
@@ -338,80 +362,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
338
362
|
* and a cursor which can be used to request more entities.
|
|
339
363
|
*/
|
|
340
364
|
async query(conditions, sortProperties, properties, cursor, pageSize) {
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
|
|
344
|
-
let indexName;
|
|
345
|
-
// If we have a sortable property defined in the descriptor then we must use
|
|
346
|
-
// the secondary index for the query
|
|
347
|
-
if (Is.arrayValue(sortProperties)) {
|
|
348
|
-
if (sortProperties.length > 1) {
|
|
349
|
-
throw new GeneralError(this.CLASS_NAME, "sortSingle");
|
|
350
|
-
}
|
|
351
|
-
for (const sortProperty of sortProperties) {
|
|
352
|
-
const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
|
|
353
|
-
if (Is.undefined(propertySchema) ||
|
|
354
|
-
(!propertySchema.isPrimary &&
|
|
355
|
-
!propertySchema.isSecondary &&
|
|
356
|
-
Is.empty(propertySchema.sortDirection))) {
|
|
357
|
-
throw new GeneralError(this.CLASS_NAME, "sortNotIndexed", {
|
|
358
|
-
property: sortProperty.property
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
indexName = propertySchema.isPrimary
|
|
362
|
-
? undefined
|
|
363
|
-
: `${sortProperty.property}Index`;
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const attributeNames = { "#partitionId": "partitionId" };
|
|
367
|
-
const attributeValues = {
|
|
368
|
-
[`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
|
|
369
|
-
S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues);
|
|
373
|
-
let keyExpression = "#partitionId = :partitionId";
|
|
374
|
-
if (expressions.keyCondition.length > 0) {
|
|
375
|
-
keyExpression += ` AND ${expressions.keyCondition}`;
|
|
376
|
-
}
|
|
377
|
-
const query = new QueryCommand({
|
|
378
|
-
TableName: this._config.tableName,
|
|
379
|
-
IndexName: indexName,
|
|
380
|
-
KeyConditionExpression: keyExpression,
|
|
381
|
-
FilterExpression: Is.stringValue(expressions.filterCondition)
|
|
382
|
-
? expressions.filterCondition
|
|
383
|
-
: undefined,
|
|
384
|
-
ExpressionAttributeNames: attributeNames,
|
|
385
|
-
ExpressionAttributeValues: attributeValues,
|
|
386
|
-
ProjectionExpression: properties?.map(p => p).join(", "),
|
|
387
|
-
Limit: returnSize,
|
|
388
|
-
ExclusiveStartKey: Is.empty(cursor)
|
|
389
|
-
? undefined
|
|
390
|
-
: ObjectHelper.fromBytes(Converter.base64ToBytes(cursor))
|
|
391
|
-
});
|
|
392
|
-
const connection = this.createDocClient();
|
|
393
|
-
const results = await connection.send(query);
|
|
394
|
-
let entities = [];
|
|
395
|
-
if (Is.arrayValue(results.Items)) {
|
|
396
|
-
entities = results.Items.map(item => unmarshall(item));
|
|
397
|
-
}
|
|
398
|
-
return {
|
|
399
|
-
entities,
|
|
400
|
-
cursor: Is.empty(results.LastEvaluatedKey)
|
|
401
|
-
? undefined
|
|
402
|
-
: Converter.bytesToBase64(ObjectHelper.toBytes(results.LastEvaluatedKey))
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
catch (err) {
|
|
406
|
-
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
407
|
-
throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
408
|
-
table: this._config.tableName
|
|
409
|
-
}, err);
|
|
410
|
-
}
|
|
411
|
-
throw new GeneralError(this.CLASS_NAME, "queryFailed", {
|
|
412
|
-
sql
|
|
413
|
-
}, err);
|
|
414
|
-
}
|
|
365
|
+
return this.internalQuery(conditions, sortProperties, properties, cursor, pageSize);
|
|
415
366
|
}
|
|
416
367
|
/**
|
|
417
368
|
* Delete the table.
|
|
@@ -425,7 +376,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
425
376
|
catch { }
|
|
426
377
|
}
|
|
427
378
|
/**
|
|
428
|
-
* Create
|
|
379
|
+
* Create the parameters for a query.
|
|
429
380
|
* @param objectPath The path for the nested object.
|
|
430
381
|
* @param condition The conditions to create the query from.
|
|
431
382
|
* @param attributeNames The attribute names to use in the query.
|
|
@@ -433,7 +384,7 @@ class DynamoDbEntityStorageConnector {
|
|
|
433
384
|
* @returns The condition clause.
|
|
434
385
|
* @internal
|
|
435
386
|
*/
|
|
436
|
-
buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
|
|
387
|
+
buildQueryParameters(objectPath, condition, attributeNames, attributeValues, secondaryIndex) {
|
|
437
388
|
// If no conditions are defined then return empty string
|
|
438
389
|
if (Is.undefined(condition)) {
|
|
439
390
|
return {
|
|
@@ -442,11 +393,21 @@ class DynamoDbEntityStorageConnector {
|
|
|
442
393
|
};
|
|
443
394
|
}
|
|
444
395
|
if ("conditions" in condition) {
|
|
396
|
+
if (condition.conditions.length === 0) {
|
|
397
|
+
return {
|
|
398
|
+
keyCondition: "",
|
|
399
|
+
filterCondition: ""
|
|
400
|
+
};
|
|
401
|
+
}
|
|
445
402
|
// It's a group of comparisons, so check the individual items and combine with the logical operator
|
|
446
|
-
const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues));
|
|
403
|
+
const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues, secondaryIndex));
|
|
447
404
|
const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
|
|
448
|
-
const keyCondition = joinConditions
|
|
405
|
+
const keyCondition = joinConditions
|
|
406
|
+
.filter(j => j.keyCondition.length > 0)
|
|
407
|
+
.map(j => j.keyCondition)
|
|
408
|
+
.join(` ${logicalOperator} `);
|
|
449
409
|
const filterCondition = joinConditions
|
|
410
|
+
.filter(j => j.filterCondition.length > 0)
|
|
450
411
|
.map(j => j.filterCondition)
|
|
451
412
|
.join(` ${logicalOperator} `);
|
|
452
413
|
return {
|
|
@@ -457,9 +418,10 @@ class DynamoDbEntityStorageConnector {
|
|
|
457
418
|
const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
|
|
458
419
|
// It's a single value so just create the property comparison for the condition
|
|
459
420
|
const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
|
|
421
|
+
const isKey = schemaProp?.isPrimary || (schemaProp?.isSecondary && schemaProp?.property === secondaryIndex);
|
|
460
422
|
return {
|
|
461
|
-
keyCondition:
|
|
462
|
-
filterCondition:
|
|
423
|
+
keyCondition: isKey ? comparison : "",
|
|
424
|
+
filterCondition: !isKey ? comparison : ""
|
|
463
425
|
};
|
|
464
426
|
}
|
|
465
427
|
/**
|
|
@@ -640,6 +602,128 @@ class DynamoDbEntityStorageConnector {
|
|
|
640
602
|
return false;
|
|
641
603
|
}
|
|
642
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Find all the entities which match the conditions.
|
|
607
|
+
* @param conditions The conditions to match for the entities.
|
|
608
|
+
* @param sortProperties The optional sort order.
|
|
609
|
+
* @param properties The optional properties to return, defaults to all.
|
|
610
|
+
* @param cursor The cursor to request the next page of entities.
|
|
611
|
+
* @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
612
|
+
* @param secondaryIndex The secondary index to use for the query.
|
|
613
|
+
* @returns All the entities for the storage matching the conditions,
|
|
614
|
+
* and a cursor which can be used to request more entities.
|
|
615
|
+
* @internal
|
|
616
|
+
*/
|
|
617
|
+
async internalQuery(conditions, sortProperties, properties, cursor, pageSize, secondaryIndex) {
|
|
618
|
+
try {
|
|
619
|
+
const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
|
|
620
|
+
let indexName = Is.stringValue(secondaryIndex)
|
|
621
|
+
? `${secondaryIndex}Index`
|
|
622
|
+
: undefined;
|
|
623
|
+
// If we have a sortable property defined in the descriptor then we must use
|
|
624
|
+
// the secondary index for the query
|
|
625
|
+
let scanAscending = true;
|
|
626
|
+
if (Is.arrayValue(sortProperties)) {
|
|
627
|
+
if (sortProperties.length > 1) {
|
|
628
|
+
throw new GeneralError(this.CLASS_NAME, "sortSingle");
|
|
629
|
+
}
|
|
630
|
+
for (const sortProperty of sortProperties) {
|
|
631
|
+
const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
|
|
632
|
+
if (Is.undefined(propertySchema) ||
|
|
633
|
+
(!propertySchema.isPrimary &&
|
|
634
|
+
!propertySchema.isSecondary &&
|
|
635
|
+
Is.empty(propertySchema.sortDirection))) {
|
|
636
|
+
throw new GeneralError(this.CLASS_NAME, "sortNotIndexed", {
|
|
637
|
+
property: sortProperty.property
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
indexName = propertySchema.isPrimary
|
|
641
|
+
? undefined
|
|
642
|
+
: `${sortProperty.property}Index`;
|
|
643
|
+
scanAscending = sortProperty.sortDirection === SortDirection.Ascending;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
const attributeNames = { "#partitionId": "partitionId" };
|
|
647
|
+
const attributeValues = {
|
|
648
|
+
[`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
|
|
649
|
+
S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues, secondaryIndex);
|
|
653
|
+
let keyExpression = "#partitionId = :partitionId";
|
|
654
|
+
if (expressions.keyCondition.length > 0) {
|
|
655
|
+
keyExpression += ` AND ${expressions.keyCondition}`;
|
|
656
|
+
}
|
|
657
|
+
const query = new QueryCommand({
|
|
658
|
+
TableName: this._config.tableName,
|
|
659
|
+
IndexName: indexName,
|
|
660
|
+
KeyConditionExpression: keyExpression,
|
|
661
|
+
FilterExpression: Is.stringValue(expressions.filterCondition)
|
|
662
|
+
? expressions.filterCondition
|
|
663
|
+
: undefined,
|
|
664
|
+
ExpressionAttributeNames: attributeNames,
|
|
665
|
+
ExpressionAttributeValues: attributeValues,
|
|
666
|
+
ProjectionExpression: properties?.map(p => p).join(", "),
|
|
667
|
+
Limit: returnSize,
|
|
668
|
+
ScanIndexForward: scanAscending,
|
|
669
|
+
ExclusiveStartKey: Is.empty(cursor)
|
|
670
|
+
? undefined
|
|
671
|
+
: ObjectHelper.fromBytes(Converter.base64ToBytes(cursor))
|
|
672
|
+
});
|
|
673
|
+
const connection = this.createDocClient();
|
|
674
|
+
const results = await connection.send(query);
|
|
675
|
+
let entities = [];
|
|
676
|
+
if (Is.arrayValue(results.Items)) {
|
|
677
|
+
entities = results.Items.map(item => unmarshall(item));
|
|
678
|
+
}
|
|
679
|
+
return {
|
|
680
|
+
entities,
|
|
681
|
+
cursor: Is.empty(results.LastEvaluatedKey)
|
|
682
|
+
? undefined
|
|
683
|
+
: Converter.bytesToBase64(ObjectHelper.toBytes(results.LastEvaluatedKey))
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
688
|
+
throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
689
|
+
table: this._config.tableName
|
|
690
|
+
}, err);
|
|
691
|
+
}
|
|
692
|
+
throw new GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Build the condition expression for the query.
|
|
697
|
+
* @param conditions The conditions to build the expression from.
|
|
698
|
+
* @returns The condition expression.
|
|
699
|
+
* @throws GeneralError if the property is not found in the schema.
|
|
700
|
+
* @internal
|
|
701
|
+
*/
|
|
702
|
+
buildConditionExpression(conditions) {
|
|
703
|
+
let conditionExpression;
|
|
704
|
+
let attributeNames;
|
|
705
|
+
let attributeValues;
|
|
706
|
+
if (Is.arrayValue(conditions)) {
|
|
707
|
+
const expressions = [];
|
|
708
|
+
for (const c of conditions) {
|
|
709
|
+
const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
|
|
710
|
+
if (Is.undefined(schemaProp)) {
|
|
711
|
+
throw new GeneralError(this.CLASS_NAME, "propertyNotFound", {
|
|
712
|
+
property: c.property
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
const attributeName = `#${c.property}`;
|
|
716
|
+
const attributeValueName = `:${c.property}`;
|
|
717
|
+
attributeNames ??= {};
|
|
718
|
+
attributeValues ??= {};
|
|
719
|
+
attributeNames[attributeName] = c.property;
|
|
720
|
+
attributeValues[attributeValueName] = c.value;
|
|
721
|
+
expressions.push(`${attributeName} = ${attributeValueName}`);
|
|
722
|
+
}
|
|
723
|
+
conditionExpression = expressions.join(" AND ");
|
|
724
|
+
}
|
|
725
|
+
return { conditionExpression, attributeNames, attributeValues };
|
|
726
|
+
}
|
|
643
727
|
}
|
|
644
728
|
|
|
645
729
|
export { DynamoDbEntityStorageConnector };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type EntityCondition, type SortDirection } from "@twin.org/entity";
|
|
1
|
+
import { type EntityCondition, type IEntitySchema, SortDirection } from "@twin.org/entity";
|
|
2
2
|
import type { IEntityStorageConnector } from "@twin.org/entity-storage-models";
|
|
3
|
-
import type {
|
|
3
|
+
import type { IDynamoDbEntityStorageConnectorConstructorOptions } from "./models/IDynamoDbEntityStorageConnectorConstructorOptions";
|
|
4
4
|
/**
|
|
5
5
|
* Class for performing entity storage operations using Dynamo DB.
|
|
6
6
|
*/
|
|
@@ -12,40 +12,50 @@ export declare class DynamoDbEntityStorageConnector<T = unknown> implements IEnt
|
|
|
12
12
|
/**
|
|
13
13
|
* Create a new instance of DynamoDbEntityStorageConnector.
|
|
14
14
|
* @param options The options for the connector.
|
|
15
|
-
* @param options.entitySchema The schema for the entity.
|
|
16
|
-
* @param options.loggingConnectorType The type of logging connector to use, defaults to no logging.
|
|
17
|
-
* @param options.config The configuration for the connector.
|
|
18
15
|
*/
|
|
19
|
-
constructor(options:
|
|
20
|
-
entitySchema: string;
|
|
21
|
-
loggingConnectorType?: string;
|
|
22
|
-
config: IDynamoDbEntityStorageConnectorConfig;
|
|
23
|
-
});
|
|
16
|
+
constructor(options: IDynamoDbEntityStorageConnectorConstructorOptions);
|
|
24
17
|
/**
|
|
25
18
|
* Bootstrap the component by creating and initializing any resources it needs.
|
|
26
19
|
* @param nodeLoggingConnectorType The node logging connector type, defaults to "node-logging".
|
|
27
20
|
* @returns True if the bootstrapping process was successful.
|
|
28
21
|
*/
|
|
29
22
|
bootstrap(nodeLoggingConnectorType?: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Get the schema for the entities.
|
|
25
|
+
* @returns The schema for the entities.
|
|
26
|
+
*/
|
|
27
|
+
getSchema(): IEntitySchema;
|
|
30
28
|
/**
|
|
31
29
|
* Get an entity.
|
|
32
30
|
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
33
31
|
* @param secondaryIndex Get the item using a secondary index.
|
|
32
|
+
* @param conditions The optional conditions to match for the entities.
|
|
34
33
|
* @returns The object if it can be found or undefined.
|
|
35
34
|
*/
|
|
36
|
-
get(id: string, secondaryIndex?: keyof T
|
|
35
|
+
get(id: string, secondaryIndex?: keyof T, conditions?: {
|
|
36
|
+
property: keyof T;
|
|
37
|
+
value: unknown;
|
|
38
|
+
}[]): Promise<T | undefined>;
|
|
37
39
|
/**
|
|
38
40
|
* Set an entity.
|
|
39
41
|
* @param entity The entity to set.
|
|
42
|
+
* @param conditions The optional conditions to match for the entities.
|
|
40
43
|
* @returns The id of the entity.
|
|
41
44
|
*/
|
|
42
|
-
set(entity: T
|
|
45
|
+
set(entity: T, conditions?: {
|
|
46
|
+
property: keyof T;
|
|
47
|
+
value: unknown;
|
|
48
|
+
}[]): Promise<void>;
|
|
43
49
|
/**
|
|
44
50
|
* Remove the entity.
|
|
45
51
|
* @param id The id of the entity to remove.
|
|
52
|
+
* @param conditions The optional conditions to match for the entities.
|
|
46
53
|
* @returns Nothing.
|
|
47
54
|
*/
|
|
48
|
-
remove(id: string
|
|
55
|
+
remove(id: string, conditions?: {
|
|
56
|
+
property: keyof T;
|
|
57
|
+
value: unknown;
|
|
58
|
+
}[]): Promise<void>;
|
|
49
59
|
/**
|
|
50
60
|
* Find all the entities which match the conditions.
|
|
51
61
|
* @param conditions The conditions to match for the entities.
|
package/dist/types/index.d.ts
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { IDynamoDbEntityStorageConnectorConfig } from "./IDynamoDbEntityStorageConnectorConfig";
|
|
2
|
+
/**
|
|
3
|
+
* Options for the Dynamo DB Entity Storage Connector constructor.
|
|
4
|
+
*/
|
|
5
|
+
export interface IDynamoDbEntityStorageConnectorConstructorOptions {
|
|
6
|
+
/**
|
|
7
|
+
* The schema for the entity
|
|
8
|
+
*/
|
|
9
|
+
entitySchema: string;
|
|
10
|
+
/**
|
|
11
|
+
* The type of logging connector to use, defaults to no logging.
|
|
12
|
+
*/
|
|
13
|
+
loggingConnectorType?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The configuration for the connector.
|
|
16
|
+
*/
|
|
17
|
+
config: IDynamoDbEntityStorageConnectorConfig;
|
|
18
|
+
}
|
package/docs/changelog.md
CHANGED
|
@@ -20,21 +20,11 @@ Create a new instance of DynamoDbEntityStorageConnector.
|
|
|
20
20
|
|
|
21
21
|
#### Parameters
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
##### options
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
• **options.entitySchema**: `string`
|
|
28
|
-
|
|
29
|
-
The schema for the entity.
|
|
30
|
-
|
|
31
|
-
• **options.loggingConnectorType?**: `string`
|
|
32
|
-
|
|
33
|
-
The type of logging connector to use, defaults to no logging.
|
|
34
|
-
|
|
35
|
-
• **options.config**: [`IDynamoDbEntityStorageConnectorConfig`](../interfaces/IDynamoDbEntityStorageConnectorConfig.md)
|
|
25
|
+
[`IDynamoDbEntityStorageConnectorConstructorOptions`](../interfaces/IDynamoDbEntityStorageConnectorConstructorOptions.md)
|
|
36
26
|
|
|
37
|
-
The
|
|
27
|
+
The options for the connector.
|
|
38
28
|
|
|
39
29
|
#### Returns
|
|
40
30
|
|
|
@@ -62,7 +52,9 @@ Bootstrap the component by creating and initializing any resources it needs.
|
|
|
62
52
|
|
|
63
53
|
#### Parameters
|
|
64
54
|
|
|
65
|
-
|
|
55
|
+
##### nodeLoggingConnectorType?
|
|
56
|
+
|
|
57
|
+
`string`
|
|
66
58
|
|
|
67
59
|
The node logging connector type, defaults to "node-logging".
|
|
68
60
|
|
|
@@ -78,22 +70,50 @@ True if the bootstrapping process was successful.
|
|
|
78
70
|
|
|
79
71
|
***
|
|
80
72
|
|
|
73
|
+
### getSchema()
|
|
74
|
+
|
|
75
|
+
> **getSchema**(): `IEntitySchema`
|
|
76
|
+
|
|
77
|
+
Get the schema for the entities.
|
|
78
|
+
|
|
79
|
+
#### Returns
|
|
80
|
+
|
|
81
|
+
`IEntitySchema`
|
|
82
|
+
|
|
83
|
+
The schema for the entities.
|
|
84
|
+
|
|
85
|
+
#### Implementation of
|
|
86
|
+
|
|
87
|
+
`IEntityStorageConnector.getSchema`
|
|
88
|
+
|
|
89
|
+
***
|
|
90
|
+
|
|
81
91
|
### get()
|
|
82
92
|
|
|
83
|
-
> **get**(`id`, `secondaryIndex`?): `Promise`\<`undefined` \| `T`\>
|
|
93
|
+
> **get**(`id`, `secondaryIndex`?, `conditions`?): `Promise`\<`undefined` \| `T`\>
|
|
84
94
|
|
|
85
95
|
Get an entity.
|
|
86
96
|
|
|
87
97
|
#### Parameters
|
|
88
98
|
|
|
89
|
-
|
|
99
|
+
##### id
|
|
100
|
+
|
|
101
|
+
`string`
|
|
90
102
|
|
|
91
103
|
The id of the entity to get, or the index value if secondaryIndex is set.
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
##### secondaryIndex?
|
|
106
|
+
|
|
107
|
+
keyof `T`
|
|
94
108
|
|
|
95
109
|
Get the item using a secondary index.
|
|
96
110
|
|
|
111
|
+
##### conditions?
|
|
112
|
+
|
|
113
|
+
`object`[]
|
|
114
|
+
|
|
115
|
+
The optional conditions to match for the entities.
|
|
116
|
+
|
|
97
117
|
#### Returns
|
|
98
118
|
|
|
99
119
|
`Promise`\<`undefined` \| `T`\>
|
|
@@ -108,16 +128,24 @@ The object if it can be found or undefined.
|
|
|
108
128
|
|
|
109
129
|
### set()
|
|
110
130
|
|
|
111
|
-
> **set**(`entity`): `Promise`\<`void`\>
|
|
131
|
+
> **set**(`entity`, `conditions`?): `Promise`\<`void`\>
|
|
112
132
|
|
|
113
133
|
Set an entity.
|
|
114
134
|
|
|
115
135
|
#### Parameters
|
|
116
136
|
|
|
117
|
-
|
|
137
|
+
##### entity
|
|
138
|
+
|
|
139
|
+
`T`
|
|
118
140
|
|
|
119
141
|
The entity to set.
|
|
120
142
|
|
|
143
|
+
##### conditions?
|
|
144
|
+
|
|
145
|
+
`object`[]
|
|
146
|
+
|
|
147
|
+
The optional conditions to match for the entities.
|
|
148
|
+
|
|
121
149
|
#### Returns
|
|
122
150
|
|
|
123
151
|
`Promise`\<`void`\>
|
|
@@ -132,16 +160,24 @@ The id of the entity.
|
|
|
132
160
|
|
|
133
161
|
### remove()
|
|
134
162
|
|
|
135
|
-
> **remove**(`id`): `Promise`\<`void`\>
|
|
163
|
+
> **remove**(`id`, `conditions`?): `Promise`\<`void`\>
|
|
136
164
|
|
|
137
165
|
Remove the entity.
|
|
138
166
|
|
|
139
167
|
#### Parameters
|
|
140
168
|
|
|
141
|
-
|
|
169
|
+
##### id
|
|
170
|
+
|
|
171
|
+
`string`
|
|
142
172
|
|
|
143
173
|
The id of the entity to remove.
|
|
144
174
|
|
|
175
|
+
##### conditions?
|
|
176
|
+
|
|
177
|
+
`object`[]
|
|
178
|
+
|
|
179
|
+
The optional conditions to match for the entities.
|
|
180
|
+
|
|
145
181
|
#### Returns
|
|
146
182
|
|
|
147
183
|
`Promise`\<`void`\>
|
|
@@ -156,51 +192,49 @@ Nothing.
|
|
|
156
192
|
|
|
157
193
|
### query()
|
|
158
194
|
|
|
159
|
-
> **query**(`conditions`?, `sortProperties`?, `properties`?, `cursor`?, `pageSize`?): `Promise`\<`
|
|
195
|
+
> **query**(`conditions`?, `sortProperties`?, `properties`?, `cursor`?, `pageSize`?): `Promise`\<\{ `entities`: `Partial`\<`T`\>[]; `cursor`: `string`; \}\>
|
|
160
196
|
|
|
161
197
|
Find all the entities which match the conditions.
|
|
162
198
|
|
|
163
199
|
#### Parameters
|
|
164
200
|
|
|
165
|
-
|
|
201
|
+
##### conditions?
|
|
202
|
+
|
|
203
|
+
`EntityCondition`\<`T`\>
|
|
166
204
|
|
|
167
205
|
The conditions to match for the entities.
|
|
168
206
|
|
|
169
|
-
|
|
207
|
+
##### sortProperties?
|
|
208
|
+
|
|
209
|
+
`object`[]
|
|
170
210
|
|
|
171
211
|
The optional sort order.
|
|
172
212
|
|
|
173
|
-
|
|
213
|
+
##### properties?
|
|
214
|
+
|
|
215
|
+
keyof `T`[]
|
|
174
216
|
|
|
175
217
|
The optional properties to return, defaults to all.
|
|
176
218
|
|
|
177
|
-
|
|
219
|
+
##### cursor?
|
|
220
|
+
|
|
221
|
+
`string`
|
|
178
222
|
|
|
179
223
|
The cursor to request the next page of entities.
|
|
180
224
|
|
|
181
|
-
|
|
225
|
+
##### pageSize?
|
|
226
|
+
|
|
227
|
+
`number`
|
|
182
228
|
|
|
183
229
|
The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
184
230
|
|
|
185
231
|
#### Returns
|
|
186
232
|
|
|
187
|
-
`Promise`\<`
|
|
233
|
+
`Promise`\<\{ `entities`: `Partial`\<`T`\>[]; `cursor`: `string`; \}\>
|
|
188
234
|
|
|
189
235
|
All the entities for the storage matching the conditions,
|
|
190
236
|
and a cursor which can be used to request more entities.
|
|
191
237
|
|
|
192
|
-
##### entities
|
|
193
|
-
|
|
194
|
-
> **entities**: `Partial`\<`T`\>[]
|
|
195
|
-
|
|
196
|
-
The entities, which can be partial if a limited keys list was provided.
|
|
197
|
-
|
|
198
|
-
##### cursor?
|
|
199
|
-
|
|
200
|
-
> `optional` **cursor**: `string`
|
|
201
|
-
|
|
202
|
-
An optional cursor, when defined can be used to call find to get more entities.
|
|
203
|
-
|
|
204
238
|
#### Implementation of
|
|
205
239
|
|
|
206
240
|
`IEntityStorageConnector.query`
|
package/docs/reference/index.md
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Interface: IDynamoDbEntityStorageConnectorConstructorOptions
|
|
2
|
+
|
|
3
|
+
Options for the Dynamo DB Entity Storage Connector constructor.
|
|
4
|
+
|
|
5
|
+
## Properties
|
|
6
|
+
|
|
7
|
+
### entitySchema
|
|
8
|
+
|
|
9
|
+
> **entitySchema**: `string`
|
|
10
|
+
|
|
11
|
+
The schema for the entity
|
|
12
|
+
|
|
13
|
+
***
|
|
14
|
+
|
|
15
|
+
### loggingConnectorType?
|
|
16
|
+
|
|
17
|
+
> `optional` **loggingConnectorType**: `string`
|
|
18
|
+
|
|
19
|
+
The type of logging connector to use, defaults to no logging.
|
|
20
|
+
|
|
21
|
+
***
|
|
22
|
+
|
|
23
|
+
### config
|
|
24
|
+
|
|
25
|
+
> **config**: [`IDynamoDbEntityStorageConnectorConfig`](IDynamoDbEntityStorageConnectorConfig.md)
|
|
26
|
+
|
|
27
|
+
The configuration for the connector.
|
package/locales/en.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"setFailed": "Unable to set entity \"{id}\"",
|
|
14
14
|
"getFailed": "Unable to get entity \"{id}\"",
|
|
15
15
|
"removeFailed": "Unable to remove entity \"{id}\"",
|
|
16
|
-
"queryFailed": "The query failed
|
|
16
|
+
"queryFailed": "The query failed",
|
|
17
17
|
"comparisonNotSupported": "Comparison operator \"{comparison}\" is not supported",
|
|
18
18
|
"conditionalNotSupported": "Conditional operator \"{operator}\" is not supported",
|
|
19
19
|
"sortSingle": "You can only sort by a single property",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twin.org/entity-storage-connector-dynamodb",
|
|
3
|
-
"version": "0.0.1-next.
|
|
3
|
+
"version": "0.0.1-next.21",
|
|
4
4
|
"description": "Entity Storage connector implementation using DynamoDb storage",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,56 +13,26 @@
|
|
|
13
13
|
"engines": {
|
|
14
14
|
"node": ">=20.0.0"
|
|
15
15
|
},
|
|
16
|
-
"scripts": {
|
|
17
|
-
"clean": "rimraf dist coverage docs/reference",
|
|
18
|
-
"build": "tspc",
|
|
19
|
-
"test": "vitest --run --config ./vitest.config.ts --no-cache",
|
|
20
|
-
"coverage": "vitest --run --coverage --config ./vitest.config.ts --no-cache",
|
|
21
|
-
"bundle:esm": "rollup --config rollup.config.mjs --environment MODULE:esm",
|
|
22
|
-
"bundle:cjs": "rollup --config rollup.config.mjs --environment MODULE:cjs",
|
|
23
|
-
"bundle": "npm run bundle:esm && npm run bundle:cjs",
|
|
24
|
-
"docs:clean": "rimraf docs/reference",
|
|
25
|
-
"docs:generate": "typedoc",
|
|
26
|
-
"docs": "npm run docs:clean && npm run docs:generate",
|
|
27
|
-
"dist": "npm run clean && npm run build && npm run test && npm run bundle && npm run docs"
|
|
28
|
-
},
|
|
29
16
|
"dependencies": {
|
|
17
|
+
"@aws-sdk/client-dynamodb": "3.738",
|
|
18
|
+
"@aws-sdk/lib-dynamodb": "3.738",
|
|
19
|
+
"@aws-sdk/util-dynamodb": "3.738",
|
|
30
20
|
"@twin.org/core": "next",
|
|
31
21
|
"@twin.org/entity": "next",
|
|
32
|
-
"@twin.org/
|
|
33
|
-
"@twin.org/entity-storage-models": "0.0.1-next.2",
|
|
22
|
+
"@twin.org/entity-storage-models": "0.0.1-next.21",
|
|
34
23
|
"@twin.org/logging-models": "next",
|
|
35
|
-
"@
|
|
36
|
-
"@aws-sdk/lib-dynamodb": "3.651",
|
|
37
|
-
"@aws-sdk/util-dynamodb": "3.651"
|
|
38
|
-
},
|
|
39
|
-
"devDependencies": {
|
|
40
|
-
"@twin.org/entity-storage-connector-memory": "0.0.1-next.2",
|
|
41
|
-
"@twin.org/logging-connector-entity-storage": "next",
|
|
42
|
-
"@twin.org/nameof-transformer": "next",
|
|
43
|
-
"@vitest/coverage-v8": "2.1.1",
|
|
44
|
-
"copyfiles": "2.4.1",
|
|
45
|
-
"dotenv": "16.4.5",
|
|
46
|
-
"rimraf": "6.0.1",
|
|
47
|
-
"rollup": "4.21.3",
|
|
48
|
-
"rollup-plugin-copy": "3.5.0",
|
|
49
|
-
"rollup-plugin-typescript2": "0.36.0",
|
|
50
|
-
"ts-patch": "3.2.1",
|
|
51
|
-
"typedoc": "0.26.7",
|
|
52
|
-
"typedoc-plugin-markdown": "4.2.7",
|
|
53
|
-
"typescript": "5.6.2",
|
|
54
|
-
"vitest": "2.1.1"
|
|
24
|
+
"@twin.org/nameof": "next"
|
|
55
25
|
},
|
|
56
26
|
"main": "./dist/cjs/index.cjs",
|
|
57
27
|
"module": "./dist/esm/index.mjs",
|
|
58
28
|
"types": "./dist/types/index.d.ts",
|
|
59
29
|
"exports": {
|
|
60
30
|
".": {
|
|
31
|
+
"types": "./dist/types/index.d.ts",
|
|
61
32
|
"require": "./dist/cjs/index.cjs",
|
|
62
|
-
"import": "./dist/esm/index.mjs"
|
|
63
|
-
"types": "./dist/types/index.d.ts"
|
|
33
|
+
"import": "./dist/esm/index.mjs"
|
|
64
34
|
},
|
|
65
|
-
"./locales": "./locales"
|
|
35
|
+
"./locales/*.json": "./locales/*.json"
|
|
66
36
|
},
|
|
67
37
|
"files": [
|
|
68
38
|
"dist/cjs",
|