db-model-router 1.0.0

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,381 @@
1
+ const { MongoClient, ObjectId } = require("mongodb");
2
+ const { jsonSafeParse } = require("../commons/function");
3
+
4
+ let client = null;
5
+ let database = null;
6
+ const WHERE_INVALID = "Invalid filter object";
7
+
8
+ function connect(config) {
9
+ const username = config.username || config.user || "";
10
+ const password = config.password || "";
11
+ const host = config.host || "localhost";
12
+ const port = config.port || 27017;
13
+ const dbName = config.database || config.db || "test";
14
+
15
+ let uri;
16
+ if (config.uri || config.url) {
17
+ uri = config.uri || config.url;
18
+ } else if (username && password) {
19
+ uri = `mongodb://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`;
20
+ } else {
21
+ uri = `mongodb://${host}:${port}`;
22
+ }
23
+
24
+ client = new MongoClient(uri, config.options || {});
25
+ database = client.db(dbName);
26
+ return client;
27
+ }
28
+
29
+ async function query(collection, operation, ...args) {
30
+ const col = database.collection(collection);
31
+ return col[operation](...args);
32
+ }
33
+
34
+ function where(filter, safeDelete = null) {
35
+ if (filter !== null && filter !== "" && !Array.isArray(filter)) {
36
+ return null;
37
+ }
38
+ try {
39
+ if (
40
+ filter === null ||
41
+ filter === "" ||
42
+ filter.length === 0 ||
43
+ (Array.isArray(filter[0]) && filter[0].length === 0) ||
44
+ (Array.isArray(filter[0]) &&
45
+ Array.isArray(filter[0][0]) &&
46
+ filter[0][0].length === 0)
47
+ ) {
48
+ if (safeDelete === null) {
49
+ return { query: {}, value: [] };
50
+ } else {
51
+ filter = [[]];
52
+ }
53
+ }
54
+ } catch (err) {
55
+ return null;
56
+ }
57
+
58
+ if (safeDelete !== null) {
59
+ for (const filterItem of filter) {
60
+ filterItem.push([safeDelete, "=", 0]);
61
+ }
62
+ }
63
+
64
+ const valid_conditionals = [
65
+ "=",
66
+ "like",
67
+ "not like",
68
+ "in",
69
+ "not in",
70
+ "<",
71
+ ">",
72
+ "<=",
73
+ ">=",
74
+ "!=",
75
+ ];
76
+
77
+ let orGroups = [];
78
+
79
+ for (const group of filter) {
80
+ let andConditions = [];
81
+ for (const condition of group) {
82
+ if (!valid_conditionals.includes(condition[1])) {
83
+ return null;
84
+ }
85
+ if (
86
+ (condition[1] === "in" || condition[1] === "not in") &&
87
+ !Array.isArray(condition[2])
88
+ ) {
89
+ return null;
90
+ }
91
+
92
+ const field = condition[0];
93
+ const op = condition[1];
94
+ let val = condition[2];
95
+
96
+ // Convert _id string values to ObjectId when they look like valid ObjectIds
97
+ if (
98
+ field === "_id" &&
99
+ typeof val === "string" &&
100
+ /^[0-9a-fA-F]{24}$/.test(val)
101
+ ) {
102
+ try {
103
+ val = new ObjectId(val);
104
+ } catch (e) {
105
+ /* keep as string */
106
+ }
107
+ }
108
+
109
+ switch (op) {
110
+ case "=":
111
+ andConditions.push({ [field]: val });
112
+ break;
113
+ case "!=":
114
+ andConditions.push({ [field]: { $ne: val } });
115
+ break;
116
+ case "<":
117
+ andConditions.push({ [field]: { $lt: val } });
118
+ break;
119
+ case ">":
120
+ andConditions.push({ [field]: { $gt: val } });
121
+ break;
122
+ case "<=":
123
+ andConditions.push({ [field]: { $lte: val } });
124
+ break;
125
+ case ">=":
126
+ andConditions.push({ [field]: { $gte: val } });
127
+ break;
128
+ case "like":
129
+ andConditions.push({
130
+ [field]: { $regex: escapeRegex(val), $options: "i" },
131
+ });
132
+ break;
133
+ case "not like":
134
+ andConditions.push({
135
+ [field]: { $not: { $regex: escapeRegex(val), $options: "i" } },
136
+ });
137
+ break;
138
+ case "in":
139
+ andConditions.push({ [field]: { $in: val } });
140
+ break;
141
+ case "not in":
142
+ andConditions.push({ [field]: { $nin: val } });
143
+ break;
144
+ }
145
+ }
146
+
147
+ if (andConditions.length > 0) {
148
+ orGroups.push({ $and: andConditions });
149
+ }
150
+ }
151
+
152
+ let mongoQuery = {};
153
+ if (orGroups.length === 1) {
154
+ mongoQuery = orGroups[0];
155
+ } else if (orGroups.length > 1) {
156
+ mongoQuery = { $or: orGroups };
157
+ }
158
+
159
+ return { query: mongoQuery, value: [] };
160
+ }
161
+
162
+ async function get(table, filter = [], sort = [], safeDelete = null) {
163
+ const whereData = where(filter, safeDelete);
164
+ if (whereData == null) {
165
+ throw new Error(WHERE_INVALID);
166
+ }
167
+
168
+ const col = database.collection(table);
169
+ const mongoSort = buildSort(sort);
170
+ const rows = await col.find(whereData.query).sort(mongoSort).toArray();
171
+ const count = await col.countDocuments(whereData.query);
172
+ return { data: jsonSafeParse(rows), count };
173
+ }
174
+
175
+ async function list(
176
+ table,
177
+ filter = [],
178
+ sort = [],
179
+ safeDelete = null,
180
+ page = 0,
181
+ limit = 30,
182
+ ) {
183
+ const whereData = where(filter, safeDelete);
184
+ if (whereData == null) {
185
+ throw new Error(WHERE_INVALID);
186
+ }
187
+
188
+ const col = database.collection(table);
189
+ const mongoSort = buildSort(sort);
190
+ const rows = await col
191
+ .find(whereData.query)
192
+ .sort(mongoSort)
193
+ .skip(page * limit)
194
+ .limit(limit)
195
+ .toArray();
196
+ const count = await col.countDocuments(whereData.query);
197
+ return { data: jsonSafeParse(rows), count };
198
+ }
199
+
200
+ async function qcount(table, filter, safeDelete = null) {
201
+ const whereData = where(filter, safeDelete);
202
+ if (whereData == null) {
203
+ return 0;
204
+ }
205
+ try {
206
+ const col = database.collection(table);
207
+ return await col.countDocuments(whereData.query);
208
+ } catch (err) {
209
+ return 0;
210
+ }
211
+ }
212
+
213
+ async function remove(table, filter, safeDelete = null) {
214
+ const whereData = where(filter);
215
+ if (whereData == null) {
216
+ throw new Error(WHERE_INVALID);
217
+ }
218
+ if (Object.keys(whereData.query).length < 1 && whereData.value.length < 1) {
219
+ throw new Error("unable to remove as there are no filter attributes");
220
+ }
221
+
222
+ const col = database.collection(table);
223
+
224
+ if (safeDelete != null) {
225
+ const result = await col.updateMany(whereData.query, {
226
+ $set: { [safeDelete]: 1 },
227
+ });
228
+ const rows = result.modifiedCount || 0;
229
+ return {
230
+ message: rows + " " + table + (rows > 1 ? "s" : "") + " removed",
231
+ };
232
+ } else {
233
+ const result = await col.deleteMany(whereData.query);
234
+ const rows = result.deletedCount || 0;
235
+ return {
236
+ message: rows + " " + table + (rows > 1 ? "s" : "") + " removed",
237
+ };
238
+ }
239
+ }
240
+
241
+ async function upsert(table, data, uniqueKeys = []) {
242
+ let array = Array.isArray(data) ? [...data] : [data];
243
+ const total = array.length;
244
+ const col = database.collection(table);
245
+
246
+ if (!uniqueKeys || uniqueKeys.length === 0) {
247
+ return insert(table, data, uniqueKeys);
248
+ }
249
+
250
+ let lastId = null;
251
+ for (const row of array) {
252
+ const filterObj = {};
253
+ for (const key of uniqueKeys) {
254
+ let val = row[key];
255
+ // Convert _id string values to ObjectId when they look like valid ObjectIds
256
+ if (
257
+ key === "_id" &&
258
+ typeof val === "string" &&
259
+ /^[0-9a-fA-F]{24}$/.test(val)
260
+ ) {
261
+ try {
262
+ val = new ObjectId(val);
263
+ } catch (e) {
264
+ /* keep as string */
265
+ }
266
+ }
267
+ filterObj[key] = val;
268
+ }
269
+ const updateDoc = { $set: { ...row } };
270
+ // Remove _id from $set as MongoDB doesn't allow modifying _id
271
+ delete updateDoc.$set._id;
272
+ const result = await col.updateOne(filterObj, updateDoc, { upsert: true });
273
+ if (result.upsertedId) {
274
+ lastId = result.upsertedId;
275
+ }
276
+ }
277
+
278
+ const response = {
279
+ rows: total,
280
+ message:
281
+ (total === 1
282
+ ? `1 ${namify(table)} is `
283
+ : `${total} ${namify(table)}s are `) + "saved",
284
+ type: "success",
285
+ };
286
+ if (total === 1 && lastId) {
287
+ response.id = lastId;
288
+ }
289
+ return response;
290
+ }
291
+
292
+ async function insert(table, data, uniqueKeys = []) {
293
+ let array = Array.isArray(data) ? [...data] : [data];
294
+ const total = array.length;
295
+ const col = database.collection(table);
296
+
297
+ try {
298
+ let lastId = null;
299
+ if (total === 1) {
300
+ const result = await col.insertOne(array[0]);
301
+ lastId = result.insertedId;
302
+ } else {
303
+ const result = await col.insertMany(array, { ordered: false });
304
+ // For bulk, no single id to return
305
+ }
306
+
307
+ const response = {
308
+ rows: total,
309
+ message:
310
+ (total === 1
311
+ ? `1 ${namify(table)} is `
312
+ : `${total} ${namify(table)}s are `) + "saved",
313
+ type: "success",
314
+ };
315
+ if (total === 1 && lastId) {
316
+ response.id = lastId;
317
+ }
318
+ return response;
319
+ } catch (err) {
320
+ if (err.code === 11000 || (err.message && err.message.includes("E11000"))) {
321
+ const dupError = new Error(err.message);
322
+ dupError.code = "ER_DUP_ENTRY";
323
+ dupError.sqlMessage = err.message;
324
+ throw dupError;
325
+ }
326
+ throw err;
327
+ }
328
+ }
329
+
330
+ async function disconnect() {
331
+ if (client) {
332
+ await client.close();
333
+ client = null;
334
+ database = null;
335
+ }
336
+ }
337
+
338
+ // --- Utility helpers ---
339
+
340
+ /**
341
+ * Escape special regex characters in a string so it can be safely used in $regex.
342
+ */
343
+ function escapeRegex(str) {
344
+ return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
345
+ }
346
+
347
+ function buildSort(sort) {
348
+ if (!sort || sort.length < 1) {
349
+ return {};
350
+ }
351
+ const sortObj = {};
352
+ for (const item of sort) {
353
+ if (item[0] === "-") {
354
+ sortObj[item.substring(1)] = -1;
355
+ } else {
356
+ sortObj[item] = 1;
357
+ }
358
+ }
359
+ return sortObj;
360
+ }
361
+
362
+ function namify(text) {
363
+ return text
364
+ .replace("_", " ")
365
+ .replace(/(^\w{1})|(\s+\w{1})/g, (letter) => letter.toUpperCase());
366
+ }
367
+
368
+ module.exports = {
369
+ connect,
370
+ get,
371
+ list,
372
+ where,
373
+ query,
374
+ qcount,
375
+ remove,
376
+ upsert,
377
+ change: upsert,
378
+ insert,
379
+ disconnect,
380
+ close: disconnect,
381
+ };