@twin.org/entity-storage-connector-mongodb 0.0.3-next.3 → 0.0.3-next.30
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/models/IMongoDbEntityStorageConnectorConstructorOptions.js.map +1 -1
- package/dist/es/mongoDbEntityStorageConnector.js +333 -64
- 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 +578 -47
- 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 -12
- 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 +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";
|
|
5
|
-
import { ComparisonOperator, EntitySchemaFactory, EntitySchemaHelper, LogicalOperator } from "@twin.org/entity";
|
|
4
|
+
import { BaseError, ComponentFactory, GeneralError, Guards, HealthStatus, Is, ObjectHelper, Validation } 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.
|
|
@@ -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,62 @@ 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
|
+
const collection = await this.getCollection();
|
|
335
|
+
await collection.drop();
|
|
336
|
+
await nodeLogging?.log({
|
|
337
|
+
level: "info",
|
|
338
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
339
|
+
ts: Date.now(),
|
|
340
|
+
message: "collectionDropped",
|
|
341
|
+
data: { collection: this._config.collection }
|
|
342
|
+
});
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
await nodeLogging?.log({
|
|
347
|
+
level: "error",
|
|
348
|
+
source: MongoDbEntityStorageConnector.CLASS_NAME,
|
|
349
|
+
ts: Date.now(),
|
|
350
|
+
message: "teardownFailed",
|
|
351
|
+
error: BaseError.fromError(err)
|
|
352
|
+
});
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
228
356
|
/**
|
|
229
357
|
* Find all the entities which match the conditions.
|
|
230
358
|
* @param conditions The conditions to match for the entities.
|
|
@@ -236,27 +364,15 @@ export class MongoDbEntityStorageConnector {
|
|
|
236
364
|
* and a cursor which can be used to request more entities.
|
|
237
365
|
*/
|
|
238
366
|
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);
|
|
367
|
+
EntityStorageHelper.validateSortProperties(this._entitySchema, sortProperties);
|
|
368
|
+
EntityStorageHelper.validateProperties(this._entitySchema, properties);
|
|
369
|
+
if (!Is.empty(limit)) {
|
|
370
|
+
const validationFailures = [];
|
|
371
|
+
Validation.integer("limit", limit, validationFailures, undefined, { minValue: 1 });
|
|
372
|
+
Validation.asValidationError(MongoDbEntityStorageConnector.CLASS_NAME, "query", validationFailures);
|
|
259
373
|
}
|
|
374
|
+
const returnSize = limit ?? MongoDbEntityStorageConnector._DEFAULT_LIMIT;
|
|
375
|
+
const filter = this.buildFilter(conditions);
|
|
260
376
|
const sort = new Map();
|
|
261
377
|
if (Array.isArray(sortProperties)) {
|
|
262
378
|
for (const sortProperty of sortProperties) {
|
|
@@ -277,31 +393,123 @@ export class MongoDbEntityStorageConnector {
|
|
|
277
393
|
?.find(filter, { projection })
|
|
278
394
|
.sort(sort)
|
|
279
395
|
.skip(cursorValue)
|
|
280
|
-
.limit(returnSize)
|
|
396
|
+
.limit(returnSize + 1)
|
|
281
397
|
.toArray();
|
|
282
|
-
const
|
|
283
|
-
|
|
398
|
+
const rawResults = entitiesResult ?? [];
|
|
399
|
+
const hasMore = rawResults.length > returnSize;
|
|
400
|
+
const entities = hasMore ? rawResults.slice(0, returnSize) : rawResults;
|
|
401
|
+
for (let i = 0; i < entities.length; i++) {
|
|
402
|
+
const entity = entities[i];
|
|
284
403
|
ObjectHelper.propertyDelete(entity, "_id");
|
|
285
|
-
|
|
404
|
+
entities[i] = EntityStorageHelper.unPrepareEntity(entity, []);
|
|
286
405
|
}
|
|
287
406
|
return {
|
|
288
407
|
entities,
|
|
289
|
-
cursor:
|
|
408
|
+
cursor: hasMore ? String(cursorValue + returnSize) : undefined
|
|
290
409
|
};
|
|
291
410
|
}
|
|
292
411
|
/**
|
|
293
|
-
*
|
|
294
|
-
* @
|
|
412
|
+
* Count all the entities which match the conditions.
|
|
413
|
+
* @param conditions The optional conditions to match for the entities.
|
|
414
|
+
* @returns The total count of entities in the storage.
|
|
295
415
|
*/
|
|
296
|
-
async
|
|
416
|
+
async count(conditions) {
|
|
297
417
|
try {
|
|
418
|
+
const filter = this.buildFilter(conditions);
|
|
298
419
|
const collection = await this.getCollection();
|
|
299
|
-
await collection.
|
|
420
|
+
return await collection.countDocuments(filter);
|
|
300
421
|
}
|
|
301
|
-
catch {
|
|
302
|
-
|
|
422
|
+
catch (err) {
|
|
423
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Get all unique partition context ids present in the collection.
|
|
428
|
+
* @returns An array of context id objects, one per unique partition.
|
|
429
|
+
*/
|
|
430
|
+
async getPartitionContextIds() {
|
|
431
|
+
if (!Is.arrayValue(this._partitionContextIds)) {
|
|
432
|
+
return [];
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
const prefix = `${this._config.collection}_`;
|
|
436
|
+
const escapedPrefix = prefix.replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
437
|
+
const db = this._client.db(this._config.database);
|
|
438
|
+
const collections = await db
|
|
439
|
+
.listCollections({ name: { $regex: `^${escapedPrefix}` } })
|
|
440
|
+
.toArray();
|
|
441
|
+
return collections.map(col => ContextIdHelper.shortSplit(this._partitionContextIds ?? [], col.name.slice(prefix.length)));
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "getPartitionContextIdsFailed", undefined, err);
|
|
303
445
|
}
|
|
304
446
|
}
|
|
447
|
+
/**
|
|
448
|
+
* Create the target connector for performing the migration using a temporary collection.
|
|
449
|
+
* @param newEntitySchema The name of the new entity schema to create the connector for.
|
|
450
|
+
* @returns Connector for performing the migration.
|
|
451
|
+
*/
|
|
452
|
+
async createTargetConnector(newEntitySchema) {
|
|
453
|
+
const migrationCollectionName = `${this._config.collection}Migration${Date.now()}`;
|
|
454
|
+
return new MongoDbEntityStorageConnector({
|
|
455
|
+
entitySchema: newEntitySchema,
|
|
456
|
+
config: {
|
|
457
|
+
...this._config,
|
|
458
|
+
collection: migrationCollectionName
|
|
459
|
+
},
|
|
460
|
+
partitionContextIds: this._partitionContextIds
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Finalize the migration by dropping the source collection and renaming the migration collection to the original name.
|
|
465
|
+
* @param targetConnector The connector holding the migrated data in a temporary collection.
|
|
466
|
+
* @param options The options to control how the migration is finalized.
|
|
467
|
+
* @param loggingComponentType The logging component type to use during finalization.
|
|
468
|
+
* @returns The final connector using the original collection name with the new schema.
|
|
469
|
+
*/
|
|
470
|
+
async finalizeMigration(targetConnector, options, loggingComponentType) {
|
|
471
|
+
// With collection-per-partition each partition is a separate collection, so we must
|
|
472
|
+
// rename every target partition collection to the corresponding source name. We do this
|
|
473
|
+
// without relying on context so that all partitions are handled in a single call.
|
|
474
|
+
const targetBase = targetConnector._config.collection;
|
|
475
|
+
const sourceBase = this._config.collection;
|
|
476
|
+
const targetDb = targetConnector._client.db(targetConnector._config.database);
|
|
477
|
+
const sourceDb = this._client.db(this._config.database);
|
|
478
|
+
// Find all collections the target connector wrote to (exact base name or with a _suffix).
|
|
479
|
+
const allCollections = await targetDb.listCollections().toArray();
|
|
480
|
+
const migrationCollections = allCollections.filter(c => c.name === targetBase || c.name.startsWith(`${targetBase}_`));
|
|
481
|
+
for (const col of migrationCollections) {
|
|
482
|
+
// Preserve whatever suffix (empty, or "_partitionKey") was appended to the base name.
|
|
483
|
+
const suffix = col.name.slice(targetBase.length);
|
|
484
|
+
const finalName = `${sourceBase}${suffix}`;
|
|
485
|
+
// Drop the existing source collection to free up the name.
|
|
486
|
+
try {
|
|
487
|
+
await sourceDb.collection(finalName).drop();
|
|
488
|
+
}
|
|
489
|
+
catch { } // collection may not exist yet
|
|
490
|
+
await targetDb.collection(col.name).rename(finalName);
|
|
491
|
+
}
|
|
492
|
+
const finalConnector = new MongoDbEntityStorageConnector({
|
|
493
|
+
entitySchema: targetConnector._entitySchemaName,
|
|
494
|
+
config: this._config,
|
|
495
|
+
partitionContextIds: this._partitionContextIds
|
|
496
|
+
});
|
|
497
|
+
if (await finalConnector.bootstrap(loggingComponentType)) {
|
|
498
|
+
await targetConnector.stop?.();
|
|
499
|
+
return finalConnector;
|
|
500
|
+
}
|
|
501
|
+
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "finalizeMigrationFailedBootstrap", undefined);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Cleanup a failed or aborted migration by dropping the temporary migration collection.
|
|
505
|
+
* @param targetConnector The target connector to cleanup.
|
|
506
|
+
* @param options The options to control how the migration is cleaned up.
|
|
507
|
+
* @param loggingComponentType The optional component type to use for logging.
|
|
508
|
+
* @returns A promise that resolves when the cleanup is complete.
|
|
509
|
+
*/
|
|
510
|
+
async cleanupMigration(targetConnector, options, loggingComponentType) {
|
|
511
|
+
await targetConnector?.teardown?.(loggingComponentType);
|
|
512
|
+
}
|
|
305
513
|
/**
|
|
306
514
|
* Create a new DB connection configuration.
|
|
307
515
|
* @returns The MongoDb connection configuration.
|
|
@@ -316,13 +524,44 @@ export class MongoDbEntityStorageConnector {
|
|
|
316
524
|
return `mongodb://${host}${portPart}/${database}`;
|
|
317
525
|
}
|
|
318
526
|
/**
|
|
319
|
-
* Return a Mongo DB collection.
|
|
527
|
+
* Return a Mongo DB collection for the current partition context.
|
|
320
528
|
* @returns The MongoDb collection.
|
|
321
529
|
* @internal
|
|
322
530
|
*/
|
|
323
531
|
async getCollection() {
|
|
324
|
-
const
|
|
325
|
-
return this._client.db(database).collection(
|
|
532
|
+
const collectionName = await this.resolveCollectionName(this._config.collection);
|
|
533
|
+
return this._client.db(this._config.database).collection(collectionName);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Resolve the collection name for a base name, appending the partition key when applicable.
|
|
537
|
+
* @param base The base collection name.
|
|
538
|
+
* @returns The resolved collection name.
|
|
539
|
+
* @internal
|
|
540
|
+
*/
|
|
541
|
+
async resolveCollectionName(base) {
|
|
542
|
+
if (!Is.arrayValue(this._partitionContextIds)) {
|
|
543
|
+
return base;
|
|
544
|
+
}
|
|
545
|
+
const contextIds = await ContextIdStore.getContextIds();
|
|
546
|
+
const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
|
|
547
|
+
return Is.stringValue(partitionKey) ? `${base}_${partitionKey.replace(/[\0$]/g, "_")}` : base;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Build a MongoDB filter from optional conditions.
|
|
551
|
+
* @param conditions The optional entity conditions to include.
|
|
552
|
+
* @returns The MongoDB filter object.
|
|
553
|
+
* @internal
|
|
554
|
+
*/
|
|
555
|
+
buildFilter(conditions) {
|
|
556
|
+
const filter = {};
|
|
557
|
+
if (!Is.empty(conditions)) {
|
|
558
|
+
const finalConditions = {
|
|
559
|
+
conditions: [conditions],
|
|
560
|
+
logicalOperator: LogicalOperator.And
|
|
561
|
+
};
|
|
562
|
+
this.buildQueryParameters("", finalConditions, filter);
|
|
563
|
+
}
|
|
564
|
+
return filter;
|
|
326
565
|
}
|
|
327
566
|
/**
|
|
328
567
|
* Create an MongoDB filter query.
|
|
@@ -352,8 +591,17 @@ export class MongoDbEntityStorageConnector {
|
|
|
352
591
|
}
|
|
353
592
|
}
|
|
354
593
|
else {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
594
|
+
const propertyPath = String(condition.property);
|
|
595
|
+
const prop = objectPath ? `${objectPath}.${propertyPath}` : propertyPath;
|
|
596
|
+
const propertyParts = propertyPath.split(".");
|
|
597
|
+
const schemaLookupName = propertyParts.length > 1 ? propertyParts[0] : propertyPath;
|
|
598
|
+
const propertySchema = this._entitySchema.properties?.find(p => p.property === schemaLookupName);
|
|
599
|
+
// For dot-notation paths the leaf field is always a string value; using the root
|
|
600
|
+
// type directly would send Includes into $elemMatch which does not work for nested
|
|
601
|
+
// string fields. Keeping String here causes mapComparisonOperator to emit $regex,
|
|
602
|
+
// which MongoDB handles correctly for both nested object and array traversal.
|
|
603
|
+
const propertyType = propertyParts.length > 1 ? EntitySchemaPropertyType.String : propertySchema?.type;
|
|
604
|
+
const comparison = this.mapComparisonOperator(condition.comparison, condition.value, propertyType);
|
|
357
605
|
filter[prop] = comparison;
|
|
358
606
|
}
|
|
359
607
|
}
|
|
@@ -361,10 +609,12 @@ export class MongoDbEntityStorageConnector {
|
|
|
361
609
|
* Map the framework comparison operators to those in MongoDB.
|
|
362
610
|
* @param comparison The comparison operator.
|
|
363
611
|
* @param value The value to compare.
|
|
612
|
+
* @param type The type of the property from the schema.
|
|
364
613
|
* @returns The MongoDB comparison expression.
|
|
614
|
+
* @throws GeneralError if the comparison operator is not supported.
|
|
365
615
|
* @internal
|
|
366
616
|
*/
|
|
367
|
-
mapComparisonOperator(comparison, value) {
|
|
617
|
+
mapComparisonOperator(comparison, value, type) {
|
|
368
618
|
switch (comparison) {
|
|
369
619
|
case ComparisonOperator.Equals:
|
|
370
620
|
return value;
|
|
@@ -381,9 +631,28 @@ export class MongoDbEntityStorageConnector {
|
|
|
381
631
|
case ComparisonOperator.In:
|
|
382
632
|
return { $in: Array.isArray(value) ? value : [value] };
|
|
383
633
|
case ComparisonOperator.Includes:
|
|
634
|
+
// For string fields, use regex for substring matching
|
|
635
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
636
|
+
// Escape special regex characters in the value
|
|
637
|
+
const escapedValue = String(value).replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
638
|
+
return { $regex: escapedValue };
|
|
639
|
+
}
|
|
640
|
+
// For array and object fields, use $elemMatch
|
|
641
|
+
if (type === EntitySchemaPropertyType.Array || type === EntitySchemaPropertyType.Object) {
|
|
642
|
+
return { $elemMatch: { $eq: value } };
|
|
643
|
+
}
|
|
644
|
+
// Fallback to $elemMatch for backwards compatibility
|
|
384
645
|
return { $elemMatch: { $eq: value } };
|
|
385
646
|
case ComparisonOperator.NotIncludes:
|
|
386
|
-
|
|
647
|
+
// For string fields, use negated regex
|
|
648
|
+
if (type === EntitySchemaPropertyType.String) {
|
|
649
|
+
const escapedValue = String(value).replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
|
|
650
|
+
return { $not: { $regex: escapedValue } };
|
|
651
|
+
}
|
|
652
|
+
// For array/object fields: $ne on an array field matches documents where
|
|
653
|
+
// none of the array elements equal the value (MongoDB element-wise semantics).
|
|
654
|
+
// $elemMatch: { $ne: value } is wrong — it matches if *any* element ≠ value.
|
|
655
|
+
return { $ne: value };
|
|
387
656
|
default:
|
|
388
657
|
throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "unsupportedComparisonOperator", { comparison });
|
|
389
658
|
}
|