@twin.org/entity-storage-connector-dynamodb 0.0.2-next.9 → 0.0.3-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.
- package/README.md +1 -1
- package/dist/{esm/index.mjs → es/dynamoDbEntityStorageConnector.js} +96 -73
- package/dist/es/dynamoDbEntityStorageConnector.js.map +1 -0
- package/dist/es/index.js +6 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConfig.js +4 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConfig.js.map +1 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConstructorOptions.js +2 -0
- package/dist/es/models/IDynamoDbEntityStorageConnectorConstructorOptions.js.map +1 -0
- package/dist/types/dynamoDbEntityStorageConnector.d.ts +14 -9
- package/dist/types/index.d.ts +3 -3
- package/dist/types/models/IDynamoDbEntityStorageConnectorConfig.d.ts +1 -1
- package/dist/types/models/IDynamoDbEntityStorageConnectorConstructorOptions.d.ts +5 -1
- package/docs/changelog.md +65 -0
- package/docs/reference/classes/DynamoDbEntityStorageConnector.md +38 -24
- package/docs/reference/interfaces/IDynamoDbEntityStorageConnectorConfig.md +1 -1
- package/docs/reference/interfaces/IDynamoDbEntityStorageConnectorConstructorOptions.md +8 -0
- package/locales/en.json +2 -1
- package/package.json +15 -12
- package/dist/cjs/index.cjs +0 -751
package/dist/cjs/index.cjs
DELETED
|
@@ -1,751 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var clientDynamodb = require('@aws-sdk/client-dynamodb');
|
|
4
|
-
var libDynamodb = require('@aws-sdk/lib-dynamodb');
|
|
5
|
-
var utilDynamodb = require('@aws-sdk/util-dynamodb');
|
|
6
|
-
var core = require('@twin.org/core');
|
|
7
|
-
var entity = require('@twin.org/entity');
|
|
8
|
-
|
|
9
|
-
// Copyright 2024 IOTA Stiftung.
|
|
10
|
-
// SPDX-License-Identifier: Apache-2.0.
|
|
11
|
-
/**
|
|
12
|
-
* Class for performing entity storage operations using Dynamo DB.
|
|
13
|
-
*/
|
|
14
|
-
class DynamoDbEntityStorageConnector {
|
|
15
|
-
/**
|
|
16
|
-
* Limit the number of entities when finding.
|
|
17
|
-
* @internal
|
|
18
|
-
*/
|
|
19
|
-
static _PAGE_SIZE = 40;
|
|
20
|
-
/**
|
|
21
|
-
* Partition id field name.
|
|
22
|
-
* @internal
|
|
23
|
-
*/
|
|
24
|
-
static _PARTITION_ID_NAME = "partitionId";
|
|
25
|
-
/**
|
|
26
|
-
* Partition id field value.
|
|
27
|
-
* @internal
|
|
28
|
-
*/
|
|
29
|
-
static _PARTITION_ID_VALUE = "1";
|
|
30
|
-
/**
|
|
31
|
-
* Runtime name for the class.
|
|
32
|
-
*/
|
|
33
|
-
CLASS_NAME = "DynamoDbEntityStorageConnector";
|
|
34
|
-
/**
|
|
35
|
-
* The schema for the entity.
|
|
36
|
-
* @internal
|
|
37
|
-
*/
|
|
38
|
-
_entitySchema;
|
|
39
|
-
/**
|
|
40
|
-
* The primary key.
|
|
41
|
-
* @internal
|
|
42
|
-
*/
|
|
43
|
-
_primaryKey;
|
|
44
|
-
/**
|
|
45
|
-
* The configuration for the connector.
|
|
46
|
-
* @internal
|
|
47
|
-
*/
|
|
48
|
-
_config;
|
|
49
|
-
/**
|
|
50
|
-
* Create a new instance of DynamoDbEntityStorageConnector.
|
|
51
|
-
* @param options The options for the connector.
|
|
52
|
-
*/
|
|
53
|
-
constructor(options) {
|
|
54
|
-
core.Guards.object(this.CLASS_NAME, "options", options);
|
|
55
|
-
core.Guards.stringValue(this.CLASS_NAME, "options.entitySchema", options.entitySchema);
|
|
56
|
-
core.Guards.object(this.CLASS_NAME, "options.config", options.config);
|
|
57
|
-
options.config.authMode ??= "credentials";
|
|
58
|
-
if (options.config.authMode === "credentials") {
|
|
59
|
-
core.Guards.stringValue(this.CLASS_NAME, "options.config.accessKeyId", options.config.accessKeyId);
|
|
60
|
-
core.Guards.stringValue(this.CLASS_NAME, "options.config.secretAccessKey", options.config.secretAccessKey);
|
|
61
|
-
}
|
|
62
|
-
core.Guards.stringValue(this.CLASS_NAME, "options.config.region", options.config.region);
|
|
63
|
-
core.Guards.stringValue(this.CLASS_NAME, "options.config.tableName", options.config.tableName);
|
|
64
|
-
this._entitySchema = entity.EntitySchemaFactory.get(options.entitySchema);
|
|
65
|
-
this._primaryKey = entity.EntitySchemaHelper.getPrimaryKey(this._entitySchema);
|
|
66
|
-
this._config = options.config;
|
|
67
|
-
this._config.endpoint = core.Is.stringValue(this._config.endpoint)
|
|
68
|
-
? this._config.endpoint
|
|
69
|
-
: undefined;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Bootstrap the component by creating and initializing any resources it needs.
|
|
73
|
-
* @param nodeLoggingComponentType The node logging component type.
|
|
74
|
-
* @returns True if the bootstrapping process was successful.
|
|
75
|
-
*/
|
|
76
|
-
async bootstrap(nodeLoggingComponentType) {
|
|
77
|
-
const nodeLogging = core.ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
78
|
-
if (!(await this.tableExists(this._config.tableName))) {
|
|
79
|
-
await nodeLogging?.log({
|
|
80
|
-
level: "info",
|
|
81
|
-
source: this.CLASS_NAME,
|
|
82
|
-
ts: Date.now(),
|
|
83
|
-
message: "tableCreating",
|
|
84
|
-
data: {
|
|
85
|
-
tableName: this._config.tableName
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
try {
|
|
89
|
-
const dbConnection = this.createConnection();
|
|
90
|
-
const tableParams = {
|
|
91
|
-
AttributeDefinitions: [],
|
|
92
|
-
KeySchema: [],
|
|
93
|
-
ProvisionedThroughput: {
|
|
94
|
-
ReadCapacityUnits: 1,
|
|
95
|
-
WriteCapacityUnits: 1
|
|
96
|
-
},
|
|
97
|
-
TableName: this._config.tableName
|
|
98
|
-
};
|
|
99
|
-
// We always add a partition key to the table as a non optional hash key
|
|
100
|
-
// is always required when querying using sort parameters
|
|
101
|
-
tableParams.AttributeDefinitions?.push({
|
|
102
|
-
AttributeName: DynamoDbEntityStorageConnector._PARTITION_ID_NAME,
|
|
103
|
-
AttributeType: "S"
|
|
104
|
-
});
|
|
105
|
-
tableParams.KeySchema?.push({
|
|
106
|
-
AttributeName: DynamoDbEntityStorageConnector._PARTITION_ID_NAME,
|
|
107
|
-
KeyType: "HASH"
|
|
108
|
-
});
|
|
109
|
-
const gsi = [];
|
|
110
|
-
if (core.Is.arrayValue(this._entitySchema.properties)) {
|
|
111
|
-
for (const prop of this._entitySchema.properties) {
|
|
112
|
-
if (prop.isPrimary) {
|
|
113
|
-
tableParams.AttributeDefinitions?.push({
|
|
114
|
-
AttributeName: prop.property,
|
|
115
|
-
AttributeType: prop.type === "integer" || prop.type === "number" ? "N" : "S"
|
|
116
|
-
});
|
|
117
|
-
tableParams.KeySchema?.push({
|
|
118
|
-
AttributeName: prop.property,
|
|
119
|
-
KeyType: "RANGE"
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
else if (core.Is.stringValue(prop.sortDirection) || prop.isSecondary) {
|
|
123
|
-
// You can only query and sort items if you have a secondary index
|
|
124
|
-
// defined for the property
|
|
125
|
-
tableParams.AttributeDefinitions?.push({
|
|
126
|
-
AttributeName: prop.property,
|
|
127
|
-
AttributeType: prop.type === "integer" || prop.type === "number" ? "N" : "S"
|
|
128
|
-
});
|
|
129
|
-
gsi.push({
|
|
130
|
-
IndexName: `${prop.property}Index`,
|
|
131
|
-
KeySchema: [
|
|
132
|
-
{
|
|
133
|
-
AttributeName: DynamoDbEntityStorageConnector._PARTITION_ID_NAME,
|
|
134
|
-
KeyType: "HASH"
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
AttributeName: prop.property,
|
|
138
|
-
KeyType: "RANGE"
|
|
139
|
-
}
|
|
140
|
-
],
|
|
141
|
-
Projection: {
|
|
142
|
-
ProjectionType: "ALL"
|
|
143
|
-
},
|
|
144
|
-
ProvisionedThroughput: {
|
|
145
|
-
ReadCapacityUnits: 1,
|
|
146
|
-
WriteCapacityUnits: 1
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
if (gsi.length > 0) {
|
|
153
|
-
tableParams.GlobalSecondaryIndexes = gsi;
|
|
154
|
-
}
|
|
155
|
-
await dbConnection.createTable(tableParams);
|
|
156
|
-
// Wait for table to exist
|
|
157
|
-
await clientDynamodb.waitUntilTableExists({
|
|
158
|
-
client: dbConnection,
|
|
159
|
-
maxWaitTime: 60000
|
|
160
|
-
}, {
|
|
161
|
-
TableName: this._config.tableName
|
|
162
|
-
});
|
|
163
|
-
await nodeLogging?.log({
|
|
164
|
-
level: "info",
|
|
165
|
-
source: this.CLASS_NAME,
|
|
166
|
-
ts: Date.now(),
|
|
167
|
-
message: "tableCreated",
|
|
168
|
-
data: {
|
|
169
|
-
tableName: this._config.tableName
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
if (core.BaseError.isErrorCode(err, "ResourceInUseException")) {
|
|
175
|
-
await nodeLogging?.log({
|
|
176
|
-
level: "info",
|
|
177
|
-
source: this.CLASS_NAME,
|
|
178
|
-
ts: Date.now(),
|
|
179
|
-
message: "tableExists",
|
|
180
|
-
data: {
|
|
181
|
-
tableName: this._config.tableName
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
else {
|
|
186
|
-
await nodeLogging?.log({
|
|
187
|
-
level: "error",
|
|
188
|
-
source: this.CLASS_NAME,
|
|
189
|
-
ts: Date.now(),
|
|
190
|
-
message: "tableCreateFailed",
|
|
191
|
-
error: core.BaseError.fromError(err),
|
|
192
|
-
data: {
|
|
193
|
-
tableName: this._config.tableName
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
201
|
-
await nodeLogging?.log({
|
|
202
|
-
level: "info",
|
|
203
|
-
source: this.CLASS_NAME,
|
|
204
|
-
ts: Date.now(),
|
|
205
|
-
message: "tableExists",
|
|
206
|
-
data: {
|
|
207
|
-
tableName: this._config.tableName
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Get the schema for the entities.
|
|
215
|
-
* @returns The schema for the entities.
|
|
216
|
-
*/
|
|
217
|
-
getSchema() {
|
|
218
|
-
return this._entitySchema;
|
|
219
|
-
}
|
|
220
|
-
/**
|
|
221
|
-
* Get an entity.
|
|
222
|
-
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
223
|
-
* @param secondaryIndex Get the item using a secondary index.
|
|
224
|
-
* @param conditions The optional conditions to match for the entities.
|
|
225
|
-
* @returns The object if it can be found or undefined.
|
|
226
|
-
*/
|
|
227
|
-
async get(id, secondaryIndex, conditions) {
|
|
228
|
-
core.Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
229
|
-
try {
|
|
230
|
-
const docClient = this.createDocClient();
|
|
231
|
-
if (core.Is.empty(secondaryIndex) && core.Is.empty(conditions)) {
|
|
232
|
-
const getCommand = new libDynamodb.GetCommand({
|
|
233
|
-
TableName: this._config.tableName,
|
|
234
|
-
Key: {
|
|
235
|
-
[DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
236
|
-
[this._primaryKey.property]: id
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
const response = await docClient.send(getCommand);
|
|
240
|
-
delete response.Item?.[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
|
|
241
|
-
return response.Item;
|
|
242
|
-
}
|
|
243
|
-
const finalConditions = {
|
|
244
|
-
conditions: []
|
|
245
|
-
};
|
|
246
|
-
if (core.Is.stringValue(secondaryIndex)) {
|
|
247
|
-
finalConditions.conditions.push({
|
|
248
|
-
property: secondaryIndex,
|
|
249
|
-
comparison: entity.ComparisonOperator.Equals,
|
|
250
|
-
value: id
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
if (core.Is.arrayValue(conditions)) {
|
|
254
|
-
for (const c of conditions) {
|
|
255
|
-
finalConditions.conditions.push({
|
|
256
|
-
property: c.property,
|
|
257
|
-
comparison: entity.ComparisonOperator.Equals,
|
|
258
|
-
value: c.value
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const queryResult = await this.internalQuery(finalConditions, undefined, undefined, undefined, 1, secondaryIndex);
|
|
263
|
-
return queryResult.entities[0];
|
|
264
|
-
}
|
|
265
|
-
catch (err) {
|
|
266
|
-
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
267
|
-
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
268
|
-
table: this._config.tableName
|
|
269
|
-
}, err);
|
|
270
|
-
}
|
|
271
|
-
throw new core.GeneralError(this.CLASS_NAME, "getFailed", {
|
|
272
|
-
id
|
|
273
|
-
}, err);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
/**
|
|
277
|
-
* Set an entity.
|
|
278
|
-
* @param entity The entity to set.
|
|
279
|
-
* @param conditions The optional conditions to match for the entities.
|
|
280
|
-
* @returns The id of the entity.
|
|
281
|
-
*/
|
|
282
|
-
async set(entity$1, conditions) {
|
|
283
|
-
core.Guards.object(this.CLASS_NAME, "entity", entity$1);
|
|
284
|
-
entity.EntitySchemaHelper.validateEntity(entity$1, this.getSchema());
|
|
285
|
-
const id = entity$1[this._primaryKey.property];
|
|
286
|
-
try {
|
|
287
|
-
const docClient = this.createDocClient();
|
|
288
|
-
const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
|
|
289
|
-
const putCommand = new libDynamodb.PutCommand({
|
|
290
|
-
TableName: this._config.tableName,
|
|
291
|
-
Item: {
|
|
292
|
-
[DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
293
|
-
...entity$1
|
|
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
|
|
302
|
-
});
|
|
303
|
-
await docClient.send(putCommand);
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
310
|
-
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
311
|
-
tableName: this._config.tableName
|
|
312
|
-
}, err);
|
|
313
|
-
}
|
|
314
|
-
throw new core.GeneralError(this.CLASS_NAME, "setFailed", {
|
|
315
|
-
id
|
|
316
|
-
}, err);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
/**
|
|
320
|
-
* Remove the entity.
|
|
321
|
-
* @param id The id of the entity to remove.
|
|
322
|
-
* @param conditions The optional conditions to match for the entities.
|
|
323
|
-
* @returns Nothing.
|
|
324
|
-
*/
|
|
325
|
-
async remove(id, conditions) {
|
|
326
|
-
core.Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
327
|
-
try {
|
|
328
|
-
const docClient = this.createDocClient();
|
|
329
|
-
const { conditionExpression, attributeNames, attributeValues } = this.buildConditionExpression(conditions);
|
|
330
|
-
const deleteCommand = new libDynamodb.DeleteCommand({
|
|
331
|
-
TableName: this._config.tableName,
|
|
332
|
-
Key: {
|
|
333
|
-
[DynamoDbEntityStorageConnector._PARTITION_ID_NAME]: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
334
|
-
[this._primaryKey.property]: id
|
|
335
|
-
},
|
|
336
|
-
ConditionExpression: conditionExpression,
|
|
337
|
-
ExpressionAttributeNames: attributeNames,
|
|
338
|
-
ExpressionAttributeValues: attributeValues
|
|
339
|
-
});
|
|
340
|
-
await docClient.send(deleteCommand);
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
if (core.BaseError.isErrorName(err, "ConditionalCheckFailedException")) {
|
|
344
|
-
return;
|
|
345
|
-
}
|
|
346
|
-
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
347
|
-
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
348
|
-
table: this._config.tableName
|
|
349
|
-
}, err);
|
|
350
|
-
}
|
|
351
|
-
throw new core.GeneralError(this.CLASS_NAME, "removeFailed", {
|
|
352
|
-
id
|
|
353
|
-
}, err);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Find all the entities which match the conditions.
|
|
358
|
-
* @param conditions The conditions to match for the entities.
|
|
359
|
-
* @param sortProperties The optional sort order.
|
|
360
|
-
* @param properties The optional properties to return, defaults to all.
|
|
361
|
-
* @param cursor The cursor to request the next page of entities.
|
|
362
|
-
* @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
363
|
-
* @returns All the entities for the storage matching the conditions,
|
|
364
|
-
* and a cursor which can be used to request more entities.
|
|
365
|
-
*/
|
|
366
|
-
async query(conditions, sortProperties, properties, cursor, pageSize) {
|
|
367
|
-
return this.internalQuery(conditions, sortProperties, properties, cursor, pageSize);
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Delete the table.
|
|
371
|
-
* @returns Nothing.
|
|
372
|
-
*/
|
|
373
|
-
async tableDelete() {
|
|
374
|
-
try {
|
|
375
|
-
const dbConnection = this.createConnection();
|
|
376
|
-
await dbConnection.deleteTable({ TableName: this._config.tableName });
|
|
377
|
-
}
|
|
378
|
-
catch { }
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Create the parameters for a query.
|
|
382
|
-
* @param objectPath The path for the nested object.
|
|
383
|
-
* @param condition The conditions to create the query from.
|
|
384
|
-
* @param attributeNames The attribute names to use in the query.
|
|
385
|
-
* @param attributeValues The attribute values to use in the query.
|
|
386
|
-
* @returns The condition clause.
|
|
387
|
-
* @internal
|
|
388
|
-
*/
|
|
389
|
-
buildQueryParameters(objectPath, condition, attributeNames, attributeValues, secondaryIndex) {
|
|
390
|
-
// If no conditions are defined then return empty string
|
|
391
|
-
if (core.Is.undefined(condition)) {
|
|
392
|
-
return {
|
|
393
|
-
keyCondition: "",
|
|
394
|
-
filterCondition: ""
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
if ("conditions" in condition) {
|
|
398
|
-
if (condition.conditions.length === 0) {
|
|
399
|
-
return {
|
|
400
|
-
keyCondition: "",
|
|
401
|
-
filterCondition: ""
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
// It's a group of comparisons, so check the individual items and combine with the logical operator
|
|
405
|
-
const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues, secondaryIndex));
|
|
406
|
-
const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
|
|
407
|
-
const keyCondition = joinConditions
|
|
408
|
-
.filter(j => j.keyCondition.length > 0)
|
|
409
|
-
.map(j => j.keyCondition)
|
|
410
|
-
.join(` ${logicalOperator} `);
|
|
411
|
-
const filterCondition = joinConditions
|
|
412
|
-
.filter(j => j.filterCondition.length > 0)
|
|
413
|
-
.map(j => j.filterCondition)
|
|
414
|
-
.join(` ${logicalOperator} `);
|
|
415
|
-
return {
|
|
416
|
-
keyCondition: core.Is.stringValue(keyCondition) ? ` (${keyCondition}) ` : "",
|
|
417
|
-
filterCondition: core.Is.stringValue(filterCondition) ? ` (${filterCondition}) ` : ""
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
|
|
421
|
-
// It's a single value so just create the property comparison for the condition
|
|
422
|
-
const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
|
|
423
|
-
const isKey = schemaProp?.isPrimary ?? (schemaProp?.isSecondary && schemaProp?.property === secondaryIndex);
|
|
424
|
-
return {
|
|
425
|
-
keyCondition: isKey ? comparison : "",
|
|
426
|
-
filterCondition: !isKey ? comparison : ""
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Map the framework comparison operators to those in DynamoDB.
|
|
431
|
-
* @param objectPath The prefix to use for the condition.
|
|
432
|
-
* @param comparator The operator to map.
|
|
433
|
-
* @param type The type of the property.
|
|
434
|
-
* @param attributeNames The attribute names to use in the query.
|
|
435
|
-
* @param attributeValues The attribute values to use in the query.
|
|
436
|
-
* @returns The comparison expression.
|
|
437
|
-
* @throws GeneralError if the comparison operator is not supported.
|
|
438
|
-
* @internal
|
|
439
|
-
*/
|
|
440
|
-
mapComparisonOperator(objectPath, comparator, type, attributeNames, attributeValues) {
|
|
441
|
-
let prop = objectPath;
|
|
442
|
-
if (prop.length > 0) {
|
|
443
|
-
prop += ".";
|
|
444
|
-
}
|
|
445
|
-
prop += comparator.property;
|
|
446
|
-
let attributeName = this.populateAttributeNames(prop, attributeNames);
|
|
447
|
-
if (core.Is.empty(comparator.value)) {
|
|
448
|
-
if (comparator.comparison === entity.ComparisonOperator.Equals) {
|
|
449
|
-
return `attribute_not_exists(${attributeName})`;
|
|
450
|
-
}
|
|
451
|
-
else if (comparator.comparison === entity.ComparisonOperator.NotEquals) {
|
|
452
|
-
return `attribute_exists(${attributeName})`;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
let propName = `:${attributeName.replace(/\./g, "").replace(/#/g, "")}`;
|
|
456
|
-
if (core.Is.array(comparator.value)) {
|
|
457
|
-
const dbValues = comparator.value.map(v => this.propertyToDbValue(v, type));
|
|
458
|
-
const arrAttributeNames = [];
|
|
459
|
-
for (let i = 0; i < dbValues.length; i++) {
|
|
460
|
-
const arrAttributeName = `${propName}${i}`;
|
|
461
|
-
attributeValues[arrAttributeName] = dbValues[i];
|
|
462
|
-
arrAttributeNames.push(arrAttributeName);
|
|
463
|
-
}
|
|
464
|
-
propName = attributeName;
|
|
465
|
-
attributeName = `(${arrAttributeNames.join(", ")})`;
|
|
466
|
-
}
|
|
467
|
-
else {
|
|
468
|
-
attributeValues[propName] = this.propertyToDbValue(comparator.value, type);
|
|
469
|
-
}
|
|
470
|
-
if (comparator.comparison === entity.ComparisonOperator.Equals) {
|
|
471
|
-
return `${attributeName} = ${propName}`;
|
|
472
|
-
}
|
|
473
|
-
else if (comparator.comparison === entity.ComparisonOperator.NotEquals) {
|
|
474
|
-
return `${attributeName} <> ${propName}`;
|
|
475
|
-
}
|
|
476
|
-
else if (comparator.comparison === entity.ComparisonOperator.GreaterThan) {
|
|
477
|
-
return `${attributeName} > ${propName}`;
|
|
478
|
-
}
|
|
479
|
-
else if (comparator.comparison === entity.ComparisonOperator.LessThan) {
|
|
480
|
-
return `${attributeName} < ${propName}`;
|
|
481
|
-
}
|
|
482
|
-
else if (comparator.comparison === entity.ComparisonOperator.GreaterThanOrEqual) {
|
|
483
|
-
return `${attributeName} >= ${propName}`;
|
|
484
|
-
}
|
|
485
|
-
else if (comparator.comparison === entity.ComparisonOperator.LessThanOrEqual) {
|
|
486
|
-
return `${attributeName} <= ${propName}`;
|
|
487
|
-
}
|
|
488
|
-
else if (comparator.comparison === entity.ComparisonOperator.Includes) {
|
|
489
|
-
return `contains(${attributeName}, ${propName})`;
|
|
490
|
-
}
|
|
491
|
-
else if (comparator.comparison === entity.ComparisonOperator.NotIncludes) {
|
|
492
|
-
return `notContains(${attributeName}, ${propName})`;
|
|
493
|
-
}
|
|
494
|
-
else if (comparator.comparison === entity.ComparisonOperator.In) {
|
|
495
|
-
return `${propName} IN ${attributeName}`;
|
|
496
|
-
}
|
|
497
|
-
throw new core.GeneralError(this.CLASS_NAME, "comparisonNotSupported", {
|
|
498
|
-
comparison: comparator.comparison
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
/**
|
|
502
|
-
* Create a unique name for the attribute.
|
|
503
|
-
* @param name The name to create a unique name for.
|
|
504
|
-
* @param attributeNames The attribute names to use in the query.
|
|
505
|
-
* @returns The unique name.
|
|
506
|
-
* @internal
|
|
507
|
-
*/
|
|
508
|
-
populateAttributeNames(name, attributeNames) {
|
|
509
|
-
const parts = name.split(".");
|
|
510
|
-
const attributeNameParts = [];
|
|
511
|
-
for (const part of parts) {
|
|
512
|
-
const hashPart = `#${part}`;
|
|
513
|
-
if (core.Is.empty(attributeNames[hashPart])) {
|
|
514
|
-
attributeNames[hashPart] = part;
|
|
515
|
-
}
|
|
516
|
-
attributeNameParts.push(hashPart);
|
|
517
|
-
}
|
|
518
|
-
return attributeNameParts.join(".");
|
|
519
|
-
}
|
|
520
|
-
/**
|
|
521
|
-
* Map the framework conditional operators to those in DynamoDB.
|
|
522
|
-
* @param operator The operator to map.
|
|
523
|
-
* @returns The conditional operator.
|
|
524
|
-
* @throws GeneralError if the conditional operator is not supported.
|
|
525
|
-
* @internal
|
|
526
|
-
*/
|
|
527
|
-
mapConditionalOperator(operator) {
|
|
528
|
-
if ((operator ?? entity.LogicalOperator.And) === entity.LogicalOperator.And) {
|
|
529
|
-
return "AND";
|
|
530
|
-
}
|
|
531
|
-
else if (operator === entity.LogicalOperator.Or) {
|
|
532
|
-
return "OR";
|
|
533
|
-
}
|
|
534
|
-
throw new core.GeneralError(this.CLASS_NAME, "conditionalNotSupported", { operator });
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Format a value to insert into DB.
|
|
538
|
-
* @param value The value to format.
|
|
539
|
-
* @param type The type for the property.
|
|
540
|
-
* @returns The value after conversion.
|
|
541
|
-
* @internal
|
|
542
|
-
*/
|
|
543
|
-
propertyToDbValue(value, type) {
|
|
544
|
-
if (core.Is.object(value)) {
|
|
545
|
-
const map = {};
|
|
546
|
-
for (const key in value) {
|
|
547
|
-
map[key] = this.propertyToDbValue(value[key]);
|
|
548
|
-
}
|
|
549
|
-
return {
|
|
550
|
-
M: map
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
if (type === "integer" || type === "number") {
|
|
554
|
-
return { N: core.Coerce.string(value) ?? "" };
|
|
555
|
-
}
|
|
556
|
-
else if (type === "boolean") {
|
|
557
|
-
return { BOOL: core.Coerce.boolean(value) ?? false };
|
|
558
|
-
}
|
|
559
|
-
return { S: core.Coerce.string(value) ?? "" };
|
|
560
|
-
}
|
|
561
|
-
/**
|
|
562
|
-
* Create a doc client connection.
|
|
563
|
-
* @returns The dynamo db document client.
|
|
564
|
-
* @internal
|
|
565
|
-
*/
|
|
566
|
-
createDocClient() {
|
|
567
|
-
return libDynamodb.DynamoDBDocumentClient.from(new clientDynamodb.DynamoDB({
|
|
568
|
-
apiVersion: "2012-10-08",
|
|
569
|
-
...this.createConnectionConfig()
|
|
570
|
-
}), {
|
|
571
|
-
marshallOptions: {
|
|
572
|
-
removeUndefinedValues: true
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
/**
|
|
577
|
-
* Create a new DB connection.
|
|
578
|
-
* @returns The Dynamo DB connection.
|
|
579
|
-
* @internal
|
|
580
|
-
*/
|
|
581
|
-
createConnection() {
|
|
582
|
-
return new clientDynamodb.DynamoDB(this.createConnectionConfig());
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Create a new DB connection configuration.
|
|
586
|
-
* @returns The Dynamo DB connection configuration.
|
|
587
|
-
* @internal
|
|
588
|
-
*/
|
|
589
|
-
createConnectionConfig() {
|
|
590
|
-
if (core.Is.stringValue(this._config.secretAccessKey) &&
|
|
591
|
-
core.Is.stringValue(this._config.accessKeyId) &&
|
|
592
|
-
this._config.authMode === "credentials") {
|
|
593
|
-
return {
|
|
594
|
-
credentials: {
|
|
595
|
-
accessKeyId: this._config.accessKeyId,
|
|
596
|
-
secretAccessKey: this._config.secretAccessKey
|
|
597
|
-
},
|
|
598
|
-
endpoint: this._config.endpoint,
|
|
599
|
-
region: this._config.region
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
return {
|
|
603
|
-
endpoint: this._config.endpoint,
|
|
604
|
-
region: this._config.region
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
/**
|
|
608
|
-
* Check if the table exists.
|
|
609
|
-
* @param tableName The table to check.
|
|
610
|
-
* @returns True if the table exists.
|
|
611
|
-
* @internal
|
|
612
|
-
*/
|
|
613
|
-
async tableExists(tableName) {
|
|
614
|
-
try {
|
|
615
|
-
const dbConnection = this.createConnection();
|
|
616
|
-
await dbConnection.describeTable({ TableName: tableName });
|
|
617
|
-
return true;
|
|
618
|
-
}
|
|
619
|
-
catch {
|
|
620
|
-
return false;
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Find all the entities which match the conditions.
|
|
625
|
-
* @param conditions The conditions to match for the entities.
|
|
626
|
-
* @param sortProperties The optional sort order.
|
|
627
|
-
* @param properties The optional properties to return, defaults to all.
|
|
628
|
-
* @param cursor The cursor to request the next page of entities.
|
|
629
|
-
* @param pageSize The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
|
|
630
|
-
* @param secondaryIndex The secondary index to use for the query.
|
|
631
|
-
* @returns All the entities for the storage matching the conditions,
|
|
632
|
-
* and a cursor which can be used to request more entities.
|
|
633
|
-
* @internal
|
|
634
|
-
*/
|
|
635
|
-
async internalQuery(conditions, sortProperties, properties, cursor, pageSize, secondaryIndex) {
|
|
636
|
-
try {
|
|
637
|
-
const returnSize = pageSize ?? DynamoDbEntityStorageConnector._PAGE_SIZE;
|
|
638
|
-
let indexName = core.Is.stringValue(secondaryIndex)
|
|
639
|
-
? `${secondaryIndex}Index`
|
|
640
|
-
: undefined;
|
|
641
|
-
// If we have a sortable property defined in the descriptor then we must use
|
|
642
|
-
// the secondary index for the query
|
|
643
|
-
let scanAscending = true;
|
|
644
|
-
if (core.Is.arrayValue(sortProperties)) {
|
|
645
|
-
if (sortProperties.length > 1) {
|
|
646
|
-
throw new core.GeneralError(this.CLASS_NAME, "sortSingle");
|
|
647
|
-
}
|
|
648
|
-
for (const sortProperty of sortProperties) {
|
|
649
|
-
const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
|
|
650
|
-
if (core.Is.undefined(propertySchema) ||
|
|
651
|
-
(!propertySchema.isPrimary &&
|
|
652
|
-
!propertySchema.isSecondary &&
|
|
653
|
-
core.Is.empty(propertySchema.sortDirection))) {
|
|
654
|
-
throw new core.GeneralError(this.CLASS_NAME, "sortNotIndexed", {
|
|
655
|
-
property: sortProperty.property
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
indexName = propertySchema.isPrimary
|
|
659
|
-
? undefined
|
|
660
|
-
: `${sortProperty.property}Index`;
|
|
661
|
-
scanAscending = sortProperty.sortDirection === entity.SortDirection.Ascending;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
const attributeNames = { "#partitionId": "partitionId" };
|
|
665
|
-
const attributeValues = {
|
|
666
|
-
[`:${DynamoDbEntityStorageConnector._PARTITION_ID_NAME}`]: {
|
|
667
|
-
S: DynamoDbEntityStorageConnector._PARTITION_ID_VALUE
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
const expressions = this.buildQueryParameters("", conditions, attributeNames, attributeValues, secondaryIndex);
|
|
671
|
-
let keyExpression = "#partitionId = :partitionId";
|
|
672
|
-
if (expressions.keyCondition.length > 0) {
|
|
673
|
-
keyExpression += ` AND ${expressions.keyCondition}`;
|
|
674
|
-
}
|
|
675
|
-
const query = new clientDynamodb.QueryCommand({
|
|
676
|
-
TableName: this._config.tableName,
|
|
677
|
-
IndexName: indexName,
|
|
678
|
-
KeyConditionExpression: keyExpression,
|
|
679
|
-
FilterExpression: core.Is.stringValue(expressions.filterCondition)
|
|
680
|
-
? expressions.filterCondition
|
|
681
|
-
: undefined,
|
|
682
|
-
ExpressionAttributeNames: attributeNames,
|
|
683
|
-
ExpressionAttributeValues: attributeValues,
|
|
684
|
-
ProjectionExpression: properties?.map(p => p).join(", "),
|
|
685
|
-
Limit: returnSize,
|
|
686
|
-
ScanIndexForward: scanAscending,
|
|
687
|
-
ExclusiveStartKey: core.Is.empty(cursor)
|
|
688
|
-
? undefined
|
|
689
|
-
: core.ObjectHelper.fromBytes(core.Converter.base64ToBytes(cursor))
|
|
690
|
-
});
|
|
691
|
-
const connection = this.createDocClient();
|
|
692
|
-
const results = await connection.send(query);
|
|
693
|
-
let entities = [];
|
|
694
|
-
if (core.Is.arrayValue(results.Items)) {
|
|
695
|
-
entities = results.Items.map(item => {
|
|
696
|
-
const unmarshalled = utilDynamodb.unmarshall(item);
|
|
697
|
-
delete unmarshalled[DynamoDbEntityStorageConnector._PARTITION_ID_NAME];
|
|
698
|
-
return unmarshalled;
|
|
699
|
-
});
|
|
700
|
-
}
|
|
701
|
-
return {
|
|
702
|
-
entities,
|
|
703
|
-
cursor: core.Is.empty(results.LastEvaluatedKey)
|
|
704
|
-
? undefined
|
|
705
|
-
: core.Converter.bytesToBase64(core.ObjectHelper.toBytes(results.LastEvaluatedKey))
|
|
706
|
-
};
|
|
707
|
-
}
|
|
708
|
-
catch (err) {
|
|
709
|
-
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
710
|
-
throw new core.GeneralError(this.CLASS_NAME, "tableDoesNotExist", {
|
|
711
|
-
table: this._config.tableName
|
|
712
|
-
}, err);
|
|
713
|
-
}
|
|
714
|
-
throw new core.GeneralError(this.CLASS_NAME, "queryFailed", undefined, err);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
/**
|
|
718
|
-
* Build the condition expression for the query.
|
|
719
|
-
* @param conditions The conditions to build the expression from.
|
|
720
|
-
* @returns The condition expression.
|
|
721
|
-
* @throws GeneralError if the property is not found in the schema.
|
|
722
|
-
* @internal
|
|
723
|
-
*/
|
|
724
|
-
buildConditionExpression(conditions) {
|
|
725
|
-
let conditionExpression;
|
|
726
|
-
let attributeNames;
|
|
727
|
-
let attributeValues;
|
|
728
|
-
if (core.Is.arrayValue(conditions)) {
|
|
729
|
-
const expressions = [];
|
|
730
|
-
for (const c of conditions) {
|
|
731
|
-
const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
|
|
732
|
-
if (core.Is.undefined(schemaProp)) {
|
|
733
|
-
throw new core.GeneralError(this.CLASS_NAME, "propertyNotFound", {
|
|
734
|
-
property: c.property
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
const attributeName = `#${c.property}`;
|
|
738
|
-
const attributeValueName = `:${c.property}`;
|
|
739
|
-
attributeNames ??= {};
|
|
740
|
-
attributeValues ??= {};
|
|
741
|
-
attributeNames[attributeName] = c.property;
|
|
742
|
-
attributeValues[attributeValueName] = c.value;
|
|
743
|
-
expressions.push(`${attributeName} = ${attributeValueName}`);
|
|
744
|
-
}
|
|
745
|
-
conditionExpression = expressions.join(" AND ");
|
|
746
|
-
}
|
|
747
|
-
return { conditionExpression, attributeNames, attributeValues };
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
exports.DynamoDbEntityStorageConnector = DynamoDbEntityStorageConnector;
|