@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.
- package/CHANGELOG.md +20 -0
- package/README.md +141 -51
- package/package.json +1 -43
- package/src/array.js +201 -0
- package/src/database.js +110 -0
- package/src/index.js +20 -918
- package/src/object.js +509 -0
- package/src/utilities.js +296 -0
package/src/utilities.js
ADDED
|
@@ -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
|
+
};
|