@cocreate/mongodb 1.24.1 → 1.25.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.
@@ -0,0 +1,296 @@
1
+ /********************************************************************************
2
+ * Copyright (C) 2023 CoCreate and Contributors.
3
+ *
4
+ * This program is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU Affero General Public License as published
6
+ * by the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU Affero General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU Affero General Public License
15
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
+ ********************************************************************************/
17
+
18
+ import { MongoClient, ObjectId } from "mongodb";
19
+
20
+ const organizations = {};
21
+
22
+ /**
23
+ * Dynamically resolves or caches a long-lived MongoDB Client instance using modern driver syntax.
24
+ * @param {Object} data - Context parameters containing credentials and storage URL
25
+ * @returns {Promise<MongoClient>} Resolved MongoDB Client connection
26
+ */
27
+ async function dbClient(data) {
28
+ if (data.storageUrl) {
29
+ if (!organizations[data.organization_id]) {
30
+ organizations[data.organization_id] = {};
31
+ }
32
+
33
+ try {
34
+ // If connection doesn't exist, create it and store the promise to prevent concurrent duplicate connections
35
+ if (!organizations[data.organization_id][data.storageUrl]) {
36
+
37
+ // Native MongoDB Node.js Driver v6+ recommended constructor instantiation
38
+ const client = new MongoClient(data.storageUrl, {
39
+ maxPoolSize: 100,
40
+ minPoolSize: 0, // Connections scale all the way down to 0 when idle
41
+ maxIdleTimeMS: 30000, // Automatically disconnects individual sockets after 30 seconds of inactivity
42
+ connectTimeoutMS: 5000,
43
+ socketTimeoutMS: 30000
44
+ });
45
+
46
+ // Cache the connection promise so simultaneous requests share the same initialization lifecycle
47
+ organizations[data.organization_id][data.storageUrl] = client.connect();
48
+ }
49
+
50
+ // Await connection resolution safely
51
+ organizations[data.organization_id][data.storageUrl] = await organizations[data.organization_id][data.storageUrl];
52
+ return organizations[data.organization_id][data.storageUrl];
53
+ } catch (error) {
54
+ console.error(
55
+ `${data.organization_id}: storageUrl ${data.storageUrl} failed to connect to mongodb`
56
+ );
57
+ errorHandler(data, error);
58
+ return { status: false };
59
+ }
60
+ }
61
+
62
+ errorHandler(data, "missing StorageUrl");
63
+ return;
64
+ }
65
+
66
+ /**
67
+ * Global Event Listener: orgDeleted
68
+ * Triggers clean teardowns of cached database connection pools as users disconnect.
69
+ *
70
+ * DESIGN DECISION: Cooperative Connection Drain
71
+ * Instead of closing active client connections instantly, we register a 5-second
72
+ * delay timeout. This allows other events and modules (such as CoCreateUsage, telemetry,
73
+ * and system caches) to execute final asynchronous transactions and flush outstanding logs
74
+ * safely before the underlying sockets are severed.
75
+ */
76
+ process.on('orgDeleted', (organization_id) => {
77
+ const orgPools = organizations[organization_id];
78
+
79
+ if (orgPools) {
80
+ console.log(`[DB Utilities] Cleanup Scheduled: Initiating 5-second grace period for Org: ${organization_id}`);
81
+
82
+ setTimeout(async () => {
83
+ // Re-verify that the connection pool reference wasn't already deleted or modified
84
+ const currentPools = organizations[organization_id];
85
+ if (!currentPools) return;
86
+
87
+ console.log(`[DB Utilities] Cleanup Executing: Closing connection pools for inactive Org: ${organization_id}`);
88
+
89
+ // Extract connection clients directly using Object.values
90
+ const clientsList = Object.values(currentPools);
91
+
92
+ for (const client of clientsList) {
93
+ try {
94
+ if (client && typeof client.close === 'function') {
95
+ // Close socket pools gracefully to free db sockets
96
+ await client.close();
97
+ }
98
+ } catch (err) {
99
+ console.error(`[DB Utilities] Error closing connection for Org ${organization_id}:`, err);
100
+ }
101
+ }
102
+
103
+ // Remove reference to enable complete garbage collection (V8 memory release)
104
+ delete organizations[organization_id];
105
+ console.log(`[DB Utilities] Memory Cleared: Reference for Org ${organization_id} removed from runtime registry.`);
106
+ }, 5000); // 5-second grace interval
107
+ }
108
+ });
109
+
110
+ async function createFilter(data, arrayObj) {
111
+ let query = {},
112
+ sort = {},
113
+ index = 0,
114
+ limit = 0,
115
+ count;
116
+
117
+ if (data.$filter) {
118
+ function convertIfDate(value) {
119
+ if (
120
+ typeof value === "string" &&
121
+ value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/)
122
+ ) {
123
+ return new Date(value);
124
+ }
125
+ return value;
126
+ }
127
+
128
+ // Recursive function to merge keys into dot notation
129
+ function mergeToDotNotation(obj, parentKey = "", result = {}) {
130
+ for (let key in obj) {
131
+ const isOperator = key.startsWith("$");
132
+ const currentKey = parentKey ? `${parentKey}.${key}` : key;
133
+
134
+ if (
135
+ obj[key] &&
136
+ typeof obj[key] === "object" &&
137
+ !Array.isArray(obj[key])
138
+ ) {
139
+ if (isOperator) {
140
+ // Ensure operators are grouped under their parent key
141
+ result[parentKey] = result[parentKey] || {};
142
+ result[parentKey][key] = obj[key];
143
+ } else {
144
+ // Recurse into nested objects
145
+ mergeToDotNotation(obj[key], currentKey, result);
146
+ }
147
+ } else {
148
+ // Assign to result, merging into dot notation if applicable
149
+ if (isOperator) {
150
+ result[parentKey] = result[parentKey] || {};
151
+ result[parentKey][key] = obj[key];
152
+ } else {
153
+ result[currentKey] = obj[key];
154
+ }
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+
160
+ if (data.$filter.query) {
161
+ for (let key in data.$filter.query) {
162
+ if (Array.isArray(data.$filter.query[key])) {
163
+ // Handle $or operator with an array of conditions
164
+ query[key] = data.$filter.query[key].map((condition) => {
165
+ let newCondition = {};
166
+ mergeToDotNotation(condition, "", newCondition);
167
+ return newCondition;
168
+ });
169
+ } else if (
170
+ typeof data.$filter.query[key] === "object" &&
171
+ data.$filter.query[key] !== null
172
+ ) {
173
+ // Handle general object conditions
174
+ mergeToDotNotation(data.$filter.query[key], key, query);
175
+ } else {
176
+ // Handle direct values
177
+ query[key] =
178
+ key === "_id"
179
+ ? new ObjectId(data.$filter.query[key])
180
+ : convertIfDate(data.$filter.query[key]);
181
+ }
182
+ }
183
+ }
184
+
185
+ if (data.$filter.sort) {
186
+ for (let i = 0; i < data.$filter.sort.length; i++) {
187
+ let direction = data.$filter.sort[i].direction;
188
+ direction = direction === "desc" || direction === -1 ? -1 : 1;
189
+
190
+ sort[data.$filter.sort[i].key] = direction;
191
+ }
192
+ }
193
+
194
+ if (arrayObj) {
195
+ count = await arrayObj.estimatedDocumentCount();
196
+ data.$filter.count = count;
197
+ }
198
+
199
+ if (data.$filter.index) index = data.$filter.index;
200
+ if (data.$filter.limit) limit = data.$filter.limit;
201
+ }
202
+
203
+ if (data["organization_id"])
204
+ query["organization_id"] = data["organization_id"];
205
+
206
+ return { query, sort, index, limit, count };
207
+ }
208
+
209
+ function parseRegExp(regExpString) {
210
+ let matches = regExpString.match(/\/(.*)\/(.*)/);
211
+ return {
212
+ pattern: matches[1],
213
+ options: matches[2]
214
+ };
215
+ }
216
+
217
+ function createData(data, array, type) {
218
+ if (
219
+ data[type] &&
220
+ data[type][0] &&
221
+ data[type][0].isFilter === "isEmptyObjectFilter"
222
+ ) {
223
+ data[type].shift();
224
+ data.isFilter = true;
225
+ }
226
+
227
+ let key = "_id";
228
+ if (type !== "object") key = "name";
229
+
230
+ // handle case where data[type] is not an array
231
+ if (!Array.isArray(data[type]))
232
+ console.log("data[type] is not an array", type);
233
+ else {
234
+ for (let i = 0; i < array.length; i++) {
235
+ const matchIndex = data[type].findIndex(
236
+ (item) => item[key] === array[i][key]
237
+ );
238
+ if (matchIndex !== -1) {
239
+ for (let $type of ["$storage", "$database", "$array"]) {
240
+ if (!data[type][matchIndex][$type])
241
+ data[type][matchIndex][$type] = [];
242
+ if (!Array.isArray(data[type][matchIndex][$type])) {
243
+ data[type][matchIndex][$type] = [
244
+ data[type][matchIndex][$type]
245
+ ];
246
+ if (
247
+ !data[type][matchIndex][$type].includes(
248
+ array[i][$type]
249
+ )
250
+ ) {
251
+ data[type][matchIndex][$type].push(array[i][$type]);
252
+ }
253
+ } else {
254
+ data[type][matchIndex][$type].push(array[i][$type]);
255
+ }
256
+ delete array[i][$type];
257
+ }
258
+
259
+ // compare dates and merge and updates to keep all synced and up to date
260
+ data[type][matchIndex] = {
261
+ ...data[type][matchIndex],
262
+ ...array[i]
263
+ };
264
+ } else data[type].push(array[i]);
265
+ }
266
+ }
267
+
268
+ return data;
269
+ }
270
+
271
+ function errorHandler(data, error, database, array) {
272
+ let errorMessage =
273
+ typeof error === "object" && error.message ? error.message : error;
274
+
275
+ let errorObject = {
276
+ message: errorMessage,
277
+ storage: "mongodb"
278
+ };
279
+
280
+ if (database) errorObject.database = database;
281
+ if (array) errorObject.array = array;
282
+
283
+ if (Array.isArray(data.error)) {
284
+ data.error.push(errorObject);
285
+ } else {
286
+ data.error = [errorObject];
287
+ }
288
+ }
289
+
290
+ export {
291
+ dbClient,
292
+ createFilter,
293
+ parseRegExp,
294
+ createData,
295
+ errorHandler
296
+ };