@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 +7 -12
- package/dist/es/index.js +6 -0
- package/dist/es/index.js.map +1 -0
- package/dist/es/models/IMongoDbEntityStorageConnectorConfig.js +4 -0
- package/dist/es/models/IMongoDbEntityStorageConnectorConfig.js.map +1 -0
- package/dist/es/models/IMongoDbEntityStorageConnectorConstructorOptions.js +2 -0
- package/dist/es/models/IMongoDbEntityStorageConnectorConstructorOptions.js.map +1 -0
- package/dist/es/mongoDbEntityStorageConnector.js +583 -0
- package/dist/es/mongoDbEntityStorageConnector.js.map +1 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/models/IMongoDbEntityStorageConnectorConstructorOptions.d.ts +5 -1
- package/dist/types/mongoDbEntityStorageConnector.d.ts +48 -8
- package/docs/changelog.md +220 -31
- package/docs/examples.md +94 -1
- package/docs/reference/classes/MongoDbEntityStorageConnector.md +180 -22
- package/docs/reference/interfaces/IMongoDbEntityStorageConnectorConfig.md +9 -9
- package/docs/reference/interfaces/IMongoDbEntityStorageConnectorConstructorOptions.md +12 -4
- package/locales/en.json +16 -6
- package/package.json +15 -12
- package/dist/cjs/index.cjs +0 -335
- package/dist/esm/index.mjs +0 -333
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
|
package/dist/es/index.js
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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
|