@twin.org/entity-storage-connector-dynamodb 0.0.1-next.3 → 0.0.1-next.31

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 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:
@@ -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.undefined(secondaryIndex)) {
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 secIndex = secondaryIndex.toString();
240
- const globalSecondaryIndex = `${secIndex}Index`;
241
- const queryCommand = new clientDynamodb.QueryCommand({
242
- TableName: this._config.tableName,
243
- IndexName: globalSecondaryIndex,
244
- KeyConditionExpression: `#${secIndex} = :id AND #${DynamoDbEntityStorageConnector._PARTITION_ID_NAME} = :${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`,
245
- ExpressionAttributeNames: {
246
- [`#${secIndex}`]: secIndex,
247
- [`#${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: DynamoDbEntityStorageConnector._PARTITION_ID_NAME
248
- },
249
- ExpressionAttributeValues: {
250
- [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
251
- S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
252
- },
253
- ":id": { S: id }
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,40 @@ 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) {
279
- core.Guards.object(this.CLASS_NAME, "entity", entity);
280
- const id = entity[this._primaryKey.property];
283
+ async set(entity$1, conditions) {
284
+ core.Guards.object(this.CLASS_NAME, "entity", entity$1);
285
+ entity.EntitySchemaHelper.validateEntity(entity$1, this.getSchema());
286
+ const id = entity$1[this._primaryKey.property];
281
287
  try {
282
288
  const docClient = this.createDocClient();
289
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
283
290
  const putCommand = new libDynamodb.PutCommand({
284
291
  TableName: this._config.tableName,
285
292
  Item: {
286
293
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
287
- ...entity
288
- }
294
+ ...entity$1
295
+ },
296
+ // Only set the condition expression if we have conditions to match
297
+ // and the primary key exists, otherwise we are creating a new object
298
+ ConditionExpression: core.Is.stringValue(conditionExpression)
299
+ ? `(attribute_exists(${this._primaryKey.property}) AND ${conditionExpression}) OR attribute_not_exists(${this._primaryKey.property})`
300
+ : undefined,
301
+ ExpressionAttributeNames: attributeNames,
302
+ ExpressionAttributeValues: attributeValues
289
303
  });
290
304
  await docClient.send(putCommand);
291
305
  }
292
306
  catch (err) {
307
+ if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
308
+ return;
309
+ }
293
310
  if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
294
311
  throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
295
312
  tableName: this._config.tableName
@@ -303,22 +320,30 @@ class DynamoDbEntityStorageConnector {
303
320
  /**
304
321
  * Remove the entity.
305
322
  * @param id The id of the entity to remove.
323
+ * @param conditions The optional conditions to match for the entities.
306
324
  * @returns Nothing.
307
325
  */
308
- async remove(id) {
326
+ async remove(id, conditions) {
309
327
  core.Guards.stringValue(this.CLASS_NAME, "id", id);
310
328
  try {
311
329
  const docClient = this.createDocClient();
330
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
312
331
  const deleteCommand = new libDynamodb.DeleteCommand({
313
332
  TableName: this._config.tableName,
314
333
  Key: {
315
334
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
316
335
  [this._primaryKey.property]: id
317
- }
336
+ },
337
+ ConditionExpression: conditionExpression,
338
+ ExpressionAttributeNames: attributeNames,
339
+ ExpressionAttributeValues: attributeValues
318
340
  });
319
341
  await docClient.send(deleteCommand);
320
342
  }
321
343
  catch (err) {
344
+ if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
345
+ return;
346
+ }
322
347
  if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
323
348
  throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
324
349
  table: this._config.tableName
@@ -340,80 +365,7 @@ class DynamoDbEntityStorageConnector {
340
365
  * and a cursor which can be used to request more entities.
341
366
  */
342
367
  async query(conditions, sortProperties, properties, cursor, pageSize) {
343
- const sql = "";
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
- }
368
+ return this.internalQuery(conditions, sortProperties, properties, cursor, pageSize);
417
369
  }
418
370
  /**
419
371
  * Delete the table.
@@ -427,7 +379,7 @@ class DynamoDbEntityStorageConnector {
427
379
  catch { }
428
380
  }
429
381
  /**
430
- * Create an SQL condition clause.
382
+ * Create the parameters for a query.
431
383
  * @param objectPath The path for the nested object.
432
384
  * @param condition The conditions to create the query from.
433
385
  * @param attributeNames The attribute names to use in the query.
@@ -435,7 +387,7 @@ class DynamoDbEntityStorageConnector {
435
387
  * @returns The condition clause.
436
388
  * @internal
437
389
  */
438
- buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
390
+ buildQueryParameters(objectPath, condition, attributeNames, attributeValues, secondaryIndex) {
439
391
  // If no conditions are defined then return empty string
440
392
  if (core.Is.undefined(condition)) {
441
393
  return {
@@ -444,11 +396,21 @@ class DynamoDbEntityStorageConnector {
444
396
  };
445
397
  }
446
398
  if ("conditions" in condition) {
399
+ if (condition.conditions.length === 0) {
400
+ return {
401
+ keyCondition: "",
402
+ filterCondition: ""
403
+ };
404
+ }
447
405
  // 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));
406
+ const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues, secondaryIndex));
449
407
  const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
450
- const keyCondition = joinConditions.map(j => j.keyCondition).join(` ${logicalOperator} `);
408
+ const keyCondition = joinConditions
409
+ .filter(j => j.keyCondition.length > 0)
410
+ .map(j => j.keyCondition)
411
+ .join(` ${logicalOperator} `);
451
412
  const filterCondition = joinConditions
413
+ .filter(j => j.filterCondition.length > 0)
452
414
  .map(j => j.filterCondition)
453
415
  .join(` ${logicalOperator} `);
454
416
  return {
@@ -459,9 +421,10 @@ class DynamoDbEntityStorageConnector {
459
421
  const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
460
422
  // It's a single value so just create the property comparison for the condition
461
423
  const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
424
+ const isKey = schemaProp?.isPrimary || (schemaProp?.isSecondary && schemaProp?.property === secondaryIndex);
462
425
  return {
463
- keyCondition: schemaProp?.isPrimary ? comparison : "",
464
- filterCondition: schemaProp?.isPrimary ? "" : comparison
426
+ keyCondition: isKey ? comparison : "",
427
+ filterCondition: !isKey ? comparison : ""
465
428
  };
466
429
  }
467
430
  /**
@@ -482,6 +445,14 @@ class DynamoDbEntityStorageConnector {
482
445
  }
483
446
  prop += comparator.property;
484
447
  let attributeName = this.populateAttributeNames(prop, attributeNames);
448
+ if (core.Is.empty(comparator.value)) {
449
+ if (comparator.comparison === entity.ComparisonOperator.Equals) {
450
+ return `attribute_not_exists(${attributeName})`;
451
+ }
452
+ else if (comparator.comparison === entity.ComparisonOperator.NotEquals) {
453
+ return `attribute_exists(${attributeName})`;
454
+ }
455
+ }
485
456
  let propName = `:${attributeName.replace(/\./g, "").replace(/#/g, "")}`;
486
457
  if (core.Is.array(comparator.value)) {
487
458
  const dbValues = comparator.value.map(v => this.propertyToDbValue(v, type));
@@ -605,7 +576,7 @@ class DynamoDbEntityStorageConnector {
605
576
  }
606
577
  /**
607
578
  * Create a new DB connection.
608
- * @returns The dynamo db connection.
579
+ * @returns The Dynamo DB connection.
609
580
  * @internal
610
581
  */
611
582
  createConnection() {
@@ -613,7 +584,7 @@ class DynamoDbEntityStorageConnector {
613
584
  }
614
585
  /**
615
586
  * Create a new DB connection configuration.
616
- * @returns The dynamo db connection configuration.
587
+ * @returns The Dynamo DB connection configuration.
617
588
  * @internal
618
589
  */
619
590
  createConnectionConfig() {
@@ -642,6 +613,132 @@ class DynamoDbEntityStorageConnector {
642
613
  return false;
643
614
  }
644
615
  }
616
+ /**
617
+ * Find all the entities which match the conditions.
618
+ * @param conditions The conditions to match for the entities.
619
+ * @param sortProperties The optional sort order.
620
+ * @param properties The optional properties to return, defaults to all.
621
+ * @param cursor The cursor to request the next page of entities.
622
+ * @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
623
+ * @param secondaryIndex The secondary index to use for the query.
624
+ * @returns All the entities for the storage matching the conditions,
625
+ * and a cursor which can be used to request more entities.
626
+ * @internal
627
+ */
628
+ async internalQuery(conditions, sortProperties, properties, cursor, pageSize, secondaryIndex) {
629
+ try {
630
+ const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
631
+ let indexName = core.Is.stringValue(secondaryIndex)
632
+ ? `${secondaryIndex}Index`
633
+ : undefined;
634
+ // If we have a sortable property defined in the descriptor then we must use
635
+ // the secondary index for the query
636
+ let scanAscending = true;
637
+ if (core.Is.arrayValue(sortProperties)) {
638
+ if (sortProperties.length > 1) {
639
+ throw new core.GeneralError(this.CLASS_NAME, "sortSingle");
640
+ }
641
+ for (const sortProperty of sortProperties) {
642
+ const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
643
+ if (core.Is.undefined(propertySchema) ||
644
+ (!propertySchema.isPrimary &&
645
+ !propertySchema.isSecondary &&
646
+ core.Is.empty(propertySchema.sortDirection))) {
647
+ throw new core.GeneralError(this.CLASS_NAME, "sortNotIndexed", {
648
+ property: sortProperty.property
649
+ });
650
+ }
651
+ indexName = propertySchema.isPrimary
652
+ ? undefined
653
+ : `${sortProperty.property}Index`;
654
+ scanAscending = sortProperty.sortDirection === entity.SortDirection.Ascending;
655
+ }
656
+ }
657
+ const attributeNames = { "#partitionId": "partitionId" };
658
+ const attributeValues = {
659
+ [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
660
+ S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
661
+ }
662
+ };
663
+ const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues, secondaryIndex);
664
+ let keyExpression = "#partitionId = :partitionId";
665
+ if (expressions.keyCondition.length > 0) {
666
+ keyExpression += ` AND ${expressions.keyCondition}`;
667
+ }
668
+ const query = new clientDynamodb.QueryCommand({
669
+ TableName: this._config.tableName,
670
+ IndexName: indexName,
671
+ KeyConditionExpression: keyExpression,
672
+ FilterExpression: core.Is.stringValue(expressions.filterCondition)
673
+ ? expressions.filterCondition
674
+ : undefined,
675
+ ExpressionAttributeNames: attributeNames,
676
+ ExpressionAttributeValues: attributeValues,
677
+ ProjectionExpression: properties?.map(p => p).join(", "),
678
+ Limit: returnSize,
679
+ ScanIndexForward: scanAscending,
680
+ ExclusiveStartKey: core.Is.empty(cursor)
681
+ ? undefined
682
+ : core.ObjectHelper.fromBytes(core.Converter.base64ToBytes(cursor))
683
+ });
684
+ const connection = this.createDocClient();
685
+ const results = await connection.send(query);
686
+ let entities = [];
687
+ if (core.Is.arrayValue(results.Items)) {
688
+ entities = results.Items.map(item => {
689
+ const unmarshalled = utilDynamodb.unmarshall(item);
690
+ delete unmarshalled[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
691
+ return unmarshalled;
692
+ });
693
+ }
694
+ return {
695
+ entities,
696
+ cursor: core.Is.empty(results.LastEvaluatedKey)
697
+ ? undefined
698
+ : core.Converter.bytesToBase64(core.ObjectHelper.toBytes(results.LastEvaluatedKey))
699
+ };
700
+ }
701
+ catch (err) {
702
+ if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
703
+ throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
704
+ table: this._config.tableName
705
+ }, err);
706
+ }
707
+ throw new core.GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
708
+ }
709
+ }
710
+ /**
711
+ * Build the condition expression for the query.
712
+ * @param conditions The conditions to build the expression from.
713
+ * @returns The condition expression.
714
+ * @throws GeneralError if the property is not found in the schema.
715
+ * @internal
716
+ */
717
+ buildConditionExpression(conditions) {
718
+ let conditionExpression;
719
+ let attributeNames;
720
+ let attributeValues;
721
+ if (core.Is.arrayValue(conditions)) {
722
+ const expressions = [];
723
+ for (const c of conditions) {
724
+ const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
725
+ if (core.Is.undefined(schemaProp)) {
726
+ throw new core.GeneralError(this.CLASS_NAME, "propertyNotFound", {
727
+ property: c.property
728
+ });
729
+ }
730
+ const attributeName = `#${c.property}`;
731
+ const attributeValueName = `:${c.property}`;
732
+ attributeNames ??= {};
733
+ attributeValues ??= {};
734
+ attributeNames[attributeName] = c.property;
735
+ attributeValues[attributeValueName] = c.value;
736
+ expressions.push(`${attributeName} = ${attributeValueName}`);
737
+ }
738
+ conditionExpression = expressions.join(" AND ");
739
+ }
740
+ return { conditionExpression, attributeNames, attributeValues };
741
+ }
645
742
  }
646
743
 
647
744
  exports.DynamoDbEntityStorageConnector = DynamoDbEntityStorageConnector;