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

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.
@@ -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);
@@ -225,13 +222,14 @@ class DynamoDbEntityStorageConnector {
225
222
  * Get an entity.
226
223
  * @param id The id of the entity to get, or the index value if secondaryIndex is set.
227
224
  * @param secondaryIndex Get the item using a secondary index.
225
+ * @param conditions The optional conditions to match for the entities.
228
226
  * @returns The object if it can be found or undefined.
229
227
  */
230
- async get(id, secondaryIndex) {
228
+ async get(id, secondaryIndex, conditions) {
231
229
  core.Guards.stringValue(this.CLASS_NAME, "id", id);
232
230
  try {
233
231
  const docClient = this.createDocClient();
234
- if (core.Is.undefined(secondaryIndex)) {
232
+ if (core.Is.empty(secondaryIndex) && core.Is.empty(conditions)) {
235
233
  const getCommand = new libDynamodb.GetCommand({
236
234
  TableName: this._config.tableName,
237
235
  Key: {
@@ -243,27 +241,27 @@ class DynamoDbEntityStorageConnector {
243
241
  delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
244
242
  return response.Item;
245
243
  }
246
- const secIndex = secondaryIndex.toString();
247
- const globalSecondaryIndex = `${secIndex}Index`;
248
- const queryCommand = new clientDynamodb.QueryCommand({
249
- TableName: this._config.tableName,
250
- IndexName: globalSecondaryIndex,
251
- KeyConditionExpression: `#${secIndex} = :id AND #${DynamoDbEntityStorageConnector._PARTITION_ID_NAME} = :${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`,
252
- ExpressionAttributeNames: {
253
- [`#${secIndex}`]: secIndex,
254
- [`#${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: DynamoDbEntityStorageConnector._PARTITION_ID_NAME
255
- },
256
- ExpressionAttributeValues: {
257
- [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
258
- S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
259
- },
260
- ":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
+ });
261
261
  }
262
- });
263
- const response = await docClient.send(queryCommand);
264
- if (response.Items?.length === 1) {
265
- return utilDynamodb.unmarshall(response.Items[0]);
266
262
  }
263
+ const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex);
264
+ return queryResult.entities[0];
267
265
  }
268
266
  catch (err) {
269
267
  if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
@@ -275,28 +273,40 @@ class DynamoDbEntityStorageConnector {
275
273
  id
276
274
  }, err);
277
275
  }
278
- return undefined;
279
276
  }
280
277
  /**
281
278
  * Set an entity.
282
279
  * @param entity The entity to set.
280
+ * @param conditions The optional conditions to match for the entities.
283
281
  * @returns The id of the entity.
284
282
  */
285
- async set(entity) {
286
- core.Guards.object(this.CLASS_NAME, "entity", entity);
287
- 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];
288
287
  try {
289
288
  const docClient = this.createDocClient();
289
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
290
290
  const putCommand = new libDynamodb.PutCommand({
291
291
  TableName: this._config.tableName,
292
292
  Item: {
293
293
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
294
- ...entity
295
- }
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
296
303
  });
297
304
  await docClient.send(putCommand);
298
305
  }
299
306
  catch (err) {
307
+ if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
308
+ return;
309
+ }
300
310
  if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
301
311
  throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
302
312
  tableName: this._config.tableName
@@ -310,22 +320,30 @@ class DynamoDbEntityStorageConnector {
310
320
  /**
311
321
  * Remove the entity.
312
322
  * @param id The id of the entity to remove.
323
+ * @param conditions The optional conditions to match for the entities.
313
324
  * @returns Nothing.
314
325
  */
315
- async remove(id) {
326
+ async remove(id, conditions) {
316
327
  core.Guards.stringValue(this.CLASS_NAME, "id", id);
317
328
  try {
318
329
  const docClient = this.createDocClient();
330
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
319
331
  const deleteCommand = new libDynamodb.DeleteCommand({
320
332
  TableName: this._config.tableName,
321
333
  Key: {
322
334
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
323
335
  [this._primaryKey.property]: id
324
- }
336
+ },
337
+ ConditionExpression: conditionExpression,
338
+ ExpressionAttributeNames: attributeNames,
339
+ ExpressionAttributeValues: attributeValues
325
340
  });
326
341
  await docClient.send(deleteCommand);
327
342
  }
328
343
  catch (err) {
344
+ if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
345
+ return;
346
+ }
329
347
  if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
330
348
  throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
331
349
  table: this._config.tableName
@@ -347,77 +365,7 @@ class DynamoDbEntityStorageConnector {
347
365
  * and a cursor which can be used to request more entities.
348
366
  */
349
367
  async query(conditions, sortProperties, properties, cursor, pageSize) {
350
- try {
351
- const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
352
- let indexName;
353
- // If we have a sortable property defined in the descriptor then we must use
354
- // the secondary index for the query
355
- if (core.Is.arrayValue(sortProperties)) {
356
- if (sortProperties.length > 1) {
357
- throw new core.GeneralError(this.CLASS_NAME, "sortSingle");
358
- }
359
- for (const sortProperty of sortProperties) {
360
- const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
361
- if (core.Is.undefined(propertySchema) ||
362
- (!propertySchema.isPrimary &&
363
- !propertySchema.isSecondary &&
364
- core.Is.empty(propertySchema.sortDirection))) {
365
- throw new core.GeneralError(this.CLASS_NAME, "sortNotIndexed", {
366
- property: sortProperty.property
367
- });
368
- }
369
- indexName = propertySchema.isPrimary
370
- ? undefined
371
- : `${sortProperty.property}Index`;
372
- }
373
- }
374
- const attributeNames = { "#partitionId": "partitionId" };
375
- const attributeValues = {
376
- [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
377
- S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
378
- }
379
- };
380
- const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues);
381
- let keyExpression = "#partitionId = :partitionId";
382
- if (expressions.keyCondition.length > 0) {
383
- keyExpression += ` AND ${expressions.keyCondition}`;
384
- }
385
- const query = new clientDynamodb.QueryCommand({
386
- TableName: this._config.tableName,
387
- IndexName: indexName,
388
- KeyConditionExpression: keyExpression,
389
- FilterExpression: core.Is.stringValue(expressions.filterCondition)
390
- ? expressions.filterCondition
391
- : undefined,
392
- ExpressionAttributeNames: attributeNames,
393
- ExpressionAttributeValues: attributeValues,
394
- ProjectionExpression: properties?.map(p => p).join(", "),
395
- Limit: returnSize,
396
- ExclusiveStartKey: core.Is.empty(cursor)
397
- ? undefined
398
- : core.ObjectHelper.fromBytes(core.Converter.base64ToBytes(cursor))
399
- });
400
- const connection = this.createDocClient();
401
- const results = await connection.send(query);
402
- let entities = [];
403
- if (core.Is.arrayValue(results.Items)) {
404
- entities = results.Items.map(item => utilDynamodb.unmarshall(item));
405
- }
406
- return {
407
- entities,
408
- cursor: core.Is.empty(results.LastEvaluatedKey)
409
- ? undefined
410
- : core.Converter.bytesToBase64(core.ObjectHelper.toBytes(results.LastEvaluatedKey))
411
- };
412
- }
413
- catch (err) {
414
- if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
415
- throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
416
- table: this._config.tableName
417
- }, err);
418
- }
419
- throw new core.GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
420
- }
368
+ return this.internalQuery(conditions, sortProperties, properties, cursor, pageSize);
421
369
  }
422
370
  /**
423
371
  * Delete the table.
@@ -439,7 +387,7 @@ class DynamoDbEntityStorageConnector {
439
387
  * @returns The condition clause.
440
388
  * @internal
441
389
  */
442
- buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
390
+ buildQueryParameters(objectPath, condition, attributeNames, attributeValues, secondaryIndex) {
443
391
  // If no conditions are defined then return empty string
444
392
  if (core.Is.undefined(condition)) {
445
393
  return {
@@ -455,7 +403,7 @@ class DynamoDbEntityStorageConnector {
455
403
  };
456
404
  }
457
405
  // It's a group of comparisons, so check the individual items and combine with the logical operator
458
- 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));
459
407
  const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
460
408
  const keyCondition = joinConditions
461
409
  .filter(j => j.keyCondition.length > 0)
@@ -473,9 +421,10 @@ class DynamoDbEntityStorageConnector {
473
421
  const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
474
422
  // It's a single value so just create the property comparison for the condition
475
423
  const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
424
+ const isKey = schemaProp?.isPrimary || (schemaProp?.isSecondary && schemaProp?.property === secondaryIndex);
476
425
  return {
477
- keyCondition: schemaProp?.isPrimary ? comparison : "",
478
- filterCondition: schemaProp?.isPrimary ? "" : comparison
426
+ keyCondition: isKey ? comparison : "",
427
+ filterCondition: !isKey ? comparison : ""
479
428
  };
480
429
  }
481
430
  /**
@@ -496,6 +445,14 @@ class DynamoDbEntityStorageConnector {
496
445
  }
497
446
  prop += comparator.property;
498
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
+ }
499
456
  let propName = `:${attributeName.replace(/\./g, "").replace(/#/g, "")}`;
500
457
  if (core.Is.array(comparator.value)) {
501
458
  const dbValues = comparator.value.map(v => this.propertyToDbValue(v, type));
@@ -619,7 +576,7 @@ class DynamoDbEntityStorageConnector {
619
576
  }
620
577
  /**
621
578
  * Create a new DB connection.
622
- * @returns The dynamo db connection.
579
+ * @returns The Dynamo DB connection.
623
580
  * @internal
624
581
  */
625
582
  createConnection() {
@@ -627,7 +584,7 @@ class DynamoDbEntityStorageConnector {
627
584
  }
628
585
  /**
629
586
  * Create a new DB connection configuration.
630
- * @returns The dynamo db connection configuration.
587
+ * @returns The Dynamo DB connection configuration.
631
588
  * @internal
632
589
  */
633
590
  createConnectionConfig() {
@@ -656,6 +613,132 @@ class DynamoDbEntityStorageConnector {
656
613
  return false;
657
614
  }
658
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
+ }
659
742
  }
660
743
 
661
744
  exports.DynamoDbEntityStorageConnector = DynamoDbEntityStorageConnector;