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

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.
@@ -0,0 +1,645 @@
1
+ import { waitUntilTableExists, QueryCommand, DynamoDB } from '@aws-sdk/client-dynamodb';
2
+ import { GetCommand, PutCommand, DeleteCommand, DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
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';
6
+ import { LoggingConnectorFactory } from '@twin.org/logging-models';
7
+
8
+ // Copyright 2024 IOTA Stiftung.
9
+ // SPDX-License-Identifier: Apache-2.0.
10
+ /**
11
+ * Class for performing entity storage operations using Dynamo DB.
12
+ */
13
+ class DynamoDbEntityStorageConnector {
14
+ /**
15
+ * Limit the number of entities when finding.
16
+ * @internal
17
+ */
18
+ static _PAGE_SIZE = 40;
19
+ /**
20
+ * Partition id field name.
21
+ * @internal
22
+ */
23
+ static _PARTITION_ID_NAME = "partitionId";
24
+ /**
25
+ * Partition id field value.
26
+ * @internal
27
+ */
28
+ static _PARTITION_ID_VALUE = "1";
29
+ /**
30
+ * Runtime name for the class.
31
+ */
32
+ CLASS_NAME = "DynamoDbEntityStorageConnector";
33
+ /**
34
+ * The schema for the entity.
35
+ * @internal
36
+ */
37
+ _entitySchema;
38
+ /**
39
+ * The primary key.
40
+ * @internal
41
+ */
42
+ _primaryKey;
43
+ /**
44
+ * The configuration for the connector.
45
+ * @internal
46
+ */
47
+ _config;
48
+ /**
49
+ * Create a new instance of DynamoDbEntityStorageConnector.
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
+ */
55
+ constructor(options) {
56
+ Guards.object(this.CLASS_NAME, "options", options);
57
+ Guards.stringValue(this.CLASS_NAME, "options.entitySchema", options.entitySchema);
58
+ Guards.object(this.CLASS_NAME, "options.config", options.config);
59
+ Guards.stringValue(this.CLASS_NAME, "options.config.accessKeyId", options.config.accessKeyId);
60
+ Guards.stringValue(this.CLASS_NAME, "options.config.secretAccessKey", options.config.secretAccessKey);
61
+ Guards.stringValue(this.CLASS_NAME, "options.config.region", options.config.region);
62
+ Guards.stringValue(this.CLASS_NAME, "options.config.tableName", options.config.tableName);
63
+ this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
64
+ this._primaryKey = EntitySchemaHelper.getPrimaryKey(this._entitySchema);
65
+ this._config = options.config;
66
+ this._config.endpoint = Is.stringValue(this._config.endpoint)
67
+ ? this._config.endpoint
68
+ : undefined;
69
+ }
70
+ /**
71
+ * Bootstrap the component by creating and initializing any resources it needs.
72
+ * @param nodeLoggingConnectorType The node logging connector type, defaults to "node-logging".
73
+ * @returns True if the bootstrapping process was successful.
74
+ */
75
+ async bootstrap(nodeLoggingConnectorType) {
76
+ const nodeLogging = LoggingConnectorFactory.getIfExists(nodeLoggingConnectorType ?? "node-logging");
77
+ if (!(await this.tableExists(this._config.tableName))) {
78
+ await nodeLogging?.log({
79
+ level: "info",
80
+ source: this.CLASS_NAME,
81
+ ts: Date.now(),
82
+ message: "tableCreating",
83
+ data: {
84
+ tableName: this._config.tableName
85
+ }
86
+ });
87
+ try {
88
+ const dbConnection = this.createConnection();
89
+ const tableParams = {
90
+ AttributeDefinitions: [],
91
+ KeySchema: [],
92
+ ProvisionedThroughput: {
93
+ ReadCapacityUnits: 1,
94
+ WriteCapacityUnits: 1
95
+ },
96
+ TableName: this._config.tableName
97
+ };
98
+ // We always add a partition key to the table as a non optional hash key
99
+ // is always required when querying using sort parameters
100
+ tableParams.AttributeDefinitions?.push({
101
+ AttributeName: DynamoDbEntityStorageConnector._PARTITION_ID_NAME,
102
+ AttributeType: "S"
103
+ });
104
+ tableParams.KeySchema?.push({
105
+ AttributeName: DynamoDbEntityStorageConnector._PARTITION_ID_NAME,
106
+ KeyType: "HASH"
107
+ });
108
+ const gsi = [];
109
+ if (Is.arrayValue(this._entitySchema.properties)) {
110
+ for (const prop of this._entitySchema.properties) {
111
+ if (prop.isPrimary) {
112
+ tableParams.AttributeDefinitions?.push({
113
+ AttributeName: prop.property,
114
+ AttributeType: prop.type === "integer" || prop.type === "number" ? "N" : "S"
115
+ });
116
+ tableParams.KeySchema?.push({
117
+ AttributeName: prop.property,
118
+ KeyType: "RANGE"
119
+ });
120
+ }
121
+ else if (Is.stringValue(prop.sortDirection) || prop.isSecondary) {
122
+ // You can only query and sort items if you have a secondary index
123
+ // defined for the property
124
+ tableParams.AttributeDefinitions?.push({
125
+ AttributeName: prop.property,
126
+ AttributeType: prop.type === "integer" || prop.type === "number" ? "N" : "S"
127
+ });
128
+ gsi.push({
129
+ IndexName: `${prop.property}Index`,
130
+ KeySchema: [
131
+ {
132
+ AttributeName: DynamoDbEntityStorageConnector._PARTITION_ID_NAME,
133
+ KeyType: "HASH"
134
+ },
135
+ {
136
+ AttributeName: prop.property,
137
+ KeyType: "RANGE"
138
+ }
139
+ ],
140
+ Projection: {
141
+ ProjectionType: "ALL"
142
+ },
143
+ ProvisionedThroughput: {
144
+ ReadCapacityUnits: 1,
145
+ WriteCapacityUnits: 1
146
+ }
147
+ });
148
+ }
149
+ }
150
+ }
151
+ if (gsi.length > 0) {
152
+ tableParams.GlobalSecondaryIndexes = gsi;
153
+ }
154
+ await dbConnection.createTable(tableParams);
155
+ // Wait for table to exist
156
+ await waitUntilTableExists({
157
+ client: dbConnection,
158
+ maxWaitTime: 60000
159
+ }, {
160
+ TableName: this._config.tableName
161
+ });
162
+ await nodeLogging?.log({
163
+ level: "info",
164
+ source: this.CLASS_NAME,
165
+ ts: Date.now(),
166
+ message: "tableCreated",
167
+ data: {
168
+ tableName: this._config.tableName
169
+ }
170
+ });
171
+ }
172
+ catch (err) {
173
+ if (BaseError.isErrorCode(err, "ResourceInUseException")) {
174
+ await nodeLogging?.log({
175
+ level: "info",
176
+ source: this.CLASS_NAME,
177
+ ts: Date.now(),
178
+ message: "tableExists",
179
+ data: {
180
+ tableName: this._config.tableName
181
+ }
182
+ });
183
+ }
184
+ else {
185
+ const errors = err instanceof AggregateError ? err.errors : [err];
186
+ for (const error of errors) {
187
+ await nodeLogging?.log({
188
+ level: "error",
189
+ source: this.CLASS_NAME,
190
+ ts: Date.now(),
191
+ message: "tableCreateFailed",
192
+ error: BaseError.fromError(error),
193
+ data: {
194
+ tableName: this._config.tableName
195
+ }
196
+ });
197
+ }
198
+ }
199
+ return false;
200
+ }
201
+ }
202
+ else {
203
+ await nodeLogging?.log({
204
+ level: "info",
205
+ source: this.CLASS_NAME,
206
+ ts: Date.now(),
207
+ message: "tableExists",
208
+ data: {
209
+ tableName: this._config.tableName
210
+ }
211
+ });
212
+ }
213
+ return true;
214
+ }
215
+ /**
216
+ * Get an entity.
217
+ * @param id The id of the entity to get, or the index value if secondaryIndex is set.
218
+ * @param secondaryIndex Get the item using a secondary index.
219
+ * @returns The object if it can be found or undefined.
220
+ */
221
+ async get(id, secondaryIndex) {
222
+ Guards.stringValue(this.CLASS_NAME, "id", id);
223
+ try {
224
+ const docClient = this.createDocClient();
225
+ if (Is.undefined(secondaryIndex)) {
226
+ const getCommand = new GetCommand({
227
+ TableName: this._config.tableName,
228
+ Key: {
229
+ [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
230
+ [this._primaryKey.property]: id
231
+ }
232
+ });
233
+ const response = await docClient.send(getCommand);
234
+ delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
235
+ return response.Item;
236
+ }
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 }
252
+ }
253
+ });
254
+ const response = await docClient.send(queryCommand);
255
+ if (response.Items?.length === 1) {
256
+ return unmarshall(response.Items[0]);
257
+ }
258
+ }
259
+ catch (err) {
260
+ if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
261
+ throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
262
+ table: this._config.tableName
263
+ }, err);
264
+ }
265
+ throw new GeneralError(this.CLASS_NAME, "getFailed", {
266
+ id
267
+ }, err);
268
+ }
269
+ return undefined;
270
+ }
271
+ /**
272
+ * Set an entity.
273
+ * @param entity The entity to set.
274
+ * @returns The id of the entity.
275
+ */
276
+ async set(entity) {
277
+ Guards.object(this.CLASS_NAME, "entity", entity);
278
+ const id = entity[this._primaryKey.property];
279
+ try {
280
+ const docClient = this.createDocClient();
281
+ const putCommand = new PutCommand({
282
+ TableName: this._config.tableName,
283
+ Item: {
284
+ [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
285
+ ...entity
286
+ }
287
+ });
288
+ await docClient.send(putCommand);
289
+ }
290
+ catch (err) {
291
+ if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
292
+ throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
293
+ tableName: this._config.tableName
294
+ }, err);
295
+ }
296
+ throw new GeneralError(this.CLASS_NAME, "setFailed", {
297
+ id
298
+ }, err);
299
+ }
300
+ }
301
+ /**
302
+ * Remove the entity.
303
+ * @param id The id of the entity to remove.
304
+ * @returns Nothing.
305
+ */
306
+ async remove(id) {
307
+ Guards.stringValue(this.CLASS_NAME, "id", id);
308
+ try {
309
+ const docClient = this.createDocClient();
310
+ const deleteCommand = new DeleteCommand({
311
+ TableName: this._config.tableName,
312
+ Key: {
313
+ [DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
314
+ [this._primaryKey.property]: id
315
+ }
316
+ });
317
+ await docClient.send(deleteCommand);
318
+ }
319
+ catch (err) {
320
+ if (BaseError.isErrorCode(err, "ResourceNotFoundException")) {
321
+ throw new GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
322
+ table: this._config.tableName
323
+ }, err);
324
+ }
325
+ throw new GeneralError(this.CLASS_NAME, "removeFailed", {
326
+ id
327
+ }, err);
328
+ }
329
+ }
330
+ /**
331
+ * Find all the entities which match the conditions.
332
+ * @param conditions The conditions to match for the entities.
333
+ * @param sortProperties The optional sort order.
334
+ * @param properties The optional properties to return, defaults to all.
335
+ * @param cursor The cursor to request the next page of entities.
336
+ * @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
337
+ * @returns All the entities for the storage matching the conditions,
338
+ * and a cursor which can be used to request more entities.
339
+ */
340
+ 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
+ }
415
+ }
416
+ /**
417
+ * Delete the table.
418
+ * @returns Nothing.
419
+ */
420
+ async tableDelete() {
421
+ try {
422
+ const dbConnection = this.createConnection();
423
+ await dbConnection.deleteTable({ TableName: this._config.tableName });
424
+ }
425
+ catch { }
426
+ }
427
+ /**
428
+ * Create an SQL condition clause.
429
+ * @param objectPath The path for the nested object.
430
+ * @param condition The conditions to create the query from.
431
+ * @param attributeNames The attribute names to use in the query.
432
+ * @param attributeValues The attribute values to use in the query.
433
+ * @returns The condition clause.
434
+ * @internal
435
+ */
436
+ buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
437
+ // If no conditions are defined then return empty string
438
+ if (Is.undefined(condition)) {
439
+ return {
440
+ keyCondition: "",
441
+ filterCondition: ""
442
+ };
443
+ }
444
+ if ("conditions" in condition) {
445
+ // 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));
447
+ const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
448
+ const keyCondition = joinConditions.map(j => j.keyCondition).join(` ${logicalOperator} `);
449
+ const filterCondition = joinConditions
450
+ .map(j => j.filterCondition)
451
+ .join(` ${logicalOperator} `);
452
+ return {
453
+ keyCondition: Is.stringValue(keyCondition) ? ` (${keyCondition}) ` : "",
454
+ filterCondition: Is.stringValue(filterCondition) ? ` (${filterCondition}) ` : ""
455
+ };
456
+ }
457
+ const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
458
+ // It's a single value so just create the property comparison for the condition
459
+ const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
460
+ return {
461
+ keyCondition: schemaProp?.isPrimary ? comparison : "",
462
+ filterCondition: schemaProp?.isPrimary ? "" : comparison
463
+ };
464
+ }
465
+ /**
466
+ * Map the framework comparison operators to those in DynamoDB.
467
+ * @param objectPath The prefix to use for the condition.
468
+ * @param comparator The operator to map.
469
+ * @param type The type of the property.
470
+ * @param attributeNames The attribute names to use in the query.
471
+ * @param attributeValues The attribute values to use in the query.
472
+ * @returns The comparison expression.
473
+ * @throws GeneralError if the comparison operator is not supported.
474
+ * @internal
475
+ */
476
+ mapComparisonOperator(objectPath, comparator, type, attributeNames, attributeValues) {
477
+ let prop = objectPath;
478
+ if (prop.length > 0) {
479
+ prop += ".";
480
+ }
481
+ prop += comparator.property;
482
+ let attributeName = this.populateAttributeNames(prop, attributeNames);
483
+ let propName = `:${attributeName.replace(/\./g, "").replace(/#/g, "")}`;
484
+ if (Is.array(comparator.value)) {
485
+ const dbValues = comparator.value.map(v => this.propertyToDbValue(v, type));
486
+ const arrAttributeNames = [];
487
+ for (let i = 0; i < dbValues.length; i++) {
488
+ const arrAttributeName = `${propName}${i}`;
489
+ attributeValues[arrAttributeName] = dbValues[i];
490
+ arrAttributeNames.push(arrAttributeName);
491
+ }
492
+ propName = attributeName;
493
+ attributeName = `(${arrAttributeNames.join(", ")})`;
494
+ }
495
+ else {
496
+ attributeValues[propName] = this.propertyToDbValue(comparator.value, type);
497
+ }
498
+ if (comparator.comparison === ComparisonOperator.Equals) {
499
+ return `${attributeName} = ${propName}`;
500
+ }
501
+ else if (comparator.comparison === ComparisonOperator.NotEquals) {
502
+ return `${attributeName} <> ${propName}`;
503
+ }
504
+ else if (comparator.comparison === ComparisonOperator.GreaterThan) {
505
+ return `${attributeName} > ${propName}`;
506
+ }
507
+ else if (comparator.comparison === ComparisonOperator.LessThan) {
508
+ return `${attributeName} < ${propName}`;
509
+ }
510
+ else if (comparator.comparison === ComparisonOperator.GreaterThanOrEqual) {
511
+ return `${attributeName} >= ${propName}`;
512
+ }
513
+ else if (comparator.comparison === ComparisonOperator.LessThanOrEqual) {
514
+ return `${attributeName} <= ${propName}`;
515
+ }
516
+ else if (comparator.comparison === ComparisonOperator.Includes) {
517
+ return `contains(${attributeName}, ${propName})`;
518
+ }
519
+ else if (comparator.comparison === ComparisonOperator.NotIncludes) {
520
+ return `notContains(${attributeName}, ${propName})`;
521
+ }
522
+ else if (comparator.comparison === ComparisonOperator.In) {
523
+ return `${propName} IN ${attributeName}`;
524
+ }
525
+ throw new GeneralError(this.CLASS_NAME, "comparisonNotSupported", {
526
+ comparison: comparator.comparison
527
+ });
528
+ }
529
+ /**
530
+ * Create a unique name for the attribute.
531
+ * @param name The name to create a unique name for.
532
+ * @param attributeNames The attribute names to use in the query.
533
+ * @returns The unique name.
534
+ * @internal
535
+ */
536
+ populateAttributeNames(name, attributeNames) {
537
+ const parts = name.split(".");
538
+ const attributeNameParts = [];
539
+ for (const part of parts) {
540
+ const hashPart = `#${part}`;
541
+ if (Is.empty(attributeNames[hashPart])) {
542
+ attributeNames[hashPart] = part;
543
+ }
544
+ attributeNameParts.push(hashPart);
545
+ }
546
+ return attributeNameParts.join(".");
547
+ }
548
+ /**
549
+ * Map the framework conditional operators to those in DynamoDB.
550
+ * @param operator The operator to map.
551
+ * @returns The conditional operator.
552
+ * @throws GeneralError if the conditional operator is not supported.
553
+ * @internal
554
+ */
555
+ mapConditionalOperator(operator) {
556
+ if ((operator ?? LogicalOperator.And) === LogicalOperator.And) {
557
+ return "AND";
558
+ }
559
+ else if (operator === LogicalOperator.Or) {
560
+ return "OR";
561
+ }
562
+ throw new GeneralError(this.CLASS_NAME, "conditionalNotSupported", { operator });
563
+ }
564
+ /**
565
+ * Format a value to insert into DB.
566
+ * @param value The value to format.
567
+ * @param type The type for the property.
568
+ * @returns The value after conversion.
569
+ * @internal
570
+ */
571
+ propertyToDbValue(value, type) {
572
+ if (Is.object(value)) {
573
+ const map = {};
574
+ for (const key in value) {
575
+ map[key] = this.propertyToDbValue(value[key]);
576
+ }
577
+ return {
578
+ M: map
579
+ };
580
+ }
581
+ if (type === "integer" || type === "number") {
582
+ return { N: Coerce.string(value) ?? "" };
583
+ }
584
+ else if (type === "boolean") {
585
+ return { BOOL: Coerce.boolean(value) ?? false };
586
+ }
587
+ return { S: Coerce.string(value) ?? "" };
588
+ }
589
+ /**
590
+ * Create a doc client connection.
591
+ * @returns The dynamo db document client.
592
+ * @internal
593
+ */
594
+ createDocClient() {
595
+ return DynamoDBDocumentClient.from(new DynamoDB({
596
+ apiVersion: "2012-10-08",
597
+ ...this.createConnectionConfig()
598
+ }), {
599
+ marshallOptions: {
600
+ removeUndefinedValues: true
601
+ }
602
+ });
603
+ }
604
+ /**
605
+ * Create a new DB connection.
606
+ * @returns The dynamo db connection.
607
+ * @internal
608
+ */
609
+ createConnection() {
610
+ return new DynamoDB(this.createConnectionConfig());
611
+ }
612
+ /**
613
+ * Create a new DB connection configuration.
614
+ * @returns The dynamo db connection configuration.
615
+ * @internal
616
+ */
617
+ createConnectionConfig() {
618
+ return {
619
+ credentials: {
620
+ accessKeyId: this._config.accessKeyId,
621
+ secretAccessKey: this._config.secretAccessKey
622
+ },
623
+ endpoint: this._config.endpoint,
624
+ region: this._config.region
625
+ };
626
+ }
627
+ /**
628
+ * Check if the table exists.
629
+ * @param tableName The table to check.
630
+ * @returns True if the table exists.
631
+ * @internal
632
+ */
633
+ async tableExists(tableName) {
634
+ try {
635
+ const dbConnection = this.createConnection();
636
+ await dbConnection.describeTable({ TableName: tableName });
637
+ return true;
638
+ }
639
+ catch {
640
+ return false;
641
+ }
642
+ }
643
+ }
644
+
645
+ export { DynamoDbEntityStorageConnector };