@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 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,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
- 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
- }
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 an SQL condition clause.
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.map(j => j.keyCondition).join(` ${logicalOperator} `);
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: schemaProp?.isPrimary ? comparison : "",
464
- filterCondition: schemaProp?.isPrimary ? "" : comparison
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;
@@ -1,8 +1,8 @@
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';
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.undefined(secondaryIndex)) {
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 secIndex = secondaryIndex.toString();
238
- const globalSecondaryIndex = `${secIndex}Index`;
239
- const queryCommand = new QueryCommand({
240
- TableName: this._config.tableName,
241
- IndexName: globalSecondaryIndex,
242
- KeyConditionExpression: `#${secIndex} = :id AND #${DynamoDbEntityStorageConnector._PARTITION_ID_NAME} = :${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`,
243
- ExpressionAttributeNames: {
244
- [`#${secIndex}`]: secIndex,
245
- [`#${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: DynamoDbEntityStorageConnector._PARTITION_ID_NAME
246
- },
247
- ExpressionAttributeValues: {
248
- [`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
249
- S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
250
- },
251
- ":id": { S: id }
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
- const sql = "";
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 an SQL condition clause.
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.map(j => j.keyCondition).join(` ${logicalOperator} `);
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: schemaProp?.isPrimary ? comparison : "",
462
- filterCondition: schemaProp?.isPrimary ? "" : comparison
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 { IDynamoDbEntityStorageConnectorConfig } from "./models/IDynamoDbEntityStorageConnectorConfig";
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): Promise<T | undefined>;
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): Promise<void>;
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): Promise<void>;
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.
@@ -1,2 +1,3 @@
1
1
  export * from "./dynamoDbEntityStorageConnector";
2
2
  export * from "./models/IDynamoDbEntityStorageConnectorConfig";
3
+ export * from "./models/IDynamoDbEntityStorageConnectorConstructorOptions";
@@ -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
@@ -1,5 +1,5 @@
1
1
  # @twin.org/entity-storage-connector-dynamodb - Changelog
2
2
 
3
- ## v0.0.1-next.2
3
+ ## v0.0.1-next.21
4
4
 
5
5
  - Initial Release
@@ -20,21 +20,11 @@ Create a new instance of DynamoDbEntityStorageConnector.
20
20
 
21
21
  #### Parameters
22
22
 
23
- **options**
23
+ ##### options
24
24
 
25
- The options for the connector.
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 configuration for the connector.
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
- **nodeLoggingConnectorType?**: `string`
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
- **id**: `string`
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
- **secondaryIndex?**: keyof `T`
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
- **entity**: `T`
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
- **id**: `string`
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`\<`object`\>
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
- **conditions?**: `EntityCondition`\<`T`\>
201
+ ##### conditions?
202
+
203
+ `EntityCondition`\<`T`\>
166
204
 
167
205
  The conditions to match for the entities.
168
206
 
169
- **sortProperties?**: `object`[]
207
+ ##### sortProperties?
208
+
209
+ `object`[]
170
210
 
171
211
  The optional sort order.
172
212
 
173
- **properties?**: keyof `T`[]
213
+ ##### properties?
214
+
215
+ keyof `T`[]
174
216
 
175
217
  The optional properties to return, defaults to all.
176
218
 
177
- **cursor?**: `string`
219
+ ##### cursor?
220
+
221
+ `string`
178
222
 
179
223
  The cursor to request the next page of entities.
180
224
 
181
- **pageSize?**: `number`
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`\<`object`\>
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`
@@ -7,3 +7,4 @@
7
7
  ## Interfaces
8
8
 
9
9
  - [IDynamoDbEntityStorageConnectorConfig](interfaces/IDynamoDbEntityStorageConnectorConfig.md)
10
+ - [IDynamoDbEntityStorageConnectorConstructorOptions](interfaces/IDynamoDbEntityStorageConnectorConstructorOptions.md)
@@ -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 when issuing the following command \"{sql}\"",
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.2",
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/nameof": "next",
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
- "@aws-sdk/client-dynamodb": "3.651",
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",