@twin.org/entity-storage-connector-mongodb 0.0.2-next.9 → 0.0.3-next.10

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 CHANGED
@@ -1,6 +1,6 @@
1
- # TWIN Entity Storage Connector MongoDb
1
+ # Entity Storage Connector MongoDB
2
2
 
3
- Entity Storage connector implementation using MongoDb storage.
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
- ## Testing
11
+ ## Docker
12
12
 
13
- The tests developed are functional tests and need an instance of MongoDb up and running. To run MongoDb locally:
13
+ To perform testing of this component it may be necessary to launch a local instance to communicate with.
14
14
 
15
- ```sh
16
- docker run -p 27500:27017 --name twin-entity-storage-mongodb --hostname mongo -d mongo
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
@@ -0,0 +1,6 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ export * from "./mongoDbEntityStorageConnector.js";
4
+ export * from "./models/IMongoDbEntityStorageConnectorConfig.js";
5
+ export * from "./models/IMongoDbEntityStorageConnectorConstructorOptions.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC;AACvC,cAAc,oCAAoC,CAAC;AACnD,cAAc,kDAAkD,CAAC;AACjE,cAAc,8DAA8D,CAAC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\nexport * from \"./mongoDbEntityStorageConnector.js\";\nexport * from \"./models/IMongoDbEntityStorageConnectorConfig.js\";\nexport * from \"./models/IMongoDbEntityStorageConnectorConstructorOptions.js\";\n"]}
@@ -0,0 +1,4 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ export {};
4
+ //# sourceMappingURL=IMongoDbEntityStorageConnectorConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IMongoDbEntityStorageConnectorConfig.js","sourceRoot":"","sources":["../../../src/models/IMongoDbEntityStorageConnectorConfig.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,uCAAuC","sourcesContent":["// Copyright 2024 IOTA Stiftung.\n// SPDX-License-Identifier: Apache-2.0.\n\n/**\n * Configuration for the MongoDb Entity Storage Connector.\n */\nexport interface IMongoDbEntityStorageConnectorConfig {\n\t/**\n\t * The host for the MongoDb instance.\n\t */\n\thost: string;\n\n\t/**\n\t * The port for the MongoDb instance.\n\t */\n\tport?: number;\n\n\t/**\n\t * The user for the MongoDb instance.\n\t */\n\tuser?: string;\n\n\t/**\n\t * The password for the MongoDb instance.\n\t */\n\tpassword?: string;\n\n\t/**\n\t * The name of the database to be used.\n\t */\n\tdatabase: string;\n\n\t/**\n\t * The name of the collection to be used.\n\t */\n\tcollection: string;\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=IMongoDbEntityStorageConnectorConstructorOptions.js.map
@@ -0,0 +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 * @default logging\n\t */\n\tloggingComponentType?: string;\n\n\t/**\n\t * The configuration for the connector.\n\t */\n\tconfig: IMongoDbEntityStorageConnectorConfig;\n}\n"]}
@@ -0,0 +1,583 @@
1
+ // Copyright 2024 IOTA Stiftung.
2
+ // SPDX-License-Identifier: Apache-2.0.
3
+ import { ContextIdHelper, ContextIdStore } from "@twin.org/context";
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 { MongoClient } from "mongodb";
7
+ /**
8
+ * Class for performing entity storage operations using MongoDb.
9
+ */
10
+ export class MongoDbEntityStorageConnector {
11
+ /**
12
+ * Runtime name for the class.
13
+ */
14
+ static CLASS_NAME = "MongoDbEntityStorageConnector";
15
+ /**
16
+ * Limit the number of entities when finding.
17
+ * @internal
18
+ */
19
+ static _DEFAULT_LIMIT = 40;
20
+ /**
21
+ * Partition id field name.
22
+ * @internal
23
+ */
24
+ static _PARTITION_KEY = "partitionId";
25
+ /**
26
+ * The schema for the entity.
27
+ * @internal
28
+ */
29
+ _entitySchema;
30
+ /**
31
+ * The keys to use from the context ids to create partitions.
32
+ * @internal
33
+ */
34
+ _partitionContextIds;
35
+ /**
36
+ * The configuration for the connector.
37
+ * @internal
38
+ */
39
+ _config;
40
+ /**
41
+ * The MongoDb client.
42
+ * @internal
43
+ */
44
+ _client;
45
+ /**
46
+ * Create a new instance of MongoDbEntityStorageConnector.
47
+ * @param options The options for the connector.
48
+ */
49
+ constructor(options) {
50
+ Guards.object(MongoDbEntityStorageConnector.CLASS_NAME, "options", options);
51
+ Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.entitySchema", options.entitySchema);
52
+ Guards.object(MongoDbEntityStorageConnector.CLASS_NAME, "options.config", options.config);
53
+ Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.host", options.config.host);
54
+ Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.database", options.config.database);
55
+ Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "options.config.collection", options.config.collection);
56
+ this._entitySchema = EntitySchemaFactory.get(options.entitySchema);
57
+ this._partitionContextIds = options.partitionContextIds;
58
+ this._config = options.config;
59
+ this._client = new MongoClient(this.createConnectionConfig());
60
+ }
61
+ /**
62
+ * Initialize the MongoDb environment.
63
+ * @param nodeLoggingComponentType Optional type of the logging component.
64
+ * @returns A promise that resolves to a boolean indicating success.
65
+ */
66
+ async bootstrap(nodeLoggingComponentType) {
67
+ const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
68
+ try {
69
+ await this._client.connect();
70
+ await nodeLogging?.log({
71
+ level: "info",
72
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
73
+ ts: Date.now(),
74
+ message: "databaseCreating",
75
+ data: {
76
+ databaseName: this._config.database
77
+ }
78
+ });
79
+ // Create the database if it does not exist
80
+ this._client.db(this._config.database);
81
+ await nodeLogging?.log({
82
+ level: "info",
83
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
84
+ ts: Date.now(),
85
+ message: "databaseExists",
86
+ data: {
87
+ databaseName: this._config.database
88
+ }
89
+ });
90
+ await this.getCollection();
91
+ await nodeLogging?.log({
92
+ level: "info",
93
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
94
+ ts: Date.now(),
95
+ message: "collectionExists",
96
+ data: {
97
+ collectionName: this._config.collection
98
+ }
99
+ });
100
+ }
101
+ catch (error) {
102
+ await nodeLogging?.log({
103
+ level: "error",
104
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
105
+ ts: Date.now(),
106
+ message: "databaseCreateFailed",
107
+ error: BaseError.fromError(error),
108
+ data: {
109
+ databaseName: this._config.database
110
+ }
111
+ });
112
+ return false;
113
+ }
114
+ return true;
115
+ }
116
+ /**
117
+ * The component needs to be stopped when the node is closed.
118
+ * @param nodeLoggingComponentType The node logging component type.
119
+ * @returns Nothing.
120
+ */
121
+ async stop(nodeLoggingComponentType) {
122
+ await this._client.close();
123
+ }
124
+ /**
125
+ * Returns the class name of the component.
126
+ * @returns The class name of the component.
127
+ */
128
+ className() {
129
+ return MongoDbEntityStorageConnector.CLASS_NAME;
130
+ }
131
+ /**
132
+ * Returns the health status of the component.
133
+ * @returns The health status of the component.
134
+ */
135
+ async health() {
136
+ try {
137
+ await this._client
138
+ .db(this._config.database)
139
+ .collection(this._config.collection)
140
+ .estimatedDocumentCount();
141
+ return [
142
+ {
143
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
144
+ status: HealthStatus.Ok,
145
+ description: "healthDescription"
146
+ }
147
+ ];
148
+ }
149
+ catch {
150
+ return [
151
+ {
152
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
153
+ status: HealthStatus.Error,
154
+ description: "healthDescription",
155
+ message: "connectionFailed"
156
+ }
157
+ ];
158
+ }
159
+ }
160
+ /**
161
+ * Get the schema for the entities.
162
+ * @returns The schema for the entities.
163
+ */
164
+ getSchema() {
165
+ return this._entitySchema;
166
+ }
167
+ /**
168
+ * Get an entity from MongoDb.
169
+ * @param id The id of the entity to get, or the index value if secondaryIndex is set.
170
+ * @param secondaryIndex Get the item using a secondary index.
171
+ * @param conditions The optional conditions to match for the entities.
172
+ * @returns The object if it can be found or undefined.
173
+ */
174
+ async get(id, secondaryIndex, conditions) {
175
+ Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "id", id);
176
+ const contextIds = await ContextIdStore.getContextIds();
177
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
178
+ try {
179
+ const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
180
+ const query = Is.empty(secondaryIndex)
181
+ ? { [primaryKey.property]: id }
182
+ : { [secondaryIndex]: id };
183
+ if (Is.stringValue(partitionKey)) {
184
+ query[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
185
+ }
186
+ if (conditions) {
187
+ for (const condition of conditions) {
188
+ query[condition.property] = condition.value;
189
+ }
190
+ }
191
+ const collection = await this.getCollection();
192
+ const result = await collection.findOne(query);
193
+ ObjectHelper.propertyDelete(result, "_id");
194
+ ObjectHelper.propertyDelete(result, MongoDbEntityStorageConnector._PARTITION_KEY);
195
+ return result;
196
+ }
197
+ catch (err) {
198
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "getFailed", {
199
+ id
200
+ }, err);
201
+ }
202
+ }
203
+ /**
204
+ * Set an entity.
205
+ * @param entity The entity to set.
206
+ * @param conditions The optional conditions to match for the entities.
207
+ * @returns The id of the entity.
208
+ */
209
+ async set(entity, conditions) {
210
+ Guards.object(MongoDbEntityStorageConnector.CLASS_NAME, "entity", entity);
211
+ const contextIds = await ContextIdStore.getContextIds();
212
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
213
+ EntitySchemaHelper.validateEntity(entity, this.getSchema());
214
+ const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
215
+ const id = entity[primaryKey.property];
216
+ try {
217
+ const filter = { [primaryKey.property]: id };
218
+ const finalEntity = ObjectHelper.clone(entity);
219
+ if (Is.stringValue(partitionKey)) {
220
+ filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
221
+ ObjectHelper.propertySet(finalEntity, MongoDbEntityStorageConnector._PARTITION_KEY, partitionKey);
222
+ }
223
+ if (Is.arrayValue(conditions)) {
224
+ for (const condition of conditions) {
225
+ filter[condition.property] = condition.value;
226
+ }
227
+ }
228
+ const collection = await this.getCollection();
229
+ await collection.findOneAndUpdate(filter, { $set: ObjectHelper.removeEmptyProperties(finalEntity) }, { upsert: true });
230
+ }
231
+ catch (err) {
232
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "setFailed", {
233
+ id
234
+ }, err);
235
+ }
236
+ }
237
+ /**
238
+ * Set multiple entities in a batch.
239
+ * @param entities The entities to set.
240
+ * @returns Nothing.
241
+ */
242
+ async setBatch(entities) {
243
+ Guards.arrayValue(MongoDbEntityStorageConnector.CLASS_NAME, "entities", entities);
244
+ const contextIds = await ContextIdStore.getContextIds();
245
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
246
+ const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
247
+ for (const entity of entities) {
248
+ EntitySchemaHelper.validateEntity(entity, this.getSchema());
249
+ }
250
+ try {
251
+ const collection = await this.getCollection();
252
+ await collection.bulkWrite(entities.map(entity => {
253
+ const finalEntity = ObjectHelper.clone(entity);
254
+ const filter = {
255
+ [primaryKey.property]: entity[primaryKey.property]
256
+ };
257
+ if (Is.stringValue(partitionKey)) {
258
+ filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
259
+ ObjectHelper.propertySet(finalEntity, MongoDbEntityStorageConnector._PARTITION_KEY, partitionKey);
260
+ }
261
+ return {
262
+ updateOne: {
263
+ filter,
264
+ update: {
265
+ $set: ObjectHelper.removeEmptyProperties(finalEntity)
266
+ },
267
+ upsert: true
268
+ }
269
+ };
270
+ }));
271
+ }
272
+ catch (err) {
273
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "setBatchFailed", undefined, err);
274
+ }
275
+ }
276
+ /**
277
+ * Empty the entity storage.
278
+ * @returns Nothing.
279
+ */
280
+ async empty() {
281
+ try {
282
+ const contextIds = await ContextIdStore.getContextIds();
283
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
284
+ const filter = {};
285
+ if (Is.stringValue(partitionKey)) {
286
+ filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
287
+ }
288
+ const collection = await this.getCollection();
289
+ await collection.deleteMany(filter);
290
+ }
291
+ catch (err) {
292
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "emptyFailed", undefined, err);
293
+ }
294
+ }
295
+ /**
296
+ * Remove the entity.
297
+ * @param id The id of the entity to remove.
298
+ * @param conditions The optional conditions to match for the entities.
299
+ * @returns Nothing.
300
+ */
301
+ async remove(id, conditions) {
302
+ Guards.stringValue(MongoDbEntityStorageConnector.CLASS_NAME, "id", id);
303
+ const contextIds = await ContextIdStore.getContextIds();
304
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
305
+ try {
306
+ const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
307
+ const query = { [primaryKey.property]: id };
308
+ if (Is.stringValue(partitionKey)) {
309
+ query[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
310
+ }
311
+ if (conditions) {
312
+ for (const condition of conditions) {
313
+ query[condition.property] = condition.value;
314
+ }
315
+ }
316
+ const collection = await this.getCollection();
317
+ await collection.deleteOne(query);
318
+ }
319
+ catch (err) {
320
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "removeFailed", { id }, err);
321
+ }
322
+ }
323
+ /**
324
+ * Remove multiple entities by id.
325
+ * @param ids The ids of the entities to remove.
326
+ * @returns Nothing.
327
+ */
328
+ async removeBatch(ids) {
329
+ Guards.arrayValue(MongoDbEntityStorageConnector.CLASS_NAME, "ids", ids);
330
+ const contextIds = await ContextIdStore.getContextIds();
331
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
332
+ try {
333
+ const primaryKey = EntitySchemaHelper.getPrimaryKey(this.getSchema());
334
+ const filter = {
335
+ [primaryKey.property]: { $in: ids }
336
+ };
337
+ if (Is.stringValue(partitionKey)) {
338
+ filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
339
+ }
340
+ const collection = await this.getCollection();
341
+ await collection.deleteMany(filter);
342
+ }
343
+ catch (err) {
344
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "removeBatchFailed", undefined, err);
345
+ }
346
+ }
347
+ /**
348
+ * Teardown the entity storage by dropping the collection.
349
+ * @param nodeLoggingComponentType The node logging component type.
350
+ * @returns True if the teardown process was successful.
351
+ */
352
+ async teardown(nodeLoggingComponentType) {
353
+ const nodeLogging = ComponentFactory.getIfExists(nodeLoggingComponentType);
354
+ await nodeLogging?.log({
355
+ level: "info",
356
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
357
+ ts: Date.now(),
358
+ message: "collectionDropping",
359
+ data: { collection: this._config.collection }
360
+ });
361
+ try {
362
+ const collection = await this.getCollection();
363
+ await collection.drop();
364
+ await nodeLogging?.log({
365
+ level: "info",
366
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
367
+ ts: Date.now(),
368
+ message: "collectionDropped",
369
+ data: { collection: this._config.collection }
370
+ });
371
+ return true;
372
+ }
373
+ catch (err) {
374
+ await nodeLogging?.log({
375
+ level: "error",
376
+ source: MongoDbEntityStorageConnector.CLASS_NAME,
377
+ ts: Date.now(),
378
+ message: "teardownFailed",
379
+ error: BaseError.fromError(err)
380
+ });
381
+ return false;
382
+ }
383
+ }
384
+ /**
385
+ * Find all the entities which match the conditions.
386
+ * @param conditions The conditions to match for the entities.
387
+ * @param sortProperties The optional sort order.
388
+ * @param properties The optional properties to return, defaults to all.
389
+ * @param cursor The cursor to request the next chunk of entities.
390
+ * @param limit The suggested number of entities to return in each chunk, in some scenarios can return a different amount.
391
+ * @returns All the entities for the storage matching the conditions,
392
+ * and a cursor which can be used to request more entities.
393
+ */
394
+ async query(conditions, sortProperties, properties, cursor, limit) {
395
+ const contextIds = await ContextIdStore.getContextIds();
396
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
397
+ const returnSize = limit ?? MongoDbEntityStorageConnector._DEFAULT_LIMIT;
398
+ const finalConditions = {
399
+ conditions: [],
400
+ logicalOperator: LogicalOperator.And
401
+ };
402
+ if (Is.stringValue(partitionKey)) {
403
+ finalConditions.conditions.push({
404
+ property: MongoDbEntityStorageConnector._PARTITION_KEY,
405
+ comparison: ComparisonOperator.Equals,
406
+ value: partitionKey
407
+ });
408
+ }
409
+ if (!Is.empty(conditions)) {
410
+ finalConditions.conditions.push(conditions);
411
+ }
412
+ const filter = {};
413
+ if (finalConditions.conditions.length > 0) {
414
+ this.buildQueryParameters("", finalConditions, filter);
415
+ }
416
+ const sort = new Map();
417
+ if (Array.isArray(sortProperties)) {
418
+ for (const sortProperty of sortProperties) {
419
+ sort.set(sortProperty.property, sortProperty.sortDirection);
420
+ }
421
+ }
422
+ const projection = {};
423
+ if (properties) {
424
+ for (const property of properties) {
425
+ projection[property] = 1;
426
+ }
427
+ }
428
+ const cursorValue = cursor ? Number(cursor) : 0;
429
+ const collection = await this.getCollection();
430
+ const entitiesResult = await collection
431
+ // False positive, this is not an array find call
432
+ // eslint-disable-next-line unicorn/no-array-callback-reference
433
+ ?.find(filter, { projection })
434
+ .sort(sort)
435
+ .skip(cursorValue)
436
+ .limit(returnSize)
437
+ .toArray();
438
+ const entities = entitiesResult ?? [];
439
+ for (const entity of entities) {
440
+ ObjectHelper.propertyDelete(entity, "_id");
441
+ ObjectHelper.propertyDelete(entity, MongoDbEntityStorageConnector._PARTITION_KEY);
442
+ }
443
+ return {
444
+ entities,
445
+ cursor: entities?.length === returnSize ? String(cursorValue + returnSize) : undefined
446
+ };
447
+ }
448
+ /**
449
+ * Count all the entities which match the conditions.
450
+ * @returns The total count of entities in the storage.
451
+ */
452
+ async count() {
453
+ try {
454
+ const contextIds = await ContextIdStore.getContextIds();
455
+ const partitionKey = ContextIdHelper.combinedContextKey(contextIds, this._partitionContextIds);
456
+ const filter = {};
457
+ if (Is.stringValue(partitionKey)) {
458
+ filter[MongoDbEntityStorageConnector._PARTITION_KEY] = partitionKey;
459
+ }
460
+ return await this._client
461
+ .db(this._config.database)
462
+ .collection(this._config.collection)
463
+ .countDocuments(filter);
464
+ }
465
+ catch (err) {
466
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "countFailed", undefined, err);
467
+ }
468
+ }
469
+ /**
470
+ * Create a new DB connection configuration.
471
+ * @returns The MongoDb connection configuration.
472
+ * @internal
473
+ */
474
+ createConnectionConfig() {
475
+ const { host, port, user, password, database } = this._config;
476
+ const portPart = port ? `:${port}` : "";
477
+ if (user && password) {
478
+ return `mongodb://${user}:${password}@${host}${portPart}/${database}`;
479
+ }
480
+ return `mongodb://${host}${portPart}/${database}`;
481
+ }
482
+ /**
483
+ * Return a Mongo DB collection.
484
+ * @returns The MongoDb collection.
485
+ * @internal
486
+ */
487
+ async getCollection() {
488
+ const { database, collection } = this._config;
489
+ return this._client.db(database).collection(collection);
490
+ }
491
+ /**
492
+ * Create an MongoDB filter query.
493
+ * @param objectPath The path for the nested object.
494
+ * @param condition The conditions to create the query from.
495
+ * @param filter The filter query to use.
496
+ * @internal
497
+ */
498
+ buildQueryParameters(objectPath, condition, filter) {
499
+ if (!condition) {
500
+ return;
501
+ }
502
+ if ("conditions" in condition) {
503
+ const subConditions = condition.conditions.map(c => {
504
+ const subFilter = {};
505
+ this.buildQueryParameters(objectPath, c, subFilter);
506
+ return subFilter;
507
+ });
508
+ if (condition.logicalOperator === LogicalOperator.And) {
509
+ filter.$and = subConditions;
510
+ }
511
+ else if (condition.logicalOperator === LogicalOperator.Or) {
512
+ filter.$or = subConditions;
513
+ }
514
+ else {
515
+ Object.assign(filter, subConditions[0]);
516
+ }
517
+ }
518
+ else {
519
+ const propertyPath = String(condition.property);
520
+ const prop = objectPath ? `${objectPath}.${propertyPath}` : propertyPath;
521
+ const propertyParts = propertyPath.split(".");
522
+ const schemaLookupName = propertyParts.length > 1 ? propertyParts[0] : propertyPath;
523
+ const propertySchema = this._entitySchema.properties?.find(p => p.property === schemaLookupName);
524
+ // For dot-notation paths the leaf field is always a string value; using the root
525
+ // type directly would send Includes into $elemMatch which does not work for nested
526
+ // string fields. Keeping String here causes mapComparisonOperator to emit $regex,
527
+ // which MongoDB handles correctly for both nested object and array traversal.
528
+ const propertyType = propertyParts.length > 1 ? EntitySchemaPropertyType.String : propertySchema?.type;
529
+ const comparison = this.mapComparisonOperator(condition.comparison, condition.value, propertyType);
530
+ filter[prop] = comparison;
531
+ }
532
+ }
533
+ /**
534
+ * Map the framework comparison operators to those in MongoDB.
535
+ * @param comparison The comparison operator.
536
+ * @param value The value to compare.
537
+ * @param type The type of the property from the schema.
538
+ * @returns The MongoDB comparison expression.
539
+ * @internal
540
+ */
541
+ mapComparisonOperator(comparison, value, type) {
542
+ switch (comparison) {
543
+ case ComparisonOperator.Equals:
544
+ return value;
545
+ case ComparisonOperator.NotEquals:
546
+ return { $ne: value };
547
+ case ComparisonOperator.GreaterThan:
548
+ return { $gt: value };
549
+ case ComparisonOperator.LessThan:
550
+ return { $lt: value };
551
+ case ComparisonOperator.GreaterThanOrEqual:
552
+ return { $gte: value };
553
+ case ComparisonOperator.LessThanOrEqual:
554
+ return { $lte: value };
555
+ case ComparisonOperator.In:
556
+ return { $in: Array.isArray(value) ? value : [value] };
557
+ case ComparisonOperator.Includes:
558
+ // For string fields, use regex for substring matching
559
+ if (type === EntitySchemaPropertyType.String) {
560
+ // Escape special regex characters in the value
561
+ const escapedValue = String(value).replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
562
+ return { $regex: escapedValue };
563
+ }
564
+ // For array and object fields, use $elemMatch
565
+ if (type === EntitySchemaPropertyType.Array || type === EntitySchemaPropertyType.Object) {
566
+ return { $elemMatch: { $eq: value } };
567
+ }
568
+ // Fallback to $elemMatch for backwards compatibility
569
+ return { $elemMatch: { $eq: value } };
570
+ case ComparisonOperator.NotIncludes:
571
+ // For string fields, use negated regex
572
+ if (type === EntitySchemaPropertyType.String) {
573
+ const escapedValue = String(value).replace(/[$()*+.?[\\\]^{|}]/g, "\\$&");
574
+ return { $not: { $regex: escapedValue } };
575
+ }
576
+ // For arrays, use $elemMatch with $ne
577
+ return { $elemMatch: { $ne: value } };
578
+ default:
579
+ throw new GeneralError(MongoDbEntityStorageConnector.CLASS_NAME, "unsupportedComparisonOperator", { comparison });
580
+ }
581
+ }
582
+ }
583
+ //# sourceMappingURL=mongoDbEntityStorageConnector.js.map