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

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:
@@ -225,13 +225,14 @@ class DynamoDbEntityStorageConnector {
225
225
  * Get an entity.
226
226
  * @param id The id of the entity to get, or the index value if secondaryIndex is set.
227
227
  * @param secondaryIndex Get the item using a secondary index.
228
+ * @param conditions The optional conditions to match for the entities.
228
229
  * @returns The object if it can be found or undefined.
229
230
  */
230
- async get(id, secondaryIndex) {
231
+ async get(id, secondaryIndex, conditions) {
231
232
  core.Guards.stringValue(this.CLASS_NAME, "id", id);
232
233
  try {
233
234
  const docClient = this.createDocClient();
234
- if (core.Is.undefined(secondaryIndex)) {
235
+ if (core.Is.empty(secondaryIndex) && core.Is.empty(conditions)) {
235
236
  const getCommand = new libDynamodb.GetCommand({
236
237
  TableName: this._config.tableName,
237
238
  Key: {
@@ -243,27 +244,27 @@ class DynamoDbEntityStorageConnector {
243
244
  delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
244
245
  return response.Item;
245
246
  }
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 }
247
+ const finalConditions = {
248
+ conditions: []
249
+ };
250
+ if (core.Is.stringValue(secondaryIndex)) {
251
+ finalConditions.conditions.push({
252
+ property: secondaryIndex,
253
+ comparison: entity.ComparisonOperator.Equals,
254
+ value: id
255
+ });
256
+ }
257
+ if (core.Is.arrayValue(conditions)) {
258
+ for (const c of conditions) {
259
+ finalConditions.conditions.push({
260
+ property: c.property,
261
+ comparison: entity.ComparisonOperator.Equals,
262
+ value: c.value
263
+ });
261
264
  }
262
- });
263
- const response = await docClient.send(queryCommand);
264
- if (response.Items?.length === 1) {
265
- return utilDynamodb.unmarshall(response.Items[0]);
266
265
  }
266
+ const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex);
267
+ return queryResult.entities[0];
267
268
  }
268
269
  catch (err) {
269
270
  if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
@@ -275,24 +276,28 @@ class DynamoDbEntityStorageConnector {
275
276
  id
276
277
  }, err);
277
278
  }
278
- return undefined;
279
279
  }
280
280
  /**
281
281
  * Set an entity.
282
282
  * @param entity The entity to set.
283
+ * @param conditions The optional conditions to match for the entities.
283
284
  * @returns The id of the entity.
284
285
  */
285
- async set(entity) {
286
+ async set(entity, conditions) {
286
287
  core.Guards.object(this.CLASS_NAME, "entity", entity);
287
288
  const id = entity[this._primaryKey.property];
288
289
  try {
289
290
  const docClient = this.createDocClient();
291
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
290
292
  const putCommand = new libDynamodb.PutCommand({
291
293
  TableName: this._config.tableName,
292
294
  Item: {
293
295
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
294
296
  ...entity
295
- }
297
+ },
298
+ ConditionExpression: conditionExpression,
299
+ ExpressionAttributeNames: attributeNames,
300
+ ExpressionAttributeValues: attributeValues
296
301
  });
297
302
  await docClient.send(putCommand);
298
303
  }
@@ -310,22 +315,30 @@ class DynamoDbEntityStorageConnector {
310
315
  /**
311
316
  * Remove the entity.
312
317
  * @param id The id of the entity to remove.
318
+ * @param conditions The optional conditions to match for the entities.
313
319
  * @returns Nothing.
314
320
  */
315
- async remove(id) {
321
+ async remove(id, conditions) {
316
322
  core.Guards.stringValue(this.CLASS_NAME, "id", id);
317
323
  try {
318
324
  const docClient = this.createDocClient();
325
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
319
326
  const deleteCommand = new libDynamodb.DeleteCommand({
320
327
  TableName: this._config.tableName,
321
328
  Key: {
322
329
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
323
330
  [this._primaryKey.property]: id
324
- }
331
+ },
332
+ ConditionExpression: conditionExpression,
333
+ ExpressionAttributeNames: attributeNames,
334
+ ExpressionAttributeValues: attributeValues
325
335
  });
326
336
  await docClient.send(deleteCommand);
327
337
  }
328
338
  catch (err) {
339
+ if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
340
+ return;
341
+ }
329
342
  if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
330
343
  throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
331
344
  table: this._config.tableName
@@ -347,77 +360,7 @@ class DynamoDbEntityStorageConnector {
347
360
  * and a cursor which can be used to request more entities.
348
361
  */
349
362
  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
- }
363
+ return this.internalQuery(conditions, sortProperties, properties, cursor, pageSize);
421
364
  }
422
365
  /**
423
366
  * Delete the table.
@@ -439,7 +382,7 @@ class DynamoDbEntityStorageConnector {
439
382
  * @returns The condition clause.
440
383
  * @internal
441
384
  */
442
- buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
385
+ buildQueryParameters(objectPath, condition, attributeNames, attributeValues, secondaryIndex) {
443
386
  // If no conditions are defined then return empty string
444
387
  if (core.Is.undefined(condition)) {
445
388
  return {
@@ -455,7 +398,7 @@ class DynamoDbEntityStorageConnector {
455
398
  };
456
399
  }
457
400
  // 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));
401
+ const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues, secondaryIndex));
459
402
  const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
460
403
  const keyCondition = joinConditions
461
404
  .filter(j => j.keyCondition.length > 0)
@@ -473,9 +416,10 @@ class DynamoDbEntityStorageConnector {
473
416
  const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
474
417
  // It's a single value so just create the property comparison for the condition
475
418
  const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
419
+ const isKey = schemaProp?.isPrimary || (schemaProp?.isSecondary && schemaProp?.property === secondaryIndex);
476
420
  return {
477
- keyCondition: schemaProp?.isPrimary ? comparison : "",
478
- filterCondition: schemaProp?.isPrimary ? "" : comparison
421
+ keyCondition: isKey ? comparison : "",
422
+ filterCondition: !isKey ? comparison : ""
479
423
  };
480
424
  }
481
425
  /**
@@ -656,6 +600,125 @@ class DynamoDbEntityStorageConnector {
656
600
  return false;
657
601
  }
658
602
  }
603
+ /**
604
+ * Find all the entities which match the conditions.
605
+ * @param conditions The conditions to match for the entities.
606
+ * @param sortProperties The optional sort order.
607
+ * @param properties The optional properties to return, defaults to all.
608
+ * @param cursor The cursor to request the next page of entities.
609
+ * @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
610
+ * @param secondaryIndex The secondary index to use for the query.
611
+ * @returns All the entities for the storage matching the conditions,
612
+ * and a cursor which can be used to request more entities.
613
+ * @internal
614
+ */
615
+ async internalQuery(conditions, sortProperties, properties, cursor, pageSize, secondaryIndex) {
616
+ try {
617
+ const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
618
+ let indexName = core.Is.stringValue(secondaryIndex)
619
+ ? `${secondaryIndex}Index`
620
+ : undefined;
621
+ // If we have a sortable property defined in the descriptor then we must use
622
+ // the secondary index for the query
623
+ if (core.Is.arrayValue(sortProperties)) {
624
+ if (sortProperties.length > 1) {
625
+ throw new core.GeneralError(this.CLASS_NAME, "sortSingle");
626
+ }
627
+ for (const sortProperty of sortProperties) {
628
+ const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
629
+ if (core.Is.undefined(propertySchema) ||
630
+ (!propertySchema.isPrimary &&
631
+ !propertySchema.isSecondary &&
632
+ core.Is.empty(propertySchema.sortDirection))) {
633
+ throw new core.GeneralError(this.CLASS_NAME, "sortNotIndexed", {
634
+ property: sortProperty.property
635
+ });
636
+ }
637
+ indexName = propertySchema.isPrimary
638
+ ? undefined
639
+ : `${sortProperty.property}Index`;
640
+ }
641
+ }
642
+ const attributeNames = { "#partitionId": "partitionId" };
643
+ const attributeValues = {
644
+ [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
645
+ S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
646
+ }
647
+ };
648
+ const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues, secondaryIndex);
649
+ let keyExpression = "#partitionId = :partitionId";
650
+ if (expressions.keyCondition.length > 0) {
651
+ keyExpression += ` AND ${expressions.keyCondition}`;
652
+ }
653
+ const query = new clientDynamodb.QueryCommand({
654
+ TableName: this._config.tableName,
655
+ IndexName: indexName,
656
+ KeyConditionExpression: keyExpression,
657
+ FilterExpression: core.Is.stringValue(expressions.filterCondition)
658
+ ? expressions.filterCondition
659
+ : undefined,
660
+ ExpressionAttributeNames: attributeNames,
661
+ ExpressionAttributeValues: attributeValues,
662
+ ProjectionExpression: properties?.map(p => p).join(", "),
663
+ Limit: returnSize,
664
+ ExclusiveStartKey: core.Is.empty(cursor)
665
+ ? undefined
666
+ : core.ObjectHelper.fromBytes(core.Converter.base64ToBytes(cursor))
667
+ });
668
+ const connection = this.createDocClient();
669
+ const results = await connection.send(query);
670
+ let entities = [];
671
+ if (core.Is.arrayValue(results.Items)) {
672
+ entities = results.Items.map(item => utilDynamodb.unmarshall(item));
673
+ }
674
+ return {
675
+ entities,
676
+ cursor: core.Is.empty(results.LastEvaluatedKey)
677
+ ? undefined
678
+ : core.Converter.bytesToBase64(core.ObjectHelper.toBytes(results.LastEvaluatedKey))
679
+ };
680
+ }
681
+ catch (err) {
682
+ if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
683
+ throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
684
+ table: this._config.tableName
685
+ }, err);
686
+ }
687
+ throw new core.GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
688
+ }
689
+ }
690
+ /**
691
+ * Build the condition expression for the query.
692
+ * @param conditions The conditions to build the expression from.
693
+ * @returns The condition expression.
694
+ * @throws GeneralError if the property is not found in the schema.
695
+ * @internal
696
+ */
697
+ buildConditionExpression(conditions) {
698
+ let conditionExpression;
699
+ let attributeNames;
700
+ let attributeValues;
701
+ if (core.Is.arrayValue(conditions)) {
702
+ const expressions = [];
703
+ for (const c of conditions) {
704
+ const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
705
+ if (core.Is.undefined(schemaProp)) {
706
+ throw new core.GeneralError(this.CLASS_NAME, "propertyNotFound", {
707
+ property: c.property
708
+ });
709
+ }
710
+ const attributeName = `#${c.property}`;
711
+ const attributeValueName = `:${c.property}`;
712
+ attributeNames ??= {};
713
+ attributeValues ??= {};
714
+ attributeNames[attributeName] = c.property;
715
+ attributeValues[attributeValueName] = c.value;
716
+ expressions.push(`${attributeName} = ${attributeValueName}`);
717
+ }
718
+ conditionExpression = expressions.join(" AND ");
719
+ }
720
+ return { conditionExpression, attributeNames, attributeValues };
721
+ }
659
722
  }
660
723
 
661
724
  exports.DynamoDbEntityStorageConnector = DynamoDbEntityStorageConnector;
@@ -1,7 +1,7 @@
1
- import { waitUntilTableExists, QueryCommand, DynamoDB } from '@aws-sdk/client-dynamodb';
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, Coerce } from '@twin.org/core';
4
+ import { Guards, Is, BaseError, GeneralError, Coerce, ObjectHelper, Converter } from '@twin.org/core';
5
5
  import { EntitySchemaFactory, EntitySchemaHelper, ComparisonOperator, LogicalOperator } from '@twin.org/entity';
6
6
  import { LoggingConnectorFactory } from '@twin.org/logging-models';
7
7
 
@@ -223,13 +223,14 @@ class DynamoDbEntityStorageConnector {
223
223
  * Get an entity.
224
224
  * @param id The id of the entity to get, or the index value if secondaryIndex is set.
225
225
  * @param secondaryIndex Get the item using a secondary index.
226
+ * @param conditions The optional conditions to match for the entities.
226
227
  * @returns The object if it can be found or undefined.
227
228
  */
228
- async get(id, secondaryIndex) {
229
+ async get(id, secondaryIndex, conditions) {
229
230
  Guards.stringValue(this.CLASS_NAME, "id", id);
230
231
  try {
231
232
  const docClient = this.createDocClient();
232
- if (Is.undefined(secondaryIndex)) {
233
+ if (Is.empty(secondaryIndex) && Is.empty(conditions)) {
233
234
  const getCommand = new GetCommand({
234
235
  TableName: this._config.tableName,
235
236
  Key: {
@@ -241,27 +242,27 @@ class DynamoDbEntityStorageConnector {
241
242
  delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
242
243
  return response.Item;
243
244
  }
244
- const secIndex = secondaryIndex.toString();
245
- const globalSecondaryIndex = `${secIndex}Index`;
246
- const queryCommand = new QueryCommand({
247
- TableName: this._config.tableName,
248
- IndexName: globalSecondaryIndex,
249
- KeyConditionExpression: `#${secIndex} = :id AND #${DynamoDbEntityStorageConnector._PARTITION_ID_NAME} = :${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`,
250
- ExpressionAttributeNames: {
251
- [`#${secIndex}`]: secIndex,
252
- [`#${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: DynamoDbEntityStorageConnector._PARTITION_ID_NAME
253
- },
254
- ExpressionAttributeValues: {
255
- [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
256
- S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
257
- },
258
- ":id": { S: id }
245
+ const finalConditions = {
246
+ conditions: []
247
+ };
248
+ if (Is.stringValue(secondaryIndex)) {
249
+ finalConditions.conditions.push({
250
+ property: secondaryIndex,
251
+ comparison: ComparisonOperator.Equals,
252
+ value: id
253
+ });
254
+ }
255
+ if (Is.arrayValue(conditions)) {
256
+ for (const c of conditions) {
257
+ finalConditions.conditions.push({
258
+ property: c.property,
259
+ comparison: ComparisonOperator.Equals,
260
+ value: c.value
261
+ });
259
262
  }
260
- });
261
- const response = await docClient.send(queryCommand);
262
- if (response.Items?.length === 1) {
263
- return unmarshall(response.Items[0]);
264
263
  }
264
+ const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex);
265
+ return queryResult.entities[0];
265
266
  }
266
267
  catch (err) {
267
268
  if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
@@ -273,24 +274,28 @@ class DynamoDbEntityStorageConnector {
273
274
  id
274
275
  }, err);
275
276
  }
276
- return undefined;
277
277
  }
278
278
  /**
279
279
  * Set an entity.
280
280
  * @param entity The entity to set.
281
+ * @param conditions The optional conditions to match for the entities.
281
282
  * @returns The id of the entity.
282
283
  */
283
- async set(entity) {
284
+ async set(entity, conditions) {
284
285
  Guards.object(this.CLASS_NAME, "entity", entity);
285
286
  const id = entity[this._primaryKey.property];
286
287
  try {
287
288
  const docClient = this.createDocClient();
289
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
288
290
  const putCommand = new PutCommand({
289
291
  TableName: this._config.tableName,
290
292
  Item: {
291
293
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
292
294
  ...entity
293
- }
295
+ },
296
+ ConditionExpression: conditionExpression,
297
+ ExpressionAttributeNames: attributeNames,
298
+ ExpressionAttributeValues: attributeValues
294
299
  });
295
300
  await docClient.send(putCommand);
296
301
  }
@@ -308,22 +313,30 @@ class DynamoDbEntityStorageConnector {
308
313
  /**
309
314
  * Remove the entity.
310
315
  * @param id The id of the entity to remove.
316
+ * @param conditions The optional conditions to match for the entities.
311
317
  * @returns Nothing.
312
318
  */
313
- async remove(id) {
319
+ async remove(id, conditions) {
314
320
  Guards.stringValue(this.CLASS_NAME, "id", id);
315
321
  try {
316
322
  const docClient = this.createDocClient();
323
+ const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
317
324
  const deleteCommand = new DeleteCommand({
318
325
  TableName: this._config.tableName,
319
326
  Key: {
320
327
  [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
321
328
  [this._primaryKey.property]: id
322
- }
329
+ },
330
+ ConditionExpression: conditionExpression,
331
+ ExpressionAttributeNames: attributeNames,
332
+ ExpressionAttributeValues: attributeValues
323
333
  });
324
334
  await docClient.send(deleteCommand);
325
335
  }
326
336
  catch (err) {
337
+ if (BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
338
+ return;
339
+ }
327
340
  if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
328
341
  throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
329
342
  table: this._config.tableName
@@ -345,77 +358,7 @@ class DynamoDbEntityStorageConnector {
345
358
  * and a cursor which can be used to request more entities.
346
359
  */
347
360
  async query(conditions, sortProperties, properties, cursor, pageSize) {
348
- try {
349
- const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
350
- let indexName;
351
- // If we have a sortable property defined in the descriptor then we must use
352
- // the secondary index for the query
353
- if (Is.arrayValue(sortProperties)) {
354
- if (sortProperties.length > 1) {
355
- throw new GeneralError(this.CLASS_NAME, "sortSingle");
356
- }
357
- for (const sortProperty of sortProperties) {
358
- const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
359
- if (Is.undefined(propertySchema) ||
360
- (!propertySchema.isPrimary &&
361
- !propertySchema.isSecondary &&
362
- Is.empty(propertySchema.sortDirection))) {
363
- throw new GeneralError(this.CLASS_NAME, "sortNotIndexed", {
364
- property: sortProperty.property
365
- });
366
- }
367
- indexName = propertySchema.isPrimary
368
- ? undefined
369
- : `${sortProperty.property}Index`;
370
- }
371
- }
372
- const attributeNames = { "#partitionId": "partitionId" };
373
- const attributeValues = {
374
- [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
375
- S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
376
- }
377
- };
378
- const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues);
379
- let keyExpression = "#partitionId = :partitionId";
380
- if (expressions.keyCondition.length > 0) {
381
- keyExpression += ` AND ${expressions.keyCondition}`;
382
- }
383
- const query = new QueryCommand({
384
- TableName: this._config.tableName,
385
- IndexName: indexName,
386
- KeyConditionExpression: keyExpression,
387
- FilterExpression: Is.stringValue(expressions.filterCondition)
388
- ? expressions.filterCondition
389
- : undefined,
390
- ExpressionAttributeNames: attributeNames,
391
- ExpressionAttributeValues: attributeValues,
392
- ProjectionExpression: properties?.map(p => p).join(", "),
393
- Limit: returnSize,
394
- ExclusiveStartKey: Is.empty(cursor)
395
- ? undefined
396
- : ObjectHelper.fromBytes(Converter.base64ToBytes(cursor))
397
- });
398
- const connection = this.createDocClient();
399
- const results = await connection.send(query);
400
- let entities = [];
401
- if (Is.arrayValue(results.Items)) {
402
- entities = results.Items.map(item => unmarshall(item));
403
- }
404
- return {
405
- entities,
406
- cursor: Is.empty(results.LastEvaluatedKey)
407
- ? undefined
408
- : Converter.bytesToBase64(ObjectHelper.toBytes(results.LastEvaluatedKey))
409
- };
410
- }
411
- catch (err) {
412
- if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
413
- throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
414
- table: this._config.tableName
415
- }, err);
416
- }
417
- throw new GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
418
- }
361
+ return this.internalQuery(conditions, sortProperties, properties, cursor, pageSize);
419
362
  }
420
363
  /**
421
364
  * Delete the table.
@@ -437,7 +380,7 @@ class DynamoDbEntityStorageConnector {
437
380
  * @returns The condition clause.
438
381
  * @internal
439
382
  */
440
- buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
383
+ buildQueryParameters(objectPath, condition, attributeNames, attributeValues, secondaryIndex) {
441
384
  // If no conditions are defined then return empty string
442
385
  if (Is.undefined(condition)) {
443
386
  return {
@@ -453,7 +396,7 @@ class DynamoDbEntityStorageConnector {
453
396
  };
454
397
  }
455
398
  // It's a group of comparisons, so check the individual items and combine with the logical operator
456
- const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues));
399
+ const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues, secondaryIndex));
457
400
  const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
458
401
  const keyCondition = joinConditions
459
402
  .filter(j => j.keyCondition.length > 0)
@@ -471,9 +414,10 @@ class DynamoDbEntityStorageConnector {
471
414
  const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
472
415
  // It's a single value so just create the property comparison for the condition
473
416
  const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
417
+ const isKey = schemaProp?.isPrimary || (schemaProp?.isSecondary && schemaProp?.property === secondaryIndex);
474
418
  return {
475
- keyCondition: schemaProp?.isPrimary ? comparison : "",
476
- filterCondition: schemaProp?.isPrimary ? "" : comparison
419
+ keyCondition: isKey ? comparison : "",
420
+ filterCondition: !isKey ? comparison : ""
477
421
  };
478
422
  }
479
423
  /**
@@ -654,6 +598,125 @@ class DynamoDbEntityStorageConnector {
654
598
  return false;
655
599
  }
656
600
  }
601
+ /**
602
+ * Find all the entities which match the conditions.
603
+ * @param conditions The conditions to match for the entities.
604
+ * @param sortProperties The optional sort order.
605
+ * @param properties The optional properties to return, defaults to all.
606
+ * @param cursor The cursor to request the next page of entities.
607
+ * @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
608
+ * @param secondaryIndex The secondary index to use for the query.
609
+ * @returns All the entities for the storage matching the conditions,
610
+ * and a cursor which can be used to request more entities.
611
+ * @internal
612
+ */
613
+ async internalQuery(conditions, sortProperties, properties, cursor, pageSize, secondaryIndex) {
614
+ try {
615
+ const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
616
+ let indexName = Is.stringValue(secondaryIndex)
617
+ ? `${secondaryIndex}Index`
618
+ : undefined;
619
+ // If we have a sortable property defined in the descriptor then we must use
620
+ // the secondary index for the query
621
+ if (Is.arrayValue(sortProperties)) {
622
+ if (sortProperties.length > 1) {
623
+ throw new GeneralError(this.CLASS_NAME, "sortSingle");
624
+ }
625
+ for (const sortProperty of sortProperties) {
626
+ const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
627
+ if (Is.undefined(propertySchema) ||
628
+ (!propertySchema.isPrimary &&
629
+ !propertySchema.isSecondary &&
630
+ Is.empty(propertySchema.sortDirection))) {
631
+ throw new GeneralError(this.CLASS_NAME, "sortNotIndexed", {
632
+ property: sortProperty.property
633
+ });
634
+ }
635
+ indexName = propertySchema.isPrimary
636
+ ? undefined
637
+ : `${sortProperty.property}Index`;
638
+ }
639
+ }
640
+ const attributeNames = { "#partitionId": "partitionId" };
641
+ const attributeValues = {
642
+ [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
643
+ S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
644
+ }
645
+ };
646
+ const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues, secondaryIndex);
647
+ let keyExpression = "#partitionId = :partitionId";
648
+ if (expressions.keyCondition.length > 0) {
649
+ keyExpression += ` AND ${expressions.keyCondition}`;
650
+ }
651
+ const query = new QueryCommand({
652
+ TableName: this._config.tableName,
653
+ IndexName: indexName,
654
+ KeyConditionExpression: keyExpression,
655
+ FilterExpression: Is.stringValue(expressions.filterCondition)
656
+ ? expressions.filterCondition
657
+ : undefined,
658
+ ExpressionAttributeNames: attributeNames,
659
+ ExpressionAttributeValues: attributeValues,
660
+ ProjectionExpression: properties?.map(p => p).join(", "),
661
+ Limit: returnSize,
662
+ ExclusiveStartKey: Is.empty(cursor)
663
+ ? undefined
664
+ : ObjectHelper.fromBytes(Converter.base64ToBytes(cursor))
665
+ });
666
+ const connection = this.createDocClient();
667
+ const results = await connection.send(query);
668
+ let entities = [];
669
+ if (Is.arrayValue(results.Items)) {
670
+ entities = results.Items.map(item => unmarshall(item));
671
+ }
672
+ return {
673
+ entities,
674
+ cursor: Is.empty(results.LastEvaluatedKey)
675
+ ? undefined
676
+ : Converter.bytesToBase64(ObjectHelper.toBytes(results.LastEvaluatedKey))
677
+ };
678
+ }
679
+ catch (err) {
680
+ if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
681
+ throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
682
+ table: this._config.tableName
683
+ }, err);
684
+ }
685
+ throw new GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
686
+ }
687
+ }
688
+ /**
689
+ * Build the condition expression for the query.
690
+ * @param conditions The conditions to build the expression from.
691
+ * @returns The condition expression.
692
+ * @throws GeneralError if the property is not found in the schema.
693
+ * @internal
694
+ */
695
+ buildConditionExpression(conditions) {
696
+ let conditionExpression;
697
+ let attributeNames;
698
+ let attributeValues;
699
+ if (Is.arrayValue(conditions)) {
700
+ const expressions = [];
701
+ for (const c of conditions) {
702
+ const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
703
+ if (Is.undefined(schemaProp)) {
704
+ throw new GeneralError(this.CLASS_NAME, "propertyNotFound", {
705
+ property: c.property
706
+ });
707
+ }
708
+ const attributeName = `#${c.property}`;
709
+ const attributeValueName = `:${c.property}`;
710
+ attributeNames ??= {};
711
+ attributeValues ??= {};
712
+ attributeNames[attributeName] = c.property;
713
+ attributeValues[attributeValueName] = c.value;
714
+ expressions.push(`${attributeName} = ${attributeValueName}`);
715
+ }
716
+ conditionExpression = expressions.join(" AND ");
717
+ }
718
+ return { conditionExpression, attributeNames, attributeValues };
719
+ }
657
720
  }
658
721
 
659
722
  export { DynamoDbEntityStorageConnector };
@@ -36,21 +36,33 @@ export declare class DynamoDbEntityStorageConnector<T = unknown> implements IEnt
36
36
  * Get an entity.
37
37
  * @param id The id of the entity to get, or the index value if secondaryIndex is set.
38
38
  * @param secondaryIndex Get the item using a secondary index.
39
+ * @param conditions The optional conditions to match for the entities.
39
40
  * @returns The object if it can be found or undefined.
40
41
  */
41
- get(id: string, secondaryIndex?: keyof T): Promise<T | undefined>;
42
+ get(id: string, secondaryIndex?: keyof T, conditions?: {
43
+ property: keyof T;
44
+ value: unknown;
45
+ }[]): Promise<T | undefined>;
42
46
  /**
43
47
  * Set an entity.
44
48
  * @param entity The entity to set.
49
+ * @param conditions The optional conditions to match for the entities.
45
50
  * @returns The id of the entity.
46
51
  */
47
- set(entity: T): Promise<void>;
52
+ set(entity: T, conditions?: {
53
+ property: keyof T;
54
+ value: unknown;
55
+ }[]): Promise<void>;
48
56
  /**
49
57
  * Remove the entity.
50
58
  * @param id The id of the entity to remove.
59
+ * @param conditions The optional conditions to match for the entities.
51
60
  * @returns Nothing.
52
61
  */
53
- remove(id: string): Promise<void>;
62
+ remove(id: string, conditions?: {
63
+ property: keyof T;
64
+ value: unknown;
65
+ }[]): Promise<void>;
54
66
  /**
55
67
  * Find all the entities which match the conditions.
56
68
  * @param conditions The conditions to match for the entities.
package/docs/changelog.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # @twin.org/entity-storage-connector-dynamodb - Changelog
2
2
 
3
- ## v0.0.1-next.7
3
+ ## v0.0.1-next.9
4
4
 
5
5
  - Initial Release
@@ -98,7 +98,7 @@ The schema for the entities.
98
98
 
99
99
  ### get()
100
100
 
101
- > **get**(`id`, `secondaryIndex`?): `Promise`\<`undefined` \| `T`\>
101
+ > **get**(`id`, `secondaryIndex`?, `conditions`?): `Promise`\<`undefined` \| `T`\>
102
102
 
103
103
  Get an entity.
104
104
 
@@ -112,6 +112,10 @@ The id of the entity to get, or the index value if secondaryIndex is set.
112
112
 
113
113
  Get the item using a secondary index.
114
114
 
115
+ • **conditions?**: `object`[]
116
+
117
+ The optional conditions to match for the entities.
118
+
115
119
  #### Returns
116
120
 
117
121
  `Promise`\<`undefined` \| `T`\>
@@ -126,7 +130,7 @@ The object if it can be found or undefined.
126
130
 
127
131
  ### set()
128
132
 
129
- > **set**(`entity`): `Promise`\<`void`\>
133
+ > **set**(`entity`, `conditions`?): `Promise`\<`void`\>
130
134
 
131
135
  Set an entity.
132
136
 
@@ -136,6 +140,10 @@ Set an entity.
136
140
 
137
141
  The entity to set.
138
142
 
143
+ • **conditions?**: `object`[]
144
+
145
+ The optional conditions to match for the entities.
146
+
139
147
  #### Returns
140
148
 
141
149
  `Promise`\<`void`\>
@@ -150,7 +158,7 @@ The id of the entity.
150
158
 
151
159
  ### remove()
152
160
 
153
- > **remove**(`id`): `Promise`\<`void`\>
161
+ > **remove**(`id`, `conditions`?): `Promise`\<`void`\>
154
162
 
155
163
  Remove the entity.
156
164
 
@@ -160,6 +168,10 @@ Remove the entity.
160
168
 
161
169
  The id of the entity to remove.
162
170
 
171
+ • **conditions?**: `object`[]
172
+
173
+ The optional conditions to match for the entities.
174
+
163
175
  #### Returns
164
176
 
165
177
  `Promise`\<`void`\>
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.7",
3
+ "version": "0.0.1-next.9",
4
4
  "description": "Entity Storage connector implementation using DynamoDb storage",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,7 @@
19
19
  "@aws-sdk/util-dynamodb": "3.656",
20
20
  "@twin.org/core": "next",
21
21
  "@twin.org/entity": "next",
22
- "@twin.org/entity-storage-models": "0.0.1-next.7",
22
+ "@twin.org/entity-storage-models": "0.0.1-next.9",
23
23
  "@twin.org/logging-models": "next",
24
24
  "@twin.org/nameof": "next"
25
25
  },