@twin.org/entity-storage-connector-cosmosdb 0.0.1-next.12
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/LICENSE +201 -0
- package/README.md +41 -0
- package/dist/cjs/index.cjs +597 -0
- package/dist/esm/index.mjs +595 -0
- package/dist/types/cosmosDbEntityStorageConnector.d.ts +88 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/models/ICosmosDbEntityStorageConnectorConfig.d.ts +21 -0
- package/docs/changelog.md +5 -0
- package/docs/examples.md +1 -0
- package/docs/reference/classes/CosmosDbEntityStorageConnector.md +246 -0
- package/docs/reference/index.md +9 -0
- package/docs/reference/interfaces/ICosmosDbEntityStorageConnectorConfig.md +35 -0
- package/locales/en.json +23 -0
- package/package.json +43 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var cosmos = require('@azure/cosmos');
|
|
4
|
+
var core = require('@twin.org/core');
|
|
5
|
+
var entity = require('@twin.org/entity');
|
|
6
|
+
var loggingModels = require('@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 Cosmos DB.
|
|
12
|
+
*/
|
|
13
|
+
class CosmosDbEntityStorageConnector {
|
|
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 path.
|
|
26
|
+
* @internal
|
|
27
|
+
*/
|
|
28
|
+
static _PARTITION_ID_PATH = "/partitionId";
|
|
29
|
+
/**
|
|
30
|
+
* Partition id field value.
|
|
31
|
+
* @internal
|
|
32
|
+
*/
|
|
33
|
+
static _PARTITION_ID_VALUE = "1";
|
|
34
|
+
/**
|
|
35
|
+
* Runtime name for the class.
|
|
36
|
+
*/
|
|
37
|
+
CLASS_NAME = "CosmosDbEntityStorageConnector";
|
|
38
|
+
/**
|
|
39
|
+
* The schema for the entity.
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
_entitySchema;
|
|
43
|
+
/**
|
|
44
|
+
* The primary key.
|
|
45
|
+
* @internal
|
|
46
|
+
*/
|
|
47
|
+
_primaryKey;
|
|
48
|
+
/**
|
|
49
|
+
* The configuration for the connector.
|
|
50
|
+
* @internal
|
|
51
|
+
*/
|
|
52
|
+
_config;
|
|
53
|
+
/**
|
|
54
|
+
* The Cosmos DB client.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
_client;
|
|
58
|
+
/**
|
|
59
|
+
* Container user for the data storage.
|
|
60
|
+
* @internal
|
|
61
|
+
*/
|
|
62
|
+
_container;
|
|
63
|
+
/**
|
|
64
|
+
* Create a new instance of CosmosDbEntityStorageConnector.
|
|
65
|
+
* @param options The options for the connector.
|
|
66
|
+
* @param options.entitySchema The schema for the entity.
|
|
67
|
+
* @param options.loggingConnectorType The type of logging connector to use, defaults to no logging.
|
|
68
|
+
* @param options.config The configuration for the connector.
|
|
69
|
+
*/
|
|
70
|
+
constructor(options) {
|
|
71
|
+
core.Guards.object(this.CLASS_NAME, "options", options);
|
|
72
|
+
core.Guards.stringValue(this.CLASS_NAME, "options.entitySchema", options.entitySchema);
|
|
73
|
+
core.Guards.object(this.CLASS_NAME, "options.config", options.config);
|
|
74
|
+
core.Guards.stringValue(this.CLASS_NAME, "options.config.endpoint", options.config.endpoint);
|
|
75
|
+
core.Guards.stringValue(this.CLASS_NAME, "options.config.key", options.config.key);
|
|
76
|
+
core.Guards.stringValue(this.CLASS_NAME, "options.config.databaseId", options.config.databaseId);
|
|
77
|
+
core.Guards.stringValue(this.CLASS_NAME, "options.config.containerId", options.config.containerId);
|
|
78
|
+
this._entitySchema = entity.EntitySchemaFactory.get(options.entitySchema);
|
|
79
|
+
this._primaryKey = entity.EntitySchemaHelper.getPrimaryKey(this._entitySchema);
|
|
80
|
+
this._config = options.config;
|
|
81
|
+
this._client = new cosmos.CosmosClient({
|
|
82
|
+
endpoint: this._config.endpoint,
|
|
83
|
+
key: this._config.key,
|
|
84
|
+
diagnosticLevel: cosmos.CosmosDbDiagnosticLevel.debug
|
|
85
|
+
});
|
|
86
|
+
this._container = this._client
|
|
87
|
+
.database(this._config.databaseId)
|
|
88
|
+
.container(this._config.containerId);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Initialize the Cosmos DB environment.
|
|
92
|
+
* @param nodeLoggingConnectorType Optional type of the logging connector.
|
|
93
|
+
* @returns A promise that resolves to a boolean indicating success.
|
|
94
|
+
*/
|
|
95
|
+
async bootstrap(nodeLoggingConnectorType) {
|
|
96
|
+
const nodeLogging = loggingModels.LoggingConnectorFactory.getIfExists(nodeLoggingConnectorType ?? "node-logging");
|
|
97
|
+
// Create the database if it does not exist
|
|
98
|
+
try {
|
|
99
|
+
await nodeLogging?.log({
|
|
100
|
+
level: "info",
|
|
101
|
+
source: this.CLASS_NAME,
|
|
102
|
+
ts: Date.now(),
|
|
103
|
+
message: "databaseCreating",
|
|
104
|
+
data: {
|
|
105
|
+
databaseId: this._config.databaseId
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
const { resource: databaseDefinition } = await this._client.databases.createIfNotExists({
|
|
109
|
+
id: this._config.databaseId
|
|
110
|
+
});
|
|
111
|
+
core.Guards.stringValue(this.CLASS_NAME, "databaseDefinition.id", databaseDefinition?.id);
|
|
112
|
+
await nodeLogging?.log({
|
|
113
|
+
level: "info",
|
|
114
|
+
source: this.CLASS_NAME,
|
|
115
|
+
ts: Date.now(),
|
|
116
|
+
message: "databaseExists",
|
|
117
|
+
data: {
|
|
118
|
+
databaseId: databaseDefinition.id
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
if (core.BaseError.isErrorCode(error, "Conflict")) {
|
|
124
|
+
await nodeLogging?.log({
|
|
125
|
+
level: "info",
|
|
126
|
+
source: this.CLASS_NAME,
|
|
127
|
+
ts: Date.now(),
|
|
128
|
+
message: "databaseAlreadyExists",
|
|
129
|
+
data: {
|
|
130
|
+
databaseId: this._config.databaseId
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const errors = error instanceof AggregateError ? error.errors : [error];
|
|
136
|
+
for (const err of errors) {
|
|
137
|
+
await nodeLogging?.log({
|
|
138
|
+
level: "error",
|
|
139
|
+
source: this.CLASS_NAME,
|
|
140
|
+
ts: Date.now(),
|
|
141
|
+
message: "databaseCreateFailed",
|
|
142
|
+
error: core.BaseError.fromError(err),
|
|
143
|
+
data: {
|
|
144
|
+
databaseId: this._config.databaseId
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Create the container if it does not exist
|
|
152
|
+
try {
|
|
153
|
+
const { resource: containerDefinition } = await this._client
|
|
154
|
+
.database(this._config.databaseId)
|
|
155
|
+
.containers.createIfNotExists({
|
|
156
|
+
id: this._config.containerId,
|
|
157
|
+
partitionKey: {
|
|
158
|
+
kind: cosmos.PartitionKeyKind.Hash,
|
|
159
|
+
paths: [CosmosDbEntityStorageConnector._PARTITION_ID_PATH]
|
|
160
|
+
}
|
|
161
|
+
}, { offerThroughput: 400 });
|
|
162
|
+
if (containerDefinition) {
|
|
163
|
+
await nodeLogging?.log({
|
|
164
|
+
level: "info",
|
|
165
|
+
source: this.CLASS_NAME,
|
|
166
|
+
ts: Date.now(),
|
|
167
|
+
message: "containerExists",
|
|
168
|
+
data: {
|
|
169
|
+
containerId: this._config.containerId
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
await nodeLogging?.log({
|
|
175
|
+
level: "error",
|
|
176
|
+
source: this.CLASS_NAME,
|
|
177
|
+
ts: Date.now(),
|
|
178
|
+
message: "containerNotExisting",
|
|
179
|
+
data: {
|
|
180
|
+
containerId: this._config.containerId
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
await nodeLogging?.log({
|
|
188
|
+
level: "error",
|
|
189
|
+
source: this.CLASS_NAME,
|
|
190
|
+
ts: Date.now(),
|
|
191
|
+
message: "containerCreateFailed",
|
|
192
|
+
error: core.BaseError.fromError(error),
|
|
193
|
+
data: {
|
|
194
|
+
containerId: this._config.containerId
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get the schema for the entities.
|
|
203
|
+
* @returns The schema for the entities.
|
|
204
|
+
*/
|
|
205
|
+
getSchema() {
|
|
206
|
+
return this._entitySchema;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get an entity from Cosmos DB.
|
|
210
|
+
* @param id The id of the entity to get, or the index value if secondaryIndex is set.
|
|
211
|
+
* @param secondaryIndex Get the item using a secondary index.
|
|
212
|
+
* @param conditions The optional conditions to match for the entities.
|
|
213
|
+
* @returns The object if it can be found or undefined.
|
|
214
|
+
*/
|
|
215
|
+
async get(id, secondaryIndex, conditions) {
|
|
216
|
+
core.Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
217
|
+
try {
|
|
218
|
+
// No secondary index or conditions
|
|
219
|
+
if (core.Is.empty(secondaryIndex) && !core.Is.arrayValue(conditions)) {
|
|
220
|
+
const { resource: item } = await this._container
|
|
221
|
+
.item(id, CosmosDbEntityStorageConnector._PARTITION_ID_VALUE)
|
|
222
|
+
.read();
|
|
223
|
+
return this.itemToEntity(item);
|
|
224
|
+
}
|
|
225
|
+
const whereQuery = [
|
|
226
|
+
`c.${CosmosDbEntityStorageConnector._PARTITION_ID_NAME} = @partitionKey`
|
|
227
|
+
];
|
|
228
|
+
// With a secondary index
|
|
229
|
+
if (core.Is.stringValue(secondaryIndex)) {
|
|
230
|
+
const secIndex = secondaryIndex.toString();
|
|
231
|
+
whereQuery.push(`c.${secIndex} = @id`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
whereQuery.push(`c.${this._primaryKey.property} = @id`);
|
|
235
|
+
}
|
|
236
|
+
// With conditions
|
|
237
|
+
if (core.Is.arrayValue(conditions)) {
|
|
238
|
+
for (const c of conditions) {
|
|
239
|
+
const schemaProp = this._entitySchema.properties?.find(p => p.property === c.property);
|
|
240
|
+
whereQuery.push(`c.${String(c.property)} = ${this.propertyToDbValue(c.value, schemaProp?.type)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const query = {
|
|
244
|
+
query: `SELECT * FROM c WHERE ${whereQuery.join(" AND ")}`,
|
|
245
|
+
parameters: [
|
|
246
|
+
{ name: "@id", value: id },
|
|
247
|
+
{ name: "@partitionKey", value: CosmosDbEntityStorageConnector._PARTITION_ID_VALUE }
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
const { resources: items } = await this._container.items.query(query).fetchAll();
|
|
251
|
+
if (items.length === 1) {
|
|
252
|
+
return this.itemToEntity(items[0]);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
if (core.BaseError.isErrorCode(err, "NotFound")) {
|
|
257
|
+
throw new core.GeneralError(this.CLASS_NAME, "containerDoesNotExist", {
|
|
258
|
+
container: this._config.containerId
|
|
259
|
+
}, err);
|
|
260
|
+
}
|
|
261
|
+
throw new core.GeneralError(this.CLASS_NAME, "getFailed", {
|
|
262
|
+
id
|
|
263
|
+
}, err);
|
|
264
|
+
}
|
|
265
|
+
return undefined;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Set an entity.
|
|
269
|
+
* @param entity The entity to set.
|
|
270
|
+
* @param conditions The optional conditions to match for the entities.
|
|
271
|
+
* @returns The id of the entity.
|
|
272
|
+
*/
|
|
273
|
+
async set(entity, conditions) {
|
|
274
|
+
core.Guards.object(this.CLASS_NAME, "entity", entity);
|
|
275
|
+
const id = entity[this._primaryKey.property];
|
|
276
|
+
try {
|
|
277
|
+
if (core.Is.arrayValue(conditions)) {
|
|
278
|
+
const item = await this._container.item(id, CosmosDbEntityStorageConnector._PARTITION_ID_VALUE);
|
|
279
|
+
const { resource: itemData } = await item.read();
|
|
280
|
+
if (core.Is.notEmpty(itemData) && !this.verifyConditions(conditions, itemData)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
await this._container.items.upsert({
|
|
285
|
+
id,
|
|
286
|
+
[CosmosDbEntityStorageConnector._PARTITION_ID_NAME]: CosmosDbEntityStorageConnector._PARTITION_ID_VALUE,
|
|
287
|
+
...entity
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
if (core.BaseError.isErrorCode(err, "ResourceNotFoundException")) {
|
|
292
|
+
throw new core.GeneralError(this.CLASS_NAME, "containerDoesNotExist", {
|
|
293
|
+
containerId: this._config.containerId
|
|
294
|
+
}, err);
|
|
295
|
+
}
|
|
296
|
+
throw new core.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
|
+
* @param conditions The optional conditions to match for the entities.
|
|
305
|
+
* @returns Nothing.
|
|
306
|
+
*/
|
|
307
|
+
async remove(id, conditions) {
|
|
308
|
+
core.Guards.stringValue(this.CLASS_NAME, "id", id);
|
|
309
|
+
try {
|
|
310
|
+
const item = await this._container.item(id, CosmosDbEntityStorageConnector._PARTITION_ID_VALUE);
|
|
311
|
+
const { resource: itemData } = await item.read();
|
|
312
|
+
if (core.Is.notEmpty(itemData)) {
|
|
313
|
+
if (core.Is.arrayValue(conditions) && !this.verifyConditions(conditions, itemData)) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
await item.delete();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
if (core.BaseError.fromError(err) &&
|
|
321
|
+
core.Is.object(err) &&
|
|
322
|
+
err.body?.code === "NotFound") {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
throw new core.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 ?? CosmosDbEntityStorageConnector._PAGE_SIZE;
|
|
344
|
+
let orderByClause = "";
|
|
345
|
+
if (Array.isArray(sortProperties)) {
|
|
346
|
+
if (sortProperties.length > 1) {
|
|
347
|
+
throw new core.GeneralError(this.CLASS_NAME, "sortSingle");
|
|
348
|
+
}
|
|
349
|
+
for (const sortProperty of sortProperties) {
|
|
350
|
+
const propertySchema = this._entitySchema.properties?.find(e => e.property === sortProperty.property);
|
|
351
|
+
if (!propertySchema ||
|
|
352
|
+
(!propertySchema.isPrimary &&
|
|
353
|
+
!propertySchema.isSecondary &&
|
|
354
|
+
!propertySchema.sortDirection)) {
|
|
355
|
+
throw new core.GeneralError(this.CLASS_NAME, "sortNotIndexed", {
|
|
356
|
+
property: sortProperty.property
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const direction = sortProperty.sortDirection === entity.SortDirection.Ascending ? "asc" : "desc";
|
|
360
|
+
orderByClause = `ORDER BY c.${String(sortProperty.property)} ${direction}`;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const attributeNames = {};
|
|
364
|
+
const attributeValues = {};
|
|
365
|
+
let queryClause = this.buildQueryParameters("", conditions, attributeNames, attributeValues);
|
|
366
|
+
if (queryClause.length > 0) {
|
|
367
|
+
queryClause = ` AND ${queryClause}`;
|
|
368
|
+
}
|
|
369
|
+
const querySpecs = {
|
|
370
|
+
query: `SELECT ${properties ? properties.map(p => `c.${p}`).join(", ") : "*"} FROM c WHERE c.partitionId = @partitionId ${queryClause} ${orderByClause}`,
|
|
371
|
+
parameters: [
|
|
372
|
+
{ name: "@partitionId", value: CosmosDbEntityStorageConnector._PARTITION_ID_VALUE },
|
|
373
|
+
...Object.keys(attributeValues).map(key => ({ name: `@${key}`, value: attributeValues[key] }))
|
|
374
|
+
]
|
|
375
|
+
};
|
|
376
|
+
const feedOptions = {
|
|
377
|
+
maxItemCount: returnSize,
|
|
378
|
+
continuationToken: cursor
|
|
379
|
+
};
|
|
380
|
+
const feedResponse = await this._container.items.query(querySpecs, feedOptions).fetchNext();
|
|
381
|
+
return {
|
|
382
|
+
entities: feedResponse.resources.map(i => this.itemToEntity(i)),
|
|
383
|
+
cursor: feedResponse.continuationToken
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
catch (err) {
|
|
387
|
+
throw new core.GeneralError(this.CLASS_NAME, "queryFailed", { sql }, err);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Delete the container.
|
|
392
|
+
* @returns Nothing.
|
|
393
|
+
*/
|
|
394
|
+
async containerDelete() {
|
|
395
|
+
try {
|
|
396
|
+
await this._container.deleteAllItemsForPartitionKey(CosmosDbEntityStorageConnector._PARTITION_ID_VALUE);
|
|
397
|
+
await this._container.delete();
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Ignore errors
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Create an SQL condition clause.
|
|
405
|
+
* @param objectPath The path for the nested object.
|
|
406
|
+
* @param condition The conditions to create the query from.
|
|
407
|
+
* @param attributeNames The attribute names to use in the query.
|
|
408
|
+
* @param attributeValues The attribute values to use in the query.
|
|
409
|
+
* @returns The condition clause.
|
|
410
|
+
* @internal
|
|
411
|
+
*/
|
|
412
|
+
buildQueryParameters(objectPath, condition, attributeNames, attributeValues) {
|
|
413
|
+
// If no conditions are defined then return empty string
|
|
414
|
+
if (core.Is.undefined(condition)) {
|
|
415
|
+
return "";
|
|
416
|
+
}
|
|
417
|
+
if ("conditions" in condition) {
|
|
418
|
+
if (condition.conditions.length === 0) {
|
|
419
|
+
return "";
|
|
420
|
+
}
|
|
421
|
+
// It's a group of comparisons, so check the individual items and combine with the logical operator
|
|
422
|
+
const joinConditions = condition.conditions.map(c => this.buildQueryParameters(objectPath, c, attributeNames, attributeValues));
|
|
423
|
+
const logicalOperator = this.mapConditionalOperator(condition.logicalOperator);
|
|
424
|
+
const queryClause = joinConditions
|
|
425
|
+
.filter(j => j.length > 0)
|
|
426
|
+
.map(j => j)
|
|
427
|
+
.join(` ${logicalOperator} `);
|
|
428
|
+
return core.Is.stringValue(queryClause) ? ` (${queryClause}) ` : "";
|
|
429
|
+
}
|
|
430
|
+
const schemaProp = this._entitySchema.properties?.find(p => p.property === condition.property);
|
|
431
|
+
// It's a single value so just create the property comparison for the condition
|
|
432
|
+
const comparison = this.mapComparisonOperator(objectPath, condition, schemaProp?.type, attributeNames, attributeValues);
|
|
433
|
+
return comparison;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Map the framework comparison operators to those in CosmosDB.
|
|
437
|
+
* @param objectPath The prefix to use for the condition.
|
|
438
|
+
* @param comparator The operator to map.
|
|
439
|
+
* @param type The type of the property.
|
|
440
|
+
* @param attributeValues The attribute values to use in the query.
|
|
441
|
+
* @returns The comparison expression.
|
|
442
|
+
* @throws GeneralError if the comparison operator is not supported.
|
|
443
|
+
* @internal
|
|
444
|
+
*/
|
|
445
|
+
mapComparisonOperator(objectPath, comparator, type, attributeNames, attributeValues) {
|
|
446
|
+
let prop = objectPath;
|
|
447
|
+
if (prop.length > 0) {
|
|
448
|
+
prop += ".";
|
|
449
|
+
}
|
|
450
|
+
prop += comparator.property;
|
|
451
|
+
let attributeName = this.populateAttributeNames(prop, attributeNames);
|
|
452
|
+
let propName = `${attributeName.replace(/\./g, "").replace(/@/g, "")}`;
|
|
453
|
+
if (core.Is.array(comparator.value)) {
|
|
454
|
+
const dbValues = comparator.value.map(v => this.propertyToDbValue(v, type));
|
|
455
|
+
const arrAttributeNames = [];
|
|
456
|
+
for (let i = 0; i < dbValues.length; i++) {
|
|
457
|
+
const arrAttributeName = `${propName}${i}`;
|
|
458
|
+
attributeValues[arrAttributeName] = dbValues[i];
|
|
459
|
+
arrAttributeNames.push(arrAttributeName);
|
|
460
|
+
}
|
|
461
|
+
propName = attributeName;
|
|
462
|
+
attributeName = `(${arrAttributeNames.map(name => `@${name}`).join(", ")})`;
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
attributeValues[propName] = comparator.value;
|
|
466
|
+
}
|
|
467
|
+
const matches = attributeName.split(".").length;
|
|
468
|
+
if (matches && comparator.comparison === entity.ComparisonOperator.Equals) {
|
|
469
|
+
attributeName = attributeName
|
|
470
|
+
.split(".")
|
|
471
|
+
.map(part => `["${part}"]`)
|
|
472
|
+
.join("");
|
|
473
|
+
return `c${attributeName} = @${propName}`;
|
|
474
|
+
}
|
|
475
|
+
else if (comparator.comparison === entity.ComparisonOperator.Equals) {
|
|
476
|
+
return `c.${attributeName} = @${propName}`;
|
|
477
|
+
}
|
|
478
|
+
else if (comparator.comparison === entity.ComparisonOperator.NotEquals) {
|
|
479
|
+
return `c.${attributeName} <> @${propName}`;
|
|
480
|
+
}
|
|
481
|
+
else if (comparator.comparison === entity.ComparisonOperator.GreaterThan) {
|
|
482
|
+
return `c.${attributeName} > @${propName}`;
|
|
483
|
+
}
|
|
484
|
+
else if (comparator.comparison === entity.ComparisonOperator.LessThan) {
|
|
485
|
+
return `c.${attributeName} < @${propName}`;
|
|
486
|
+
}
|
|
487
|
+
else if (comparator.comparison === entity.ComparisonOperator.GreaterThanOrEqual) {
|
|
488
|
+
return `c.${attributeName} >= @${propName}`;
|
|
489
|
+
}
|
|
490
|
+
else if (comparator.comparison === entity.ComparisonOperator.LessThanOrEqual) {
|
|
491
|
+
return `c.${attributeName} <= @${propName}`;
|
|
492
|
+
}
|
|
493
|
+
else if (typeof attributeValues[propName] === "object" &&
|
|
494
|
+
comparator.comparison === entity.ComparisonOperator.Includes) {
|
|
495
|
+
return `array_contains(c.${attributeName}, @${propName})`;
|
|
496
|
+
}
|
|
497
|
+
else if (comparator.comparison === entity.ComparisonOperator.Includes) {
|
|
498
|
+
return `contains(c.${attributeName}, @${propName})`;
|
|
499
|
+
}
|
|
500
|
+
else if (comparator.comparison === entity.ComparisonOperator.NotIncludes) {
|
|
501
|
+
return `notContains(c.${attributeName}, @${propName})`;
|
|
502
|
+
}
|
|
503
|
+
else if (comparator.comparison === entity.ComparisonOperator.In) {
|
|
504
|
+
return `c.${propName} IN ${attributeName}`;
|
|
505
|
+
}
|
|
506
|
+
throw new core.GeneralError(this.CLASS_NAME, "comparisonNotSupported", {
|
|
507
|
+
comparison: comparator.comparison
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Format a value to insert into DB.
|
|
512
|
+
* @param value The value to format.
|
|
513
|
+
* @param type The type for the property.
|
|
514
|
+
* @returns The value after conversion.
|
|
515
|
+
* @internal
|
|
516
|
+
*/
|
|
517
|
+
propertyToDbValue(value, type) {
|
|
518
|
+
if (core.Is.object(value)) {
|
|
519
|
+
const map = {};
|
|
520
|
+
for (const key in value) {
|
|
521
|
+
map[key] = this.propertyToDbValue(value[key]);
|
|
522
|
+
}
|
|
523
|
+
return map;
|
|
524
|
+
}
|
|
525
|
+
if (type === "string") {
|
|
526
|
+
return `${core.Coerce.string(value)}`;
|
|
527
|
+
}
|
|
528
|
+
else if (type === "integer" || type === "number") {
|
|
529
|
+
return core.Coerce.string(value) ?? "";
|
|
530
|
+
}
|
|
531
|
+
else if (type === "boolean") {
|
|
532
|
+
return core.Coerce.boolean(value) ?? false;
|
|
533
|
+
}
|
|
534
|
+
return core.Coerce.string(value) ?? "";
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Create a unique name for the attribute.
|
|
538
|
+
* @param name The name to create a unique name for.
|
|
539
|
+
* @param attributeNames The attribute names to use in the query.
|
|
540
|
+
* @returns The unique name.
|
|
541
|
+
* @internal
|
|
542
|
+
*/
|
|
543
|
+
populateAttributeNames(name, attributeNames) {
|
|
544
|
+
const parts = name.split(".");
|
|
545
|
+
const attributeNameParts = [];
|
|
546
|
+
for (const part of parts) {
|
|
547
|
+
const hashPart = `${part}`;
|
|
548
|
+
if (core.Is.empty(attributeNames[hashPart])) {
|
|
549
|
+
attributeNames[hashPart] = part;
|
|
550
|
+
}
|
|
551
|
+
attributeNameParts.push(hashPart);
|
|
552
|
+
}
|
|
553
|
+
return attributeNameParts.join(".");
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Map the framework conditional operators to those in CosmosDB.
|
|
557
|
+
* @param operator The operator to map.
|
|
558
|
+
* @returns The conditional operator.
|
|
559
|
+
* @throws GeneralError if the conditional operator is not supported.
|
|
560
|
+
* @internal
|
|
561
|
+
*/
|
|
562
|
+
mapConditionalOperator(operator) {
|
|
563
|
+
if ((operator ?? entity.LogicalOperator.And) === entity.LogicalOperator.And) {
|
|
564
|
+
return "AND";
|
|
565
|
+
}
|
|
566
|
+
else if (operator === entity.LogicalOperator.Or) {
|
|
567
|
+
return "OR";
|
|
568
|
+
}
|
|
569
|
+
throw new core.GeneralError(this.CLASS_NAME, "conditionalNotSupported", { operator });
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Creates the CosmosDB container to be used if it doesn't exists in the context yet.
|
|
573
|
+
* @returns The existing container.
|
|
574
|
+
* @throws GeneralError if the container was not created.
|
|
575
|
+
* @internal
|
|
576
|
+
*/
|
|
577
|
+
verifyConditions(conditions, obj) {
|
|
578
|
+
return conditions.every(condition => core.ObjectHelper.propertyGet(obj, condition.property) === condition.value);
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Convert an entity to an item.
|
|
582
|
+
* @param item The item to convert.
|
|
583
|
+
* @returns The entity.
|
|
584
|
+
* @internal
|
|
585
|
+
*/
|
|
586
|
+
itemToEntity(item) {
|
|
587
|
+
core.ObjectHelper.propertyDelete(item, "partitionId");
|
|
588
|
+
core.ObjectHelper.propertyDelete(item, "_attachments");
|
|
589
|
+
core.ObjectHelper.propertyDelete(item, "_etag");
|
|
590
|
+
core.ObjectHelper.propertyDelete(item, "_rid");
|
|
591
|
+
core.ObjectHelper.propertyDelete(item, "_self");
|
|
592
|
+
core.ObjectHelper.propertyDelete(item, "_ts");
|
|
593
|
+
return item;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
exports.CosmosDbEntityStorageConnector = CosmosDbEntityStorageConnector;
|