@twin.org/entity-storage-connector-mongodb 0.0.3-next.2 → 0.0.3-next.20
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 +7 -12
- package/dist/es/mongoDbEntityStorageConnector.js +332 -41
- package/dist/es/mongoDbEntityStorageConnector.js.map +1 -1
- package/dist/types/mongoDbEntityStorageConnector.d.ts +69 -5
- package/docs/changelog.md +402 -45
- package/docs/examples.md +94 -1
- package/docs/reference/classes/MongoDbEntityStorageConnector.md +310 -20
- package/docs/reference/interfaces/IMongoDbEntityStorageConnectorConfig.md +9 -9
- package/docs/reference/interfaces/IMongoDbEntityStorageConnectorConstructorOptions.md +6 -6
- package/locales/en.json +17 -2
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Entity Storage Connector MongoDB
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides a MongoDB backend for flexible document persistence and evolving schemas. It is designed to work with the wider storage ecosystem so applications can keep behaviour consistent across connectors and environments.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -8,18 +8,13 @@ Entity Storage connector implementation using MongoDb storage.
|
|
|
8
8
|
npm install @twin.org/entity-storage-connector-mongodb
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Docker
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
To perform testing of this component it may be necessary to launch a local instance to communicate with.
|
|
14
14
|
|
|
15
|
-
```
|
|
16
|
-
docker
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Afterwards you can run the tests as follows:
|
|
20
|
-
|
|
21
|
-
```sh
|
|
22
|
-
npm run test
|
|
15
|
+
```shell
|
|
16
|
+
docker pull mongo:latest
|
|
17
|
+
docker run -d --name twin-entity-storage-mongodb -p 27500:27017 mongo:latest
|
|
23
18
|
```
|
|
24
19
|
|
|
25
20
|
## Examples
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Copyright 2024 IOTA Stiftung.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0.
|
|
3
3
|
import { ContextIdHelper, ContextIdStore } from "@twin.org/context";
|
|
4
|
-
import { BaseError, ComponentFactory, GeneralError, Guards, Is, ObjectHelper } from "@twin.org/core";
|
|
5
|
-
import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, LogicalOperator } from "@twin.org/entity";
|
|
4
|
+
import { BaseError, ComponentFactory, GeneralError, Guards, HealthStatus, Is, ObjectHelper } from "@twin.org/core";
|
|
5
|
+
import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, EntitySchemaPropertyType, LogicalOperator } from "@twin.org/entity";
|
|
6
|
+
import { EntityStorageHelper } from "@twin.org/entity-storage-models";
|
|
6
7
|
import { MongoClient } from "mongodb";
|
|
7
8
|
/**
|
|
8
9
|
* Class for performing entity storage operations using MongoDb.
|
|
@@ -22,6 +23,11 @@ export class MongoDbEntityStorageConnector {
|
|
|
22
23
|
* @internal
|
|
23
24
|
*/
|
|
24
25
|
static _PARTITION_KEY = "partitionId";
|
|
26
|
+
/**
|
|
27
|
+
* The name for the schema.
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
30
|
+
_entitySchemaName;
|
|
25
31
|
/**
|
|
26
32
|
* The schema for the entity.
|
|
27
33
|
* @internal
|
|
@@ -53,6 +59,7 @@ export class MongoDbEntityStorageConnector {
|
|
|
53
59
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.host", options.config.host);
|
|
54
60
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.database", options.config.database);
|
|
55
61
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.collection", options.config.collection);
|
|
62
|
+
this._entitySchemaName = options.entitySchema;
|
|
56
63
|
this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
|
|
57
64
|
this._partitionContextIds = options.partitionContextIds;
|
|
58
65
|
this._config = options.config;
|
|
@@ -113,6 +120,14 @@ export class MongoDbEntityStorageConnector {
|
|
|
113
120
|
}
|
|
114
121
|
return true;
|
|
115
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* The component needs to be stopped when the node is closed.
|
|
125
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
126
|
+
* @returns Nothing.
|
|
127
|
+
*/
|
|
128
|
+
async stop(nodeLoggingComponentType) {
|
|
129
|
+
await this._client.close();
|
|
130
|
+
}
|
|
116
131
|
/**
|
|
117
132
|
* Returns the class name of the component.
|
|
118
133
|
* @returns The class name of the component.
|
|
@@ -120,6 +135,37 @@ export class MongoDbEntityStorageConnector {
|
|
|
120
135
|
className() {
|
|
121
136
|
return MongoDbEntityStorageConnector.CLASS_NAME;
|
|
122
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Returns the health status of the component.
|
|
140
|
+
* @returns The health status of the component.
|
|
141
|
+
*/
|
|
142
|
+
async health() {
|
|
143
|
+
try {
|
|
144
|
+
await this._client
|
|
145
|
+
.db(this._config.database)
|
|
146
|
+
.collection(this._config.collection)
|
|
147
|
+
.estimatedDocumentCount();
|
|
148
|
+
return [
|
|
149
|
+
{
|
|
150
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
151
|
+
status: HealthStatus.Ok,
|
|
152
|
+
description: "healthDescription",
|
|
153
|
+
data: { database: this._config.database, collection: this._config.collection }
|
|
154
|
+
}
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return [
|
|
159
|
+
{
|
|
160
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
161
|
+
status: HealthStatus.Error,
|
|
162
|
+
description: "healthDescription",
|
|
163
|
+
message: "connectionFailed",
|
|
164
|
+
data: { database: this._config.database, collection: this._config.collection }
|
|
165
|
+
}
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
123
169
|
/**
|
|
124
170
|
* Get the schema for the entities.
|
|
125
171
|
* @returns The schema for the entities.
|
|
@@ -154,8 +200,11 @@ export class MongoDbEntityStorageConnector {
|
|
|
154
200
|
const collection = await this.getCollection();
|
|
155
201
|
const result = await collection.findOne(query);
|
|
156
202
|
ObjectHelper.propertyDelete(result, "_id");
|
|
157
|
-
|
|
158
|
-
|
|
203
|
+
return Is.objectValue(result)
|
|
204
|
+
? EntityStorageHelper.unPrepareEntity(result, [
|
|
205
|
+
MongoDbEntityStorageConnector._PARTITION_KEY
|
|
206
|
+
])
|
|
207
|
+
: undefined;
|
|
159
208
|
}
|
|
160
209
|
catch (err) {
|
|
161
210
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "getFailed", {
|
|
@@ -173,15 +222,15 @@ export class MongoDbEntityStorageConnector {
|
|
|
173
222
|
Guards.object(MongoDbEntityStorageConnector.CLASS_NAME, "entity", entity);
|
|
174
223
|
const contextIds = await ContextIdStore.getContextIds();
|
|
175
224
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
176
|
-
|
|
225
|
+
const prepared = EntityStorageHelper.prepareEntity(entity, this._entitySchema, Is.stringValue(partitionKey)
|
|
226
|
+
? [{ property: MongoDbEntityStorageConnector._PARTITION_KEY, value: partitionKey }]
|
|
227
|
+
: undefined, { nullBehavior: "omit" });
|
|
177
228
|
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
178
|
-
const id =
|
|
229
|
+
const id = prepared[primaryKey.property];
|
|
179
230
|
try {
|
|
180
231
|
const filter = { [primaryKey.property]: id };
|
|
181
|
-
const finalEntity = ObjectHelper.clone(entity);
|
|
182
232
|
if (Is.stringValue(partitionKey)) {
|
|
183
233
|
filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
|
|
184
|
-
ObjectHelper.propertySet(finalEntity, MongoDbEntityStorageConnector._PARTITION_KEY, partitionKey);
|
|
185
234
|
}
|
|
186
235
|
if (Is.arrayValue(conditions)) {
|
|
187
236
|
for (const condition of conditions) {
|
|
@@ -189,7 +238,7 @@ export class MongoDbEntityStorageConnector {
|
|
|
189
238
|
}
|
|
190
239
|
}
|
|
191
240
|
const collection = await this.getCollection();
|
|
192
|
-
await collection.findOneAndUpdate(filter, { $set:
|
|
241
|
+
await collection.findOneAndUpdate(filter, { $set: prepared }, { upsert: true });
|
|
193
242
|
}
|
|
194
243
|
catch (err) {
|
|
195
244
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "setFailed", {
|
|
@@ -197,6 +246,62 @@ export class MongoDbEntityStorageConnector {
|
|
|
197
246
|
}, err);
|
|
198
247
|
}
|
|
199
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Set multiple entities in a batch.
|
|
251
|
+
* @param entities The entities to set.
|
|
252
|
+
* @returns Nothing.
|
|
253
|
+
*/
|
|
254
|
+
async setBatch(entities) {
|
|
255
|
+
Guards.arrayValue(MongoDbEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
256
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
257
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
258
|
+
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
259
|
+
const preparedEntities = entities.map(entity => EntityStorageHelper.prepareEntity(entity, this._entitySchema, Is.stringValue(partitionKey)
|
|
260
|
+
? [{ property: MongoDbEntityStorageConnector._PARTITION_KEY, value: partitionKey }]
|
|
261
|
+
: undefined, { nullBehavior: "omit" }));
|
|
262
|
+
try {
|
|
263
|
+
const collection = await this.getCollection();
|
|
264
|
+
await collection.bulkWrite(preparedEntities.map(prepared => {
|
|
265
|
+
const filter = {
|
|
266
|
+
[primaryKey.property]: prepared[primaryKey.property]
|
|
267
|
+
};
|
|
268
|
+
if (Is.stringValue(partitionKey)) {
|
|
269
|
+
filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
updateOne: {
|
|
273
|
+
filter,
|
|
274
|
+
update: {
|
|
275
|
+
$set: prepared
|
|
276
|
+
},
|
|
277
|
+
upsert: true
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}));
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Empty the entity storage.
|
|
288
|
+
* @returns Nothing.
|
|
289
|
+
*/
|
|
290
|
+
async empty() {
|
|
291
|
+
try {
|
|
292
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
293
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
294
|
+
const filter = {};
|
|
295
|
+
if (Is.stringValue(partitionKey)) {
|
|
296
|
+
filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
|
|
297
|
+
}
|
|
298
|
+
const collection = await this.getCollection();
|
|
299
|
+
await collection.deleteMany(filter);
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
200
305
|
/**
|
|
201
306
|
* Remove the entity.
|
|
202
307
|
* @param id The id of the entity to remove.
|
|
@@ -225,6 +330,67 @@ export class MongoDbEntityStorageConnector {
|
|
|
225
330
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "removeFailed", { id }, err);
|
|
226
331
|
}
|
|
227
332
|
}
|
|
333
|
+
/**
|
|
334
|
+
* Remove multiple entities by id.
|
|
335
|
+
* @param ids The ids of the entities to remove.
|
|
336
|
+
* @returns Nothing.
|
|
337
|
+
*/
|
|
338
|
+
async removeBatch(ids) {
|
|
339
|
+
Guards.arrayValue(MongoDbEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
340
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
341
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
342
|
+
try {
|
|
343
|
+
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
344
|
+
const filter = {
|
|
345
|
+
[primaryKey.property]: { $in: ids }
|
|
346
|
+
};
|
|
347
|
+
if (Is.stringValue(partitionKey)) {
|
|
348
|
+
filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
|
|
349
|
+
}
|
|
350
|
+
const collection = await this.getCollection();
|
|
351
|
+
await collection.deleteMany(filter);
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Teardown the entity storage by dropping the collection.
|
|
359
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
360
|
+
* @returns True if the teardown process was successful.
|
|
361
|
+
*/
|
|
362
|
+
async teardown(nodeLoggingComponentType) {
|
|
363
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
364
|
+
await nodeLogging?.log({
|
|
365
|
+
level: "info",
|
|
366
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
367
|
+
ts: Date.now(),
|
|
368
|
+
message: "collectionDropping",
|
|
369
|
+
data: { collection: this._config.collection }
|
|
370
|
+
});
|
|
371
|
+
try {
|
|
372
|
+
const collection = await this.getCollection();
|
|
373
|
+
await collection.drop();
|
|
374
|
+
await nodeLogging?.log({
|
|
375
|
+
level: "info",
|
|
376
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
377
|
+
ts: Date.now(),
|
|
378
|
+
message: "collectionDropped",
|
|
379
|
+
data: { collection: this._config.collection }
|
|
380
|
+
});
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
catch (err) {
|
|
384
|
+
await nodeLogging?.log({
|
|
385
|
+
level: "error",
|
|
386
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
387
|
+
ts: Date.now(),
|
|
388
|
+
message: "teardownFailed",
|
|
389
|
+
error: BaseError.fromError(err)
|
|
390
|
+
});
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
228
394
|
/**
|
|
229
395
|
* Find all the entities which match the conditions.
|
|
230
396
|
* @param conditions The conditions to match for the entities.
|
|
@@ -239,24 +405,7 @@ export class MongoDbEntityStorageConnector {
|
|
|
239
405
|
const contextIds = await ContextIdStore.getContextIds();
|
|
240
406
|
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
241
407
|
const returnSize = limit ?? MongoDbEntityStorageConnector._DEFAULT_LIMIT;
|
|
242
|
-
const
|
|
243
|
-
conditions: [],
|
|
244
|
-
logicalOperator: LogicalOperator.And
|
|
245
|
-
};
|
|
246
|
-
if (Is.stringValue(partitionKey)) {
|
|
247
|
-
finalConditions.conditions.push({
|
|
248
|
-
property: MongoDbEntityStorageConnector._PARTITION_KEY,
|
|
249
|
-
comparison: ComparisonOperator.Equals,
|
|
250
|
-
value: partitionKey
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
if (!Is.empty(conditions)) {
|
|
254
|
-
finalConditions.conditions.push(conditions);
|
|
255
|
-
}
|
|
256
|
-
const filter = {};
|
|
257
|
-
if (finalConditions.conditions.length > 0) {
|
|
258
|
-
this.buildQueryParameters("", finalConditions, filter);
|
|
259
|
-
}
|
|
408
|
+
const filter = this.buildFilter(conditions, partitionKey);
|
|
260
409
|
const sort = new Map();
|
|
261
410
|
if (Array.isArray(sortProperties)) {
|
|
262
411
|
for (const sortProperty of sortProperties) {
|
|
@@ -277,30 +426,117 @@ export class MongoDbEntityStorageConnector {
|
|
|
277
426
|
?.find(filter, { projection })
|
|
278
427
|
.sort(sort)
|
|
279
428
|
.skip(cursorValue)
|
|
280
|
-
.limit(returnSize)
|
|
429
|
+
.limit(returnSize + 1)
|
|
281
430
|
.toArray();
|
|
282
|
-
const
|
|
283
|
-
|
|
431
|
+
const rawResults = entitiesResult ?? [];
|
|
432
|
+
const hasMore = rawResults.length > returnSize;
|
|
433
|
+
const entities = hasMore ? rawResults.slice(0, returnSize) : rawResults;
|
|
434
|
+
for (let i = 0; i < entities.length; i++) {
|
|
435
|
+
const entity = entities[i];
|
|
284
436
|
ObjectHelper.propertyDelete(entity, "_id");
|
|
285
|
-
|
|
437
|
+
entities[i] = EntityStorageHelper.unPrepareEntity(entity, [
|
|
438
|
+
MongoDbEntityStorageConnector._PARTITION_KEY
|
|
439
|
+
]);
|
|
286
440
|
}
|
|
287
441
|
return {
|
|
288
442
|
entities,
|
|
289
|
-
cursor:
|
|
443
|
+
cursor: hasMore ? String(cursorValue + returnSize) : undefined
|
|
290
444
|
};
|
|
291
445
|
}
|
|
292
446
|
/**
|
|
293
|
-
*
|
|
294
|
-
* @
|
|
447
|
+
* Count all the entities which match the conditions.
|
|
448
|
+
* @param conditions The optional conditions to match for the entities.
|
|
449
|
+
* @returns The total count of entities in the storage.
|
|
450
|
+
*/
|
|
451
|
+
async count(conditions) {
|
|
452
|
+
try {
|
|
453
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
454
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
455
|
+
const filter = this.buildFilter(conditions, partitionKey);
|
|
456
|
+
return await this._client
|
|
457
|
+
.db(this._config.database)
|
|
458
|
+
.collection(this._config.collection)
|
|
459
|
+
.countDocuments(filter);
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Get all unique partition context ids present in the collection.
|
|
467
|
+
* @returns An array of context id objects, one per unique partition.
|
|
295
468
|
*/
|
|
296
|
-
async
|
|
469
|
+
async getPartitionContextIds() {
|
|
470
|
+
if (!Is.arrayValue(this._partitionContextIds)) {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
297
473
|
try {
|
|
298
474
|
const collection = await this.getCollection();
|
|
299
|
-
await collection.
|
|
475
|
+
const partitionIds = await collection.distinct(MongoDbEntityStorageConnector._PARTITION_KEY);
|
|
476
|
+
return partitionIds
|
|
477
|
+
.filter((id) => Is.stringValue(id))
|
|
478
|
+
.map(partitionId => ContextIdHelper.shortSplit(this._partitionContextIds ?? [], partitionId));
|
|
300
479
|
}
|
|
301
|
-
catch {
|
|
302
|
-
|
|
480
|
+
catch (err) {
|
|
481
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "getPartitionContextIdsFailed", undefined, err);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Create the target connector for performing the migration using a temporary collection.
|
|
486
|
+
* @param newEntitySchema The name of the new entity schema to create the connector for.
|
|
487
|
+
* @returns Connector for performing the migration.
|
|
488
|
+
*/
|
|
489
|
+
async createTargetConnector(newEntitySchema) {
|
|
490
|
+
const migrationCollectionName = `${this._config.collection}Migration${Date.now()}`;
|
|
491
|
+
return new MongoDbEntityStorageConnector({
|
|
492
|
+
entitySchema: newEntitySchema,
|
|
493
|
+
config: {
|
|
494
|
+
...this._config,
|
|
495
|
+
collection: migrationCollectionName
|
|
496
|
+
},
|
|
497
|
+
partitionContextIds: this._partitionContextIds
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Finalize the migration by dropping the source collection and renaming the migration collection to the original name.
|
|
502
|
+
* @param targetConnector The connector holding the migrated data in a temporary collection.
|
|
503
|
+
* @param options The options to control how the migration is finalized.
|
|
504
|
+
* @param loggingComponentType The logging component type to use during finalization.
|
|
505
|
+
* @returns The final connector using the original collection name with the new schema.
|
|
506
|
+
*/
|
|
507
|
+
async finalizeMigration(targetConnector, options, loggingComponentType) {
|
|
508
|
+
// Only rename if the migration collection was actually created (it won't exist if no
|
|
509
|
+
// entities were written, since MongoDB creates collections lazily on first write).
|
|
510
|
+
const migrationCollection = await targetConnector.getCollection();
|
|
511
|
+
const collections = await targetConnector._client
|
|
512
|
+
.db(targetConnector._config.database)
|
|
513
|
+
.listCollections({ name: targetConnector._config.collection })
|
|
514
|
+
.toArray();
|
|
515
|
+
if (collections.length > 0) {
|
|
516
|
+
// Teardown the existing table with the original name to free up the name for the new table
|
|
517
|
+
await this.teardown(loggingComponentType);
|
|
518
|
+
await migrationCollection.rename(this._config.collection);
|
|
303
519
|
}
|
|
520
|
+
const finalConnector = new MongoDbEntityStorageConnector({
|
|
521
|
+
entitySchema: targetConnector._entitySchemaName,
|
|
522
|
+
config: this._config,
|
|
523
|
+
partitionContextIds: this._partitionContextIds
|
|
524
|
+
});
|
|
525
|
+
if (await finalConnector.bootstrap(loggingComponentType)) {
|
|
526
|
+
await targetConnector.stop?.();
|
|
527
|
+
return finalConnector;
|
|
528
|
+
}
|
|
529
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "finalizeMigrationFailedBootstrap", undefined);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* Cleanup a failed or aborted migration by dropping the temporary migration collection.
|
|
533
|
+
* @param targetConnector The target connector to cleanup.
|
|
534
|
+
* @param options The options to control how the migration is cleaned up.
|
|
535
|
+
* @param loggingComponentType The optional component type to use for logging.
|
|
536
|
+
* @returns A promise that resolves when the cleanup is complete.
|
|
537
|
+
*/
|
|
538
|
+
async cleanupMigration(targetConnector, options, loggingComponentType) {
|
|
539
|
+
await targetConnector?.teardown?.(loggingComponentType);
|
|
304
540
|
}
|
|
305
541
|
/**
|
|
306
542
|
* Create a new DB connection configuration.
|
|
@@ -324,6 +560,34 @@ export class MongoDbEntityStorageConnector {
|
|
|
324
560
|
const { database, collection } = this._config;
|
|
325
561
|
return this._client.db(database).collection(collection);
|
|
326
562
|
}
|
|
563
|
+
/**
|
|
564
|
+
* Build a MongoDB filter combining partition key and optional conditions.
|
|
565
|
+
* @param conditions The optional entity conditions to include.
|
|
566
|
+
* @param partitionKey The partition key value.
|
|
567
|
+
* @returns The MongoDB filter object.
|
|
568
|
+
* @internal
|
|
569
|
+
*/
|
|
570
|
+
buildFilter(conditions, partitionKey) {
|
|
571
|
+
const finalConditions = {
|
|
572
|
+
conditions: [],
|
|
573
|
+
logicalOperator: LogicalOperator.And
|
|
574
|
+
};
|
|
575
|
+
if (Is.stringValue(partitionKey)) {
|
|
576
|
+
finalConditions.conditions.push({
|
|
577
|
+
property: MongoDbEntityStorageConnector._PARTITION_KEY,
|
|
578
|
+
comparison: ComparisonOperator.Equals,
|
|
579
|
+
value: partitionKey
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (!Is.empty(conditions)) {
|
|
583
|
+
finalConditions.conditions.push(conditions);
|
|
584
|
+
}
|
|
585
|
+
const filter = {};
|
|
586
|
+
if (finalConditions.conditions.length > 0) {
|
|
587
|
+
this.buildQueryParameters("", finalConditions, filter);
|
|
588
|
+
}
|
|
589
|
+
return filter;
|
|
590
|
+
}
|
|
327
591
|
/**
|
|
328
592
|
* Create an MongoDB filter query.
|
|
329
593
|
* @param objectPath The path for the nested object.
|
|
@@ -352,8 +616,17 @@ export class MongoDbEntityStorageConnector {
|
|
|
352
616
|
}
|
|
353
617
|
}
|
|
354
618
|
else {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
619
|
+
const propertyPath = String(condition.property);
|
|
620
|
+
const prop = objectPath ? `${objectPath}.${propertyPath}` : propertyPath;
|
|
621
|
+
const propertyParts = propertyPath.split(".");
|
|
622
|
+
const schemaLookupName = propertyParts.length > 1 ? propertyParts[0] : propertyPath;
|
|
623
|
+
const propertySchema = this._entitySchema.properties?.find(p => p.property === schemaLookupName);
|
|
624
|
+
// For dot-notation paths the leaf field is always a string value; using the root
|
|
625
|
+
// type directly would send Includes into $elemMatch which does not work for nested
|
|
626
|
+
// string fields. Keeping String here causes mapComparisonOperator to emit $regex,
|
|
627
|
+
// which MongoDB handles correctly for both nested object and array traversal.
|
|
628
|
+
const propertyType = propertyParts.length > 1 ? EntitySchemaPropertyType.String : propertySchema?.type;
|
|
629
|
+
const comparison = this.mapComparisonOperator(condition.comparison, condition.value, propertyType);
|
|
357
630
|
filter[prop] = comparison;
|
|
358
631
|
}
|
|
359
632
|
}
|
|
@@ -361,10 +634,11 @@ export class MongoDbEntityStorageConnector {
|
|
|
361
634
|
* Map the framework comparison operators to those in MongoDB.
|
|
362
635
|
* @param comparison The comparison operator.
|
|
363
636
|
* @param value The value to compare.
|
|
637
|
+
* @param type The type of the property from the schema.
|
|
364
638
|
* @returns The MongoDB comparison expression.
|
|
365
639
|
* @internal
|
|
366
640
|
*/
|
|
367
|
-
mapComparisonOperator(comparison, value) {
|
|
641
|
+
mapComparisonOperator(comparison, value, type) {
|
|
368
642
|
switch (comparison) {
|
|
369
643
|
case ComparisonOperator.Equals:
|
|
370
644
|
return value;
|
|
@@ -381,8 +655,25 @@ export class MongoDbEntityStorageConnector {
|
|
|
381
655
|
case ComparisonOperator.In:
|
|
382
656
|
return { $in: Array.isArray(value) ? value : [value] };
|
|
383
657
|
case ComparisonOperator.Includes:
|
|
658
|
+
// For string fields, use regex for substring matching
|
|
659
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
660
|
+
// Escape special regex characters in the value
|
|
661
|
+
const escapedValue = String(value).replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
662
|
+
return { $regex: escapedValue };
|
|
663
|
+
}
|
|
664
|
+
// For array and object fields, use $elemMatch
|
|
665
|
+
if (type === EntitySchemaPropertyType.Array || type === EntitySchemaPropertyType.Object) {
|
|
666
|
+
return { $elemMatch: { $eq: value } };
|
|
667
|
+
}
|
|
668
|
+
// Fallback to $elemMatch for backwards compatibility
|
|
384
669
|
return { $elemMatch: { $eq: value } };
|
|
385
670
|
case ComparisonOperator.NotIncludes:
|
|
671
|
+
// For string fields, use negated regex
|
|
672
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
673
|
+
const escapedValue = String(value).replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
674
|
+
return { $not: { $regex: escapedValue } };
|
|
675
|
+
}
|
|
676
|
+
// For arrays, use $elemMatch with $ne
|
|
386
677
|
return { $elemMatch: { $ne: value } };
|
|
387
678
|
default:
|
|
388
679
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "unsupportedComparisonOperator", { comparison });
|