@twin.org/entity-storage-connector-mongodb 0.0.3-next.9 → 0.9.0-next.1
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/dist/es/models/IMongoDbEntityStorageConnectorConstructorOptions.js.map +1 -1
- package/dist/es/mongoDbEntityStorageConnector.js +332 -65
- package/dist/es/mongoDbEntityStorageConnector.js.map +1 -1
- package/dist/types/models/IMongoDbEntityStorageConnectorConstructorOptions.d.ts +0 -1
- package/dist/types/mongoDbEntityStorageConnector.d.ts +69 -5
- package/docs/changelog.md +586 -55
- package/docs/reference/classes/MongoDbEntityStorageConnector.md +302 -12
- package/docs/reference/interfaces/IMongoDbEntityStorageConnectorConstructorOptions.md +0 -6
- package/locales/en.json +17 -2
- package/package.json +10 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IMongoDbEntityStorageConnectorConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IMongoDbEntityStorageConnectorConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IMongoDbEntityStorageConnectorConfig } from \"./IMongoDbEntityStorageConnectorConfig.js\";\n\n/**\n * The options for the MongoDb entity storage connector constructor.\n */\nexport interface IMongoDbEntityStorageConnectorConstructorOptions {\n\t/**\n\t * The schema for the entity.\n\t */\n\tentitySchema: string;\n\n\t/**\n\t * The keys to use from the context ids to create partitions.\n\t */\n\tpartitionContextIds?: string[];\n\n\t/**\n\t * The type of logging component to use.\n\t
|
|
1
|
+
{"version":3,"file":"IMongoDbEntityStorageConnectorConstructorOptions.js","sourceRoot":"","sources":["../../../src/models/IMongoDbEntityStorageConnectorConstructorOptions.ts"],"names":[],"mappings":"","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nimport type { IMongoDbEntityStorageConnectorConfig } from \"./IMongoDbEntityStorageConnectorConfig.js\";\n\n/**\n * The options for the MongoDb entity storage connector constructor.\n */\nexport interface IMongoDbEntityStorageConnectorConstructorOptions {\n\t/**\n\t * The schema for the entity.\n\t */\n\tentitySchema: string;\n\n\t/**\n\t * The keys to use from the context ids to create partitions.\n\t */\n\tpartitionContextIds?: string[];\n\n\t/**\n\t * The type of logging component to use.\n\t */\n\tloggingComponentType?: string;\n\n\t/**\n\t * The configuration for the connector.\n\t */\n\tconfig: IMongoDbEntityStorageConnectorConfig;\n}\n"]}
|
|
@@ -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";
|
|
4
|
+
import { BaseError, ComponentFactory, GeneralError, Guards, HealthStatus, Is, ObjectHelper, Validation } from "@twin.org/core";
|
|
5
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.
|
|
@@ -18,10 +19,10 @@ export class MongoDbEntityStorageConnector {
|
|
|
18
19
|
*/
|
|
19
20
|
static _DEFAULT_LIMIT = 40;
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
+
* The name for the schema.
|
|
22
23
|
* @internal
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
+
_entitySchemaName;
|
|
25
26
|
/**
|
|
26
27
|
* The schema for the entity.
|
|
27
28
|
* @internal
|
|
@@ -53,6 +54,7 @@ export class MongoDbEntityStorageConnector {
|
|
|
53
54
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.host", options.config.host);
|
|
54
55
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.database", options.config.database);
|
|
55
56
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.collection", options.config.collection);
|
|
57
|
+
this._entitySchemaName = options.entitySchema;
|
|
56
58
|
this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
|
|
57
59
|
this._partitionContextIds = options.partitionContextIds;
|
|
58
60
|
this._config = options.config;
|
|
@@ -113,6 +115,14 @@ export class MongoDbEntityStorageConnector {
|
|
|
113
115
|
}
|
|
114
116
|
return true;
|
|
115
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* The component needs to be stopped when the node is closed.
|
|
120
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
121
|
+
* @returns Nothing.
|
|
122
|
+
*/
|
|
123
|
+
async stop(nodeLoggingComponentType) {
|
|
124
|
+
await this._client.close();
|
|
125
|
+
}
|
|
116
126
|
/**
|
|
117
127
|
* Returns the class name of the component.
|
|
118
128
|
* @returns The class name of the component.
|
|
@@ -120,6 +130,37 @@ export class MongoDbEntityStorageConnector {
|
|
|
120
130
|
className() {
|
|
121
131
|
return MongoDbEntityStorageConnector.CLASS_NAME;
|
|
122
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Returns the health status of the component.
|
|
135
|
+
* @returns The health status of the component.
|
|
136
|
+
*/
|
|
137
|
+
async health() {
|
|
138
|
+
try {
|
|
139
|
+
await this._client
|
|
140
|
+
.db(this._config.database)
|
|
141
|
+
.collection(this._config.collection)
|
|
142
|
+
.estimatedDocumentCount();
|
|
143
|
+
return [
|
|
144
|
+
{
|
|
145
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
146
|
+
status: HealthStatus.Ok,
|
|
147
|
+
description: "healthDescription",
|
|
148
|
+
data: { database: this._config.database, collection: this._config.collection }
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return [
|
|
154
|
+
{
|
|
155
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
156
|
+
status: HealthStatus.Error,
|
|
157
|
+
description: "healthDescription",
|
|
158
|
+
message: "connectionFailed",
|
|
159
|
+
data: { database: this._config.database, collection: this._config.collection }
|
|
160
|
+
}
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
123
164
|
/**
|
|
124
165
|
* Get the schema for the entities.
|
|
125
166
|
* @returns The schema for the entities.
|
|
@@ -136,16 +177,11 @@ export class MongoDbEntityStorageConnector {
|
|
|
136
177
|
*/
|
|
137
178
|
async get(id, secondaryIndex, conditions) {
|
|
138
179
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "id", id);
|
|
139
|
-
const contextIds = await ContextIdStore.getContextIds();
|
|
140
|
-
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
141
180
|
try {
|
|
142
181
|
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
143
182
|
const query = Is.empty(secondaryIndex)
|
|
144
183
|
? { [primaryKey.property]: id }
|
|
145
184
|
: { [secondaryIndex]: id };
|
|
146
|
-
if (Is.stringValue(partitionKey)) {
|
|
147
|
-
query[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
|
|
148
|
-
}
|
|
149
185
|
if (conditions) {
|
|
150
186
|
for (const condition of conditions) {
|
|
151
187
|
query[condition.property] = condition.value;
|
|
@@ -154,8 +190,9 @@ export class MongoDbEntityStorageConnector {
|
|
|
154
190
|
const collection = await this.getCollection();
|
|
155
191
|
const result = await collection.findOne(query);
|
|
156
192
|
ObjectHelper.propertyDelete(result, "_id");
|
|
157
|
-
|
|
158
|
-
|
|
193
|
+
return Is.objectValue(result)
|
|
194
|
+
? EntityStorageHelper.unPrepareEntity(result, [])
|
|
195
|
+
: undefined;
|
|
159
196
|
}
|
|
160
197
|
catch (err) {
|
|
161
198
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "getFailed", {
|
|
@@ -171,25 +208,20 @@ export class MongoDbEntityStorageConnector {
|
|
|
171
208
|
*/
|
|
172
209
|
async set(entity, conditions) {
|
|
173
210
|
Guards.object(MongoDbEntityStorageConnector.CLASS_NAME, "entity", entity);
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
211
|
+
const prepared = EntityStorageHelper.prepareEntity(entity, this._entitySchema, undefined, {
|
|
212
|
+
nullBehavior: "omit"
|
|
213
|
+
});
|
|
177
214
|
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
178
|
-
const id =
|
|
215
|
+
const id = prepared[primaryKey.property];
|
|
179
216
|
try {
|
|
180
217
|
const filter = { [primaryKey.property]: id };
|
|
181
|
-
const finalEntity = ObjectHelper.clone(entity);
|
|
182
|
-
if (Is.stringValue(partitionKey)) {
|
|
183
|
-
filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
|
|
184
|
-
ObjectHelper.propertySet(finalEntity, MongoDbEntityStorageConnector._PARTITION_KEY, partitionKey);
|
|
185
|
-
}
|
|
186
218
|
if (Is.arrayValue(conditions)) {
|
|
187
219
|
for (const condition of conditions) {
|
|
188
220
|
filter[condition.property] = condition.value;
|
|
189
221
|
}
|
|
190
222
|
}
|
|
191
223
|
const collection = await this.getCollection();
|
|
192
|
-
await collection.findOneAndUpdate(filter, { $set:
|
|
224
|
+
await collection.findOneAndUpdate(filter, { $set: prepared }, { upsert: true });
|
|
193
225
|
}
|
|
194
226
|
catch (err) {
|
|
195
227
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "setFailed", {
|
|
@@ -197,6 +229,51 @@ export class MongoDbEntityStorageConnector {
|
|
|
197
229
|
}, err);
|
|
198
230
|
}
|
|
199
231
|
}
|
|
232
|
+
/**
|
|
233
|
+
* Set multiple entities in a batch.
|
|
234
|
+
* @param entities The entities to set.
|
|
235
|
+
* @returns Nothing.
|
|
236
|
+
*/
|
|
237
|
+
async setBatch(entities) {
|
|
238
|
+
Guards.arrayValue(MongoDbEntityStorageConnector.CLASS_NAME, "entities", entities);
|
|
239
|
+
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
240
|
+
const preparedEntities = entities.map(entity => EntityStorageHelper.prepareEntity(entity, this._entitySchema, undefined, {
|
|
241
|
+
nullBehavior: "omit"
|
|
242
|
+
}));
|
|
243
|
+
try {
|
|
244
|
+
const collection = await this.getCollection();
|
|
245
|
+
await collection.bulkWrite(preparedEntities.map(prepared => {
|
|
246
|
+
const filter = {
|
|
247
|
+
[primaryKey.property]: prepared[primaryKey.property]
|
|
248
|
+
};
|
|
249
|
+
return {
|
|
250
|
+
updateOne: {
|
|
251
|
+
filter,
|
|
252
|
+
update: {
|
|
253
|
+
$set: prepared
|
|
254
|
+
},
|
|
255
|
+
upsert: true
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
catch (err) {
|
|
261
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Empty the entity storage.
|
|
266
|
+
* @returns Nothing.
|
|
267
|
+
*/
|
|
268
|
+
async empty() {
|
|
269
|
+
try {
|
|
270
|
+
const collection = await this.getCollection();
|
|
271
|
+
await collection.deleteMany({});
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
200
277
|
/**
|
|
201
278
|
* Remove the entity.
|
|
202
279
|
* @param id The id of the entity to remove.
|
|
@@ -205,14 +282,9 @@ export class MongoDbEntityStorageConnector {
|
|
|
205
282
|
*/
|
|
206
283
|
async remove(id, conditions) {
|
|
207
284
|
Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "id", id);
|
|
208
|
-
const contextIds = await ContextIdStore.getContextIds();
|
|
209
|
-
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
210
285
|
try {
|
|
211
286
|
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
212
287
|
const query = { [primaryKey.property]: id };
|
|
213
|
-
if (Is.stringValue(partitionKey)) {
|
|
214
|
-
query[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
|
|
215
|
-
}
|
|
216
288
|
if (conditions) {
|
|
217
289
|
for (const condition of conditions) {
|
|
218
290
|
query[condition.property] = condition.value;
|
|
@@ -225,6 +297,74 @@ export class MongoDbEntityStorageConnector {
|
|
|
225
297
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "removeFailed", { id }, err);
|
|
226
298
|
}
|
|
227
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Remove multiple entities by id.
|
|
302
|
+
* @param ids The ids of the entities to remove.
|
|
303
|
+
* @returns Nothing.
|
|
304
|
+
*/
|
|
305
|
+
async removeBatch(ids) {
|
|
306
|
+
Guards.arrayValue(MongoDbEntityStorageConnector.CLASS_NAME, "ids", ids);
|
|
307
|
+
try {
|
|
308
|
+
const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
|
|
309
|
+
const filter = {
|
|
310
|
+
[primaryKey.property]: { $in: ids }
|
|
311
|
+
};
|
|
312
|
+
const collection = await this.getCollection();
|
|
313
|
+
await collection.deleteMany(filter);
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Teardown the entity storage by dropping the collection.
|
|
321
|
+
* @param nodeLoggingComponentType The node logging component type.
|
|
322
|
+
* @returns True if the teardown process was successful.
|
|
323
|
+
*/
|
|
324
|
+
async teardown(nodeLoggingComponentType) {
|
|
325
|
+
const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
|
|
326
|
+
await nodeLogging?.log({
|
|
327
|
+
level: "info",
|
|
328
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
329
|
+
ts: Date.now(),
|
|
330
|
+
message: "collectionDropping",
|
|
331
|
+
data: { collection: this._config.collection }
|
|
332
|
+
});
|
|
333
|
+
try {
|
|
334
|
+
if (Is.arrayValue(this._partitionContextIds)) {
|
|
335
|
+
const db = this._client.db(this._config.database);
|
|
336
|
+
const collections = await this.listPartitionCollections();
|
|
337
|
+
for (const col of collections) {
|
|
338
|
+
await db
|
|
339
|
+
.collection(col.name)
|
|
340
|
+
.drop()
|
|
341
|
+
.catch(() => { });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const collection = await this.getCollection();
|
|
346
|
+
await collection.drop();
|
|
347
|
+
}
|
|
348
|
+
await nodeLogging?.log({
|
|
349
|
+
level: "info",
|
|
350
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
351
|
+
ts: Date.now(),
|
|
352
|
+
message: "collectionDropped",
|
|
353
|
+
data: { collection: this._config.collection }
|
|
354
|
+
});
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
catch (err) {
|
|
358
|
+
await nodeLogging?.log({
|
|
359
|
+
level: "error",
|
|
360
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
361
|
+
ts: Date.now(),
|
|
362
|
+
message: "teardownFailed",
|
|
363
|
+
error: BaseError.fromError(err)
|
|
364
|
+
});
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
228
368
|
/**
|
|
229
369
|
* Find all the entities which match the conditions.
|
|
230
370
|
* @param conditions The conditions to match for the entities.
|
|
@@ -236,27 +376,15 @@ export class MongoDbEntityStorageConnector {
|
|
|
236
376
|
* and a cursor which can be used to request more entities.
|
|
237
377
|
*/
|
|
238
378
|
async query(conditions, sortProperties, properties, cursor, limit) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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);
|
|
379
|
+
EntityStorageHelper.validateSortProperties(this._entitySchema, sortProperties);
|
|
380
|
+
EntityStorageHelper.validateProperties(this._entitySchema, properties);
|
|
381
|
+
if (!Is.empty(limit)) {
|
|
382
|
+
const validationFailures = [];
|
|
383
|
+
Validation.integer("limit", limit, validationFailures, undefined, { minValue: 1 });
|
|
384
|
+
Validation.asValidationError(MongoDbEntityStorageConnector.CLASS_NAME, "query", validationFailures);
|
|
259
385
|
}
|
|
386
|
+
const returnSize = limit ?? MongoDbEntityStorageConnector._DEFAULT_LIMIT;
|
|
387
|
+
const filter = this.buildFilter(conditions);
|
|
260
388
|
const sort = new Map();
|
|
261
389
|
if (Array.isArray(sortProperties)) {
|
|
262
390
|
for (const sortProperty of sortProperties) {
|
|
@@ -272,35 +400,121 @@ export class MongoDbEntityStorageConnector {
|
|
|
272
400
|
const cursorValue = cursor ? Number(cursor) : 0;
|
|
273
401
|
const collection = await this.getCollection();
|
|
274
402
|
const entitiesResult = await collection
|
|
275
|
-
// False positive, this is not an array find call
|
|
276
|
-
// eslint-disable-next-line unicorn/no-array-callback-reference
|
|
277
403
|
?.find(filter, { projection })
|
|
278
404
|
.sort(sort)
|
|
279
405
|
.skip(cursorValue)
|
|
280
|
-
.limit(returnSize)
|
|
406
|
+
.limit(returnSize + 1)
|
|
281
407
|
.toArray();
|
|
282
|
-
const
|
|
283
|
-
|
|
408
|
+
const rawResults = entitiesResult ?? [];
|
|
409
|
+
const hasMore = rawResults.length > returnSize;
|
|
410
|
+
const entities = hasMore ? rawResults.slice(0, returnSize) : rawResults;
|
|
411
|
+
for (let i = 0; i < entities.length; i++) {
|
|
412
|
+
const entity = entities[i];
|
|
284
413
|
ObjectHelper.propertyDelete(entity, "_id");
|
|
285
|
-
|
|
414
|
+
entities[i] = EntityStorageHelper.unPrepareEntity(entity, []);
|
|
286
415
|
}
|
|
287
416
|
return {
|
|
288
417
|
entities,
|
|
289
|
-
cursor:
|
|
418
|
+
cursor: hasMore ? String(cursorValue + returnSize) : undefined
|
|
290
419
|
};
|
|
291
420
|
}
|
|
292
421
|
/**
|
|
293
|
-
*
|
|
294
|
-
* @
|
|
422
|
+
* Count all the entities which match the conditions.
|
|
423
|
+
* @param conditions The optional conditions to match for the entities.
|
|
424
|
+
* @returns The total count of entities in the storage.
|
|
295
425
|
*/
|
|
296
|
-
async
|
|
426
|
+
async count(conditions) {
|
|
297
427
|
try {
|
|
428
|
+
const filter = this.buildFilter(conditions);
|
|
298
429
|
const collection = await this.getCollection();
|
|
299
|
-
await collection.
|
|
430
|
+
return await collection.countDocuments(filter);
|
|
300
431
|
}
|
|
301
|
-
catch {
|
|
302
|
-
|
|
432
|
+
catch (err) {
|
|
433
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Get all unique partition context ids present in the collection.
|
|
438
|
+
* @returns An array of context id objects, one per unique partition.
|
|
439
|
+
*/
|
|
440
|
+
async getPartitionContextIds() {
|
|
441
|
+
if (!Is.arrayValue(this._partitionContextIds)) {
|
|
442
|
+
return [];
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
const prefix = `${this._config.collection}_`;
|
|
446
|
+
const collections = await this.listPartitionCollections();
|
|
447
|
+
return collections.map(col => ContextIdHelper.shortSplit(this._partitionContextIds ?? [], col.name.slice(prefix.length)));
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "getPartitionContextIdsFailed", undefined, err);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Create the target connector for performing the migration using a temporary collection.
|
|
455
|
+
* @param newEntitySchema The name of the new entity schema to create the connector for.
|
|
456
|
+
* @returns Connector for performing the migration.
|
|
457
|
+
*/
|
|
458
|
+
async createTargetConnector(newEntitySchema) {
|
|
459
|
+
const migrationCollectionName = `${this._config.collection}Migration${Date.now()}`;
|
|
460
|
+
return new MongoDbEntityStorageConnector({
|
|
461
|
+
entitySchema: newEntitySchema,
|
|
462
|
+
config: {
|
|
463
|
+
...this._config,
|
|
464
|
+
collection: migrationCollectionName
|
|
465
|
+
},
|
|
466
|
+
partitionContextIds: this._partitionContextIds
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Finalize the migration by dropping the source collection and renaming the migration collection to the original name.
|
|
471
|
+
* @param targetConnector The connector holding the migrated data in a temporary collection.
|
|
472
|
+
* @param options The options to control how the migration is finalized.
|
|
473
|
+
* @param loggingComponentType The logging component type to use during finalization.
|
|
474
|
+
* @returns The final connector using the original collection name with the new schema.
|
|
475
|
+
*/
|
|
476
|
+
async finalizeMigration(targetConnector, options, loggingComponentType) {
|
|
477
|
+
// With collection-per-partition each partition is a separate collection, so we must
|
|
478
|
+
// rename every target partition collection to the corresponding source name. We do this
|
|
479
|
+
// without relying on context so that all partitions are handled in a single call.
|
|
480
|
+
const targetBase = targetConnector._config.collection;
|
|
481
|
+
const sourceBase = this._config.collection;
|
|
482
|
+
const targetDb = targetConnector._client.db(targetConnector._config.database);
|
|
483
|
+
const sourceDb = this._client.db(this._config.database);
|
|
484
|
+
// Find all collections the target connector wrote to (exact base name or with a _suffix).
|
|
485
|
+
const allCollections = await targetDb.listCollections().toArray();
|
|
486
|
+
const migrationCollections = allCollections.filter(c => c.name === targetBase || c.name.startsWith(`${targetBase}_`));
|
|
487
|
+
for (const col of migrationCollections) {
|
|
488
|
+
// Preserve whatever suffix (empty, or "_partitionKey") was appended to the base name.
|
|
489
|
+
const suffix = col.name.slice(targetBase.length);
|
|
490
|
+
const finalName = `${sourceBase}${suffix}`;
|
|
491
|
+
// Drop the existing source collection to free up the name.
|
|
492
|
+
try {
|
|
493
|
+
await sourceDb.collection(finalName).drop();
|
|
494
|
+
}
|
|
495
|
+
catch { } // collection may not exist yet
|
|
496
|
+
await targetDb.collection(col.name).rename(finalName);
|
|
303
497
|
}
|
|
498
|
+
const finalConnector = new MongoDbEntityStorageConnector({
|
|
499
|
+
entitySchema: targetConnector._entitySchemaName,
|
|
500
|
+
config: this._config,
|
|
501
|
+
partitionContextIds: this._partitionContextIds
|
|
502
|
+
});
|
|
503
|
+
if (await finalConnector.bootstrap(loggingComponentType)) {
|
|
504
|
+
await targetConnector.stop?.();
|
|
505
|
+
return finalConnector;
|
|
506
|
+
}
|
|
507
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "finalizeMigrationFailedBootstrap", undefined);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Cleanup a failed or aborted migration by dropping the temporary migration collection.
|
|
511
|
+
* @param targetConnector The target connector to cleanup.
|
|
512
|
+
* @param options The options to control how the migration is cleaned up.
|
|
513
|
+
* @param loggingComponentType The optional component type to use for logging.
|
|
514
|
+
* @returns A promise that resolves when the cleanup is complete.
|
|
515
|
+
*/
|
|
516
|
+
async cleanupMigration(targetConnector, options, loggingComponentType) {
|
|
517
|
+
await targetConnector?.teardown?.(loggingComponentType);
|
|
304
518
|
}
|
|
305
519
|
/**
|
|
306
520
|
* Create a new DB connection configuration.
|
|
@@ -316,13 +530,63 @@ export class MongoDbEntityStorageConnector {
|
|
|
316
530
|
return `mongodb://${host}${portPart}/${database}`;
|
|
317
531
|
}
|
|
318
532
|
/**
|
|
319
|
-
* Return a Mongo DB collection.
|
|
533
|
+
* Return a Mongo DB collection for the current partition context.
|
|
320
534
|
* @returns The MongoDb collection.
|
|
321
535
|
* @internal
|
|
322
536
|
*/
|
|
323
537
|
async getCollection() {
|
|
324
|
-
const
|
|
325
|
-
return this._client.db(database).collection(
|
|
538
|
+
const collectionName = await this.resolveCollectionName(this._config.collection);
|
|
539
|
+
return this._client.db(this._config.database).collection(collectionName);
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Resolve the collection name for a base name, appending the partition key when applicable.
|
|
543
|
+
* @param base The base collection name.
|
|
544
|
+
* @returns The resolved collection name.
|
|
545
|
+
* @internal
|
|
546
|
+
*/
|
|
547
|
+
async resolveCollectionName(base) {
|
|
548
|
+
if (!Is.arrayValue(this._partitionContextIds)) {
|
|
549
|
+
return base;
|
|
550
|
+
}
|
|
551
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
552
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
553
|
+
return Is.stringValue(partitionKey) ? `${base}_${partitionKey.replace(/[\0$]/g, "_")}` : base;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Escape special regex characters in a string for use in a MongoDB $regex query.
|
|
557
|
+
* @param value The string to escape.
|
|
558
|
+
* @returns The escaped string.
|
|
559
|
+
* @internal
|
|
560
|
+
*/
|
|
561
|
+
escapeRegex(value) {
|
|
562
|
+
return value.replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* List all collections that belong to this connector's partition set.
|
|
566
|
+
* @returns The collection info objects whose names share the connector's base prefix.
|
|
567
|
+
* @internal
|
|
568
|
+
*/
|
|
569
|
+
async listPartitionCollections() {
|
|
570
|
+
const prefix = `${this._config.collection}_`;
|
|
571
|
+
const db = this._client.db(this._config.database);
|
|
572
|
+
return db.listCollections({ name: { $regex: `^${this.escapeRegex(prefix)}` } }).toArray();
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Build a MongoDB filter from optional conditions.
|
|
576
|
+
* @param conditions The optional entity conditions to include.
|
|
577
|
+
* @returns The MongoDB filter object.
|
|
578
|
+
* @internal
|
|
579
|
+
*/
|
|
580
|
+
buildFilter(conditions) {
|
|
581
|
+
const filter = {};
|
|
582
|
+
if (!Is.empty(conditions)) {
|
|
583
|
+
const finalConditions = {
|
|
584
|
+
conditions: [conditions],
|
|
585
|
+
logicalOperator: LogicalOperator.And
|
|
586
|
+
};
|
|
587
|
+
this.buildQueryParameters("", finalConditions, filter);
|
|
588
|
+
}
|
|
589
|
+
return filter;
|
|
326
590
|
}
|
|
327
591
|
/**
|
|
328
592
|
* Create an MongoDB filter query.
|
|
@@ -372,6 +636,7 @@ export class MongoDbEntityStorageConnector {
|
|
|
372
636
|
* @param value The value to compare.
|
|
373
637
|
* @param type The type of the property from the schema.
|
|
374
638
|
* @returns The MongoDB comparison expression.
|
|
639
|
+
* @throws GeneralError if the comparison operator is not supported.
|
|
375
640
|
* @internal
|
|
376
641
|
*/
|
|
377
642
|
mapComparisonOperator(comparison, value, type) {
|
|
@@ -394,7 +659,7 @@ export class MongoDbEntityStorageConnector {
|
|
|
394
659
|
// For string fields, use regex for substring matching
|
|
395
660
|
if (type === EntitySchemaPropertyType.String) {
|
|
396
661
|
// Escape special regex characters in the value
|
|
397
|
-
const escapedValue = String(value)
|
|
662
|
+
const escapedValue = this.escapeRegex(String(value));
|
|
398
663
|
return { $regex: escapedValue };
|
|
399
664
|
}
|
|
400
665
|
// For array and object fields, use $elemMatch
|
|
@@ -406,11 +671,13 @@ export class MongoDbEntityStorageConnector {
|
|
|
406
671
|
case ComparisonOperator.NotIncludes:
|
|
407
672
|
// For string fields, use negated regex
|
|
408
673
|
if (type === EntitySchemaPropertyType.String) {
|
|
409
|
-
const escapedValue = String(value)
|
|
674
|
+
const escapedValue = this.escapeRegex(String(value));
|
|
410
675
|
return { $not: { $regex: escapedValue } };
|
|
411
676
|
}
|
|
412
|
-
// For
|
|
413
|
-
|
|
677
|
+
// For array/object fields: $ne on an array field matches documents where
|
|
678
|
+
// none of the array elements equal the value (MongoDB element-wise semantics).
|
|
679
|
+
// $elemMatch: { $ne: value } is wrong — it matches if *any* element ≠ value.
|
|
680
|
+
return { $ne: value };
|
|
414
681
|
default:
|
|
415
682
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "unsupportedComparisonOperator", { comparison });
|
|
416
683
|
}
|