hola-server 1.0.10 → 2.0.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/README.md +196 -1
- package/core/array.js +79 -142
- package/core/bash.js +208 -259
- package/core/chart.js +26 -16
- package/core/cron.js +14 -3
- package/core/date.js +15 -44
- package/core/encrypt.js +19 -9
- package/core/file.js +42 -29
- package/core/lhs.js +32 -6
- package/core/meta.js +213 -289
- package/core/msg.js +20 -7
- package/core/number.js +105 -103
- package/core/obj.js +15 -12
- package/core/random.js +9 -6
- package/core/role.js +69 -77
- package/core/thread.js +12 -2
- package/core/type.js +300 -261
- package/core/url.js +20 -12
- package/core/validate.js +29 -26
- package/db/db.js +297 -227
- package/db/entity.js +631 -963
- package/db/gridfs.js +120 -166
- package/design/add_default_field_attr.md +56 -0
- package/http/context.js +22 -8
- package/http/cors.js +25 -8
- package/http/error.js +27 -9
- package/http/express.js +70 -41
- package/http/params.js +70 -42
- package/http/router.js +51 -40
- package/http/session.js +59 -36
- package/index.js +85 -9
- package/package.json +2 -2
- package/router/clone.js +28 -36
- package/router/create.js +21 -26
- package/router/delete.js +24 -28
- package/router/read.js +137 -123
- package/router/update.js +38 -56
- package/setting.js +22 -6
- package/skills/array.md +155 -0
- package/skills/bash.md +91 -0
- package/skills/chart.md +54 -0
- package/skills/code.md +422 -0
- package/skills/context.md +177 -0
- package/skills/date.md +58 -0
- package/skills/express.md +255 -0
- package/skills/file.md +60 -0
- package/skills/lhs.md +54 -0
- package/skills/meta.md +1023 -0
- package/skills/msg.md +30 -0
- package/skills/number.md +88 -0
- package/skills/obj.md +36 -0
- package/skills/params.md +206 -0
- package/skills/random.md +22 -0
- package/skills/role.md +59 -0
- package/skills/session.md +281 -0
- package/skills/storage.md +743 -0
- package/skills/thread.md +22 -0
- package/skills/type.md +547 -0
- package/skills/url.md +34 -0
- package/skills/validate.md +48 -0
- package/test/cleanup/close-db.js +5 -0
- package/test/core/array.js +226 -0
- package/test/core/chart.js +51 -0
- package/test/core/file.js +59 -0
- package/test/core/lhs.js +44 -0
- package/test/core/number.js +167 -12
- package/test/core/obj.js +47 -0
- package/test/core/random.js +24 -0
- package/test/core/thread.js +20 -0
- package/test/core/type.js +216 -0
- package/test/core/validate.js +67 -0
- package/test/db/db-ops.js +99 -0
- package/test/db/pipe_test.txt +0 -0
- package/test/db/test_case_design.md +528 -0
- package/test/db/test_db_class.js +613 -0
- package/test/db/test_entity_class.js +414 -0
- package/test/db/test_gridfs_class.js +234 -0
- package/test/entity/create.js +1 -1
- package/test/entity/delete-mixed.js +156 -0
- package/test/entity/ref-filter.js +63 -0
- package/tool/gen_i18n.js +55 -21
- package/test/crud/router.js +0 -99
- package/test/router/user.js +0 -17
package/db/db.js
CHANGED
|
@@ -1,376 +1,446 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Database utilities for MongoDB using mongoist.
|
|
3
|
+
* @module db/db
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const mongoist = require("mongoist");
|
|
2
7
|
|
|
3
8
|
const { get_settings } = require("../setting");
|
|
4
9
|
const { get_context_value } = require("../http/context");
|
|
5
10
|
const { format_date_time } = require("../core/date");
|
|
6
11
|
|
|
12
|
+
// Log level constants
|
|
7
13
|
const LOG_LEVEL_DEBUG = 0;
|
|
8
14
|
const LOG_LEVEL_INFO = 1;
|
|
9
15
|
const LOG_LEVEL_WARN = 2;
|
|
10
16
|
const LOG_LEVEL_ERROR = 3;
|
|
11
17
|
|
|
18
|
+
// Log category constants
|
|
12
19
|
const LOG_DB = "database";
|
|
13
20
|
const LOG_ENTITY = "entity";
|
|
14
21
|
const LOG_SYSTEM = "system";
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const log_msg = (category, level, msg, extra) => {
|
|
22
|
-
const time = format_date_time(new Date());
|
|
23
|
-
const db = get_db();
|
|
24
|
-
const { col_log } = get_settings().log;
|
|
25
|
-
|
|
26
|
-
const req = get_context_value("req");
|
|
27
|
-
const path = req ? req.originalUrl : "";
|
|
28
|
-
const user = req && req.session && req.session.user ? req.session.user.id : "";
|
|
29
|
-
|
|
30
|
-
db.create(col_log, { time: time, category: category, level: level, msg: msg, user: user, path: path, ...extra }).then(() => { });
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const is_log_debug = () => {
|
|
34
|
-
const { save_db, log_level } = get_settings().log;
|
|
35
|
-
return save_db && log_level <= LOG_LEVEL_DEBUG;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const is_log_info = () => {
|
|
39
|
-
const { save_db, log_level } = get_settings().log;
|
|
40
|
-
return save_db && log_level <= LOG_LEVEL_INFO;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const is_log_warn = () => {
|
|
44
|
-
const { save_db, log_level } = get_settings().log;
|
|
45
|
-
return save_db && log_level <= LOG_LEVEL_WARN;
|
|
46
|
-
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve user id from request context if available.
|
|
25
|
+
* @returns {string} user id or empty string when unauthenticated
|
|
26
|
+
*/
|
|
27
|
+
const get_session_user_id = () => get_context_value("req")?.session?.user?.id || "";
|
|
47
28
|
|
|
48
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Check if logging is enabled for the given level.
|
|
31
|
+
* @param {number} level - Log level to check
|
|
32
|
+
* @returns {boolean} true if logging is enabled
|
|
33
|
+
*/
|
|
34
|
+
const is_log_enabled = (level) => {
|
|
49
35
|
const { save_db, log_level } = get_settings().log;
|
|
50
|
-
return save_db && log_level <=
|
|
36
|
+
return save_db && log_level <= level;
|
|
51
37
|
};
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
// Log level checkers using the unified helper
|
|
40
|
+
const is_log_debug = () => is_log_enabled(LOG_LEVEL_DEBUG);
|
|
41
|
+
const is_log_info = () => is_log_enabled(LOG_LEVEL_INFO);
|
|
42
|
+
const is_log_warn = () => is_log_enabled(LOG_LEVEL_WARN);
|
|
43
|
+
const is_log_error = () => is_log_enabled(LOG_LEVEL_ERROR);
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Write a log entry to the database.
|
|
47
|
+
* @param {string} category - Log category
|
|
48
|
+
* @param {number} level - Log level
|
|
49
|
+
* @param {string} message - Log message
|
|
50
|
+
* @param {Object} extra - Additional data
|
|
51
|
+
*/
|
|
52
|
+
const log_message = (category, level, message, extra = {}) => {
|
|
53
|
+
const req = get_context_value("req");
|
|
54
|
+
const entry = {
|
|
55
|
+
time: format_date_time(new Date()),
|
|
56
|
+
category,
|
|
57
|
+
level,
|
|
58
|
+
msg: message,
|
|
59
|
+
user: req?.session?.user?.id || "",
|
|
60
|
+
path: req?.originalUrl || "",
|
|
61
|
+
...extra
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
get_db().create(get_settings().log.col_log, entry).catch(() => { });
|
|
63
65
|
};
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Create a log function for a specific level.
|
|
69
|
+
* @param {number} level - Log level
|
|
70
|
+
* @param {Function} check_fn - Function to check if logging is enabled
|
|
71
|
+
* @returns {Function} Log function
|
|
72
|
+
*/
|
|
73
|
+
const create_log_fn = (level, check_fn) => (category, msg, extra) => {
|
|
74
|
+
if (check_fn()) {
|
|
75
|
+
log_message(category, level, msg, extra);
|
|
68
76
|
}
|
|
69
77
|
};
|
|
70
78
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
};
|
|
79
|
+
const log_debug = create_log_fn(LOG_LEVEL_DEBUG, is_log_debug);
|
|
80
|
+
const log_info = create_log_fn(LOG_LEVEL_INFO, is_log_info);
|
|
81
|
+
const log_warn = create_log_fn(LOG_LEVEL_WARN, is_log_warn);
|
|
82
|
+
const log_error = create_log_fn(LOG_LEVEL_ERROR, is_log_error);
|
|
76
83
|
|
|
77
84
|
/**
|
|
78
85
|
* Construct mongodb ObjectId
|
|
79
|
-
* @param {string value of the id
|
|
80
|
-
* @returns
|
|
86
|
+
* @param {string} id - String value of the id
|
|
87
|
+
* @returns {ObjectId} MongoDB ObjectId
|
|
81
88
|
*/
|
|
82
|
-
const oid = (id) =>
|
|
83
|
-
return mongoist.ObjectId(id);
|
|
84
|
-
};
|
|
89
|
+
const oid = (id) => mongoist.ObjectId(id);
|
|
85
90
|
|
|
86
91
|
/**
|
|
87
92
|
* Construct objectId query object
|
|
88
|
-
* @param {string value of the id
|
|
89
|
-
* @returns id query if id is valid otherwise
|
|
93
|
+
* @param {string} id - String value of the id
|
|
94
|
+
* @returns {Object|null} id query if id is valid, otherwise null
|
|
90
95
|
*/
|
|
91
96
|
const oid_query = (id) => {
|
|
92
97
|
try {
|
|
93
98
|
return { _id: oid(id) };
|
|
94
|
-
} catch
|
|
99
|
+
} catch {
|
|
95
100
|
return null;
|
|
96
101
|
}
|
|
97
102
|
};
|
|
98
103
|
|
|
99
104
|
/**
|
|
100
|
-
* Construct in query of ObjectId array
|
|
101
|
-
* @param {string
|
|
102
|
-
* @returns id in query if
|
|
105
|
+
* Construct $in query of ObjectId array
|
|
106
|
+
* @param {string[]} ids - Array of string ids
|
|
107
|
+
* @returns {Object|null} id $in query if valid, otherwise null
|
|
103
108
|
*/
|
|
104
109
|
const oid_queries = (ids) => {
|
|
105
110
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} catch (e) {
|
|
111
|
+
return { _id: { $in: ids.map(oid) } };
|
|
112
|
+
} catch {
|
|
109
113
|
return null;
|
|
110
114
|
}
|
|
111
115
|
};
|
|
112
116
|
|
|
113
117
|
/**
|
|
114
118
|
* Execute bulk update using the items
|
|
115
|
-
* @param {
|
|
116
|
-
* @param {
|
|
117
|
-
* @param {
|
|
118
|
-
* @returns
|
|
119
|
+
* @param {Object} col - MongoDB collection
|
|
120
|
+
* @param {Object[]} items - Items to execute bulk update
|
|
121
|
+
* @param {string[]} attrs - Attributes used as search criteria
|
|
122
|
+
* @returns {Promise} Bulk operation result
|
|
119
123
|
*/
|
|
120
124
|
const bulk_update = async (col, items, attrs) => {
|
|
121
125
|
const bulk = col.initializeOrderedBulkOp();
|
|
122
126
|
for (const item of items) {
|
|
123
|
-
const query = {};
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
});
|
|
127
|
-
delete item["_id"];
|
|
128
|
-
bulk.find(query).upsert().update({ $set: item }, true);
|
|
127
|
+
const query = attrs.reduce((acc, attr) => ({ ...acc, [attr]: item[attr] }), {});
|
|
128
|
+
const { _id, ...without_id } = item;
|
|
129
|
+
bulk.find(query).upsert().update({ $set: without_id }, true);
|
|
129
130
|
}
|
|
130
|
-
return
|
|
131
|
+
return bulk.execute();
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Log debug message for DB operations.
|
|
136
|
+
* @param {string} operation - Operation name
|
|
137
|
+
* @param {string} code - Collection name
|
|
138
|
+
* @param {Object} params - Operation parameters
|
|
139
|
+
*/
|
|
140
|
+
const debug_log = (operation, code, params = {}) => {
|
|
141
|
+
if (!is_log_debug()) return;
|
|
142
|
+
|
|
143
|
+
const parts = Object.entries(params)
|
|
144
|
+
.filter(([, v]) => v !== undefined)
|
|
145
|
+
.map(([k, v]) => `${k}:${JSON.stringify(v)}`);
|
|
146
|
+
|
|
147
|
+
log_debug(LOG_DB, `${operation} for [${code}]${parts.length ? ` with ${parts.join(", ")}` : ""}`);
|
|
131
148
|
};
|
|
132
149
|
|
|
133
150
|
class DB {
|
|
134
151
|
constructor(url, options, callback) {
|
|
135
152
|
if (!url) {
|
|
136
|
-
|
|
153
|
+
throw new Error("Mongo url is required to initialize DB");
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
this.db = mongoist(url, options);
|
|
157
|
+
this.closed = false;
|
|
140
158
|
|
|
141
|
-
this.db.on("error",
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this.db.on('connect', function () {
|
|
148
|
-
callback && callback();
|
|
159
|
+
this.db.on("error", (err) => log_error(LOG_DB, err));
|
|
160
|
+
this.db.on("close", () => { this.closed = true; });
|
|
161
|
+
this.db.on("connect", () => {
|
|
162
|
+
this.closed = false;
|
|
163
|
+
callback?.();
|
|
149
164
|
});
|
|
150
165
|
}
|
|
151
166
|
|
|
152
167
|
/**
|
|
153
|
-
*
|
|
154
|
-
* @param {
|
|
155
|
-
* @returns
|
|
168
|
+
* Get db collection
|
|
169
|
+
* @param {string} code - Collection name
|
|
170
|
+
* @returns {Object} MongoDB collection
|
|
156
171
|
*/
|
|
157
|
-
|
|
172
|
+
col(code) {
|
|
158
173
|
return this.db[code];
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
/**
|
|
162
|
-
* Insert
|
|
163
|
-
* @param {
|
|
164
|
-
* @param {
|
|
165
|
-
* @returns
|
|
177
|
+
* Insert object to db
|
|
178
|
+
* @param {string} code - Collection name
|
|
179
|
+
* @param {Object} obj - Object to insert
|
|
180
|
+
* @returns {Promise} Insert result
|
|
166
181
|
*/
|
|
167
182
|
create(code, obj) {
|
|
168
|
-
const col = this.db[code];
|
|
169
183
|
delete obj["_id"];
|
|
170
|
-
return col.insert(obj, { checkKeys: false });
|
|
184
|
+
return this.col(code).insert(obj, { checkKeys: false });
|
|
171
185
|
}
|
|
172
186
|
|
|
173
187
|
/**
|
|
174
|
-
* Update
|
|
175
|
-
* @param {
|
|
176
|
-
* @param {
|
|
177
|
-
* @param {
|
|
178
|
-
* @returns
|
|
188
|
+
* Update objects with upsert and multi enabled
|
|
189
|
+
* @param {string} code - Collection name
|
|
190
|
+
* @param {Object} query - Query criteria
|
|
191
|
+
* @param {Object} obj - Object to update
|
|
192
|
+
* @returns {Promise} Update result
|
|
179
193
|
*/
|
|
180
194
|
update(code, query, obj) {
|
|
181
|
-
|
|
182
|
-
log_debug(LOG_DB, "updating obj:" + JSON.stringify(obj) + ", for [" + code + "] with query:" + JSON.stringify(query));
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const col = this.db[code];
|
|
195
|
+
debug_log("updating", code, { query, obj });
|
|
186
196
|
delete obj["_id"];
|
|
187
|
-
return col.update(query, { $set: obj }, { upsert: true, multi: true });
|
|
197
|
+
return this.col(code).update(query, { $set: obj }, { upsert: true, multi: true });
|
|
188
198
|
}
|
|
189
199
|
|
|
190
200
|
/**
|
|
191
|
-
* Remove
|
|
192
|
-
* @param {
|
|
193
|
-
* @param {query to execute delete
|
|
194
|
-
* @returns
|
|
201
|
+
* Remove records from mongodb
|
|
202
|
+
* @param {string} code - Collection name
|
|
203
|
+
* @param {Object} query - Query to execute delete
|
|
204
|
+
* @returns {Promise} Delete result
|
|
195
205
|
*/
|
|
196
206
|
delete(code, query) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const col = this.db[code];
|
|
202
|
-
return col.remove(query);
|
|
207
|
+
debug_log("deleting", code, { query });
|
|
208
|
+
return this.col(code).remove(query);
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
/**
|
|
206
|
-
*
|
|
207
|
-
* @param {
|
|
208
|
-
* @param {
|
|
209
|
-
* @param {
|
|
210
|
-
* @returns
|
|
212
|
+
* Find records by query
|
|
213
|
+
* @param {string} code - Collection name
|
|
214
|
+
* @param {Object} query - Search criteria
|
|
215
|
+
* @param {Object} attr - Attributes to load
|
|
216
|
+
* @returns {Promise} Query result
|
|
211
217
|
*/
|
|
212
218
|
find(code, query, attr) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const col = this.db[code];
|
|
218
|
-
return col.find(query, attr);
|
|
219
|
+
debug_log("find", code, { query, attr });
|
|
220
|
+
return this.col(code).find(query, attr);
|
|
219
221
|
}
|
|
220
222
|
|
|
221
223
|
/**
|
|
222
|
-
* Find one record
|
|
223
|
-
* @param {
|
|
224
|
-
* @param {
|
|
225
|
-
* @param {
|
|
226
|
-
* @returns
|
|
224
|
+
* Find one record
|
|
225
|
+
* @param {string} code - Collection name
|
|
226
|
+
* @param {Object} query - Search criteria
|
|
227
|
+
* @param {Object} attr - Attributes to load
|
|
228
|
+
* @returns {Promise} Single record or null
|
|
227
229
|
*/
|
|
228
230
|
find_one(code, query, attr) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const col = this.db[code];
|
|
234
|
-
return col.findOne(query, attr);
|
|
231
|
+
debug_log("find_one", code, { query, attr });
|
|
232
|
+
return this.col(code).findOne(query, attr);
|
|
235
233
|
}
|
|
236
234
|
|
|
237
235
|
/**
|
|
238
|
-
* Find
|
|
239
|
-
* @param {
|
|
240
|
-
* @param {
|
|
241
|
-
* @param {
|
|
242
|
-
* @param {
|
|
243
|
-
* @returns
|
|
236
|
+
* Find records with sorting
|
|
237
|
+
* @param {string} code - Collection name
|
|
238
|
+
* @param {Object} query - Search criteria
|
|
239
|
+
* @param {Object} sort - Sort specification
|
|
240
|
+
* @param {Object} attr - Attributes to load
|
|
241
|
+
* @returns {Promise} Sorted query result
|
|
244
242
|
*/
|
|
245
243
|
find_sort(code, query, sort, attr) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const col = this.db[code];
|
|
251
|
-
return col.find(query, attr, { sort: sort });
|
|
244
|
+
debug_log("find_sort", code, { query, attr, sort });
|
|
245
|
+
return this.col(code).find(query, attr, { sort });
|
|
252
246
|
}
|
|
253
247
|
|
|
254
248
|
/**
|
|
255
|
-
* Find
|
|
256
|
-
* @param {
|
|
257
|
-
* @param {
|
|
258
|
-
* @param {
|
|
259
|
-
* @param {
|
|
260
|
-
* @param {
|
|
261
|
-
* @param {
|
|
262
|
-
* @returns
|
|
249
|
+
* Find paginated records
|
|
250
|
+
* @param {string} code - Collection name
|
|
251
|
+
* @param {Object} query - Search criteria
|
|
252
|
+
* @param {Object} sort - Sort specification
|
|
253
|
+
* @param {number} page - Page index (1-based)
|
|
254
|
+
* @param {number} limit - Page size
|
|
255
|
+
* @param {Object} attr - Attributes to load
|
|
256
|
+
* @returns {Promise} Paginated query result
|
|
263
257
|
*/
|
|
264
258
|
find_page(code, query, sort, page, limit, attr) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const skip = (page - 1) * limit > 0 ? (page - 1) * limit : 0;
|
|
270
|
-
const col = this.db[code];
|
|
271
|
-
return col.find(query, attr, { sort: sort, skip: skip, limit: limit });
|
|
259
|
+
debug_log("find_page", code, { query, attr, sort, page, limit });
|
|
260
|
+
const skip = Math.max((page - 1) * limit, 0);
|
|
261
|
+
return this.col(code).find(query, attr, { sort, skip, limit });
|
|
272
262
|
}
|
|
273
263
|
|
|
274
264
|
/**
|
|
275
|
-
*
|
|
276
|
-
* @param {
|
|
277
|
-
* @param {
|
|
278
|
-
* @returns
|
|
265
|
+
* Count records matching query
|
|
266
|
+
* @param {string} code - Collection name
|
|
267
|
+
* @param {Object} query - Search criteria
|
|
268
|
+
* @returns {Promise<number>} Count result
|
|
279
269
|
*/
|
|
280
270
|
count(code, query) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
const col = this.db[code];
|
|
286
|
-
return col.count(query);
|
|
271
|
+
debug_log("count", code, { query });
|
|
272
|
+
return this.col(code).count(query);
|
|
287
273
|
}
|
|
288
274
|
|
|
289
275
|
/**
|
|
290
|
-
* Calculate
|
|
291
|
-
* @param {
|
|
292
|
-
* @param {
|
|
293
|
-
* @param {field name to
|
|
294
|
-
* @returns
|
|
276
|
+
* Calculate sum of a field
|
|
277
|
+
* @param {string} code - Collection name
|
|
278
|
+
* @param {Object} query - Search criteria
|
|
279
|
+
* @param {string} field - Field name to sum
|
|
280
|
+
* @returns {Promise<number>} Sum result
|
|
295
281
|
*/
|
|
296
282
|
sum(code, query, field) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const col = this.db[code];
|
|
302
|
-
return col.aggregate([{ $match: query }, { $group: { _id: null, total: { $sum: "$" + field + "" } } }]).then((result) => (result.length > 0 ? result[0].total : 0));
|
|
283
|
+
debug_log("sum", code, { query, field });
|
|
284
|
+
return this.col(code)
|
|
285
|
+
.aggregate([{ $match: query }, { $group: { _id: null, total: { $sum: `$${field}` } } }])
|
|
286
|
+
.then((result) => result[0]?.total ?? 0);
|
|
303
287
|
}
|
|
304
288
|
|
|
305
289
|
/**
|
|
306
|
-
* Pull
|
|
307
|
-
* @param {
|
|
308
|
-
* @param {
|
|
309
|
-
* @param {
|
|
310
|
-
* @returns
|
|
290
|
+
* Pull element from array field
|
|
291
|
+
* @param {string} code - Collection name
|
|
292
|
+
* @param {Object} query - Search criteria
|
|
293
|
+
* @param {Object} ele - Element to pull
|
|
294
|
+
* @returns {Promise} Update result
|
|
311
295
|
*/
|
|
312
296
|
pull(code, query, ele) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const col = this.db[code];
|
|
318
|
-
return col.update(query, { $pull: ele }, { multi: true });
|
|
297
|
+
debug_log("pull", code, { query, ele });
|
|
298
|
+
return this.col(code).update(query, { $pull: ele }, { multi: true });
|
|
319
299
|
}
|
|
320
300
|
|
|
321
301
|
/**
|
|
322
|
-
* Push
|
|
323
|
-
* @param {
|
|
324
|
-
* @param {
|
|
325
|
-
* @param {
|
|
326
|
-
* @returns
|
|
302
|
+
* Push element to array field
|
|
303
|
+
* @param {string} code - Collection name
|
|
304
|
+
* @param {Object} query - Search criteria
|
|
305
|
+
* @param {Object} ele - Element to push
|
|
306
|
+
* @returns {Promise} Update result
|
|
327
307
|
*/
|
|
328
308
|
push(code, query, ele) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const col = this.db[code];
|
|
334
|
-
return col.update(query, { $push: ele });
|
|
309
|
+
debug_log("push", code, { query, ele });
|
|
310
|
+
return this.col(code).update(query, { $push: ele });
|
|
335
311
|
}
|
|
336
312
|
|
|
337
313
|
/**
|
|
338
|
-
*
|
|
339
|
-
* @param {
|
|
340
|
-
* @param {
|
|
341
|
-
* @param {
|
|
342
|
-
* @returns
|
|
314
|
+
* Add element to set field
|
|
315
|
+
* @param {string} code - Collection name
|
|
316
|
+
* @param {Object} query - Search criteria
|
|
317
|
+
* @param {Object} ele - Element to add
|
|
318
|
+
* @returns {Promise} Update result
|
|
343
319
|
*/
|
|
344
320
|
add_to_set(code, query, ele) {
|
|
345
|
-
|
|
346
|
-
return col.update(query, { $addToSet: ele }, { upsert: true });
|
|
321
|
+
return this.col(code).update(query, { $addToSet: ele }, { upsert: true });
|
|
347
322
|
}
|
|
348
323
|
|
|
349
324
|
/**
|
|
350
|
-
*
|
|
351
|
-
* @param {mongodb collection name} code
|
|
352
|
-
* @returns
|
|
325
|
+
* Close the underlying Mongo connection
|
|
353
326
|
*/
|
|
354
|
-
|
|
355
|
-
|
|
327
|
+
async close() {
|
|
328
|
+
if (!this.db || this.closed) return;
|
|
329
|
+
if (typeof this.db.close === "function") {
|
|
330
|
+
await this.db.close();
|
|
331
|
+
}
|
|
332
|
+
this.closed = true;
|
|
356
333
|
}
|
|
357
334
|
}
|
|
358
335
|
|
|
359
336
|
let db_instance = null;
|
|
360
337
|
|
|
361
338
|
/**
|
|
362
|
-
*
|
|
363
|
-
* @
|
|
339
|
+
* Get or create the database instance
|
|
340
|
+
* @param {Function} callback - Called when connected
|
|
341
|
+
* @returns {DB} Database instance
|
|
364
342
|
*/
|
|
365
343
|
const get_db = (callback) => {
|
|
366
|
-
if (db_instance && db_instance.
|
|
344
|
+
if (db_instance?.db && !db_instance.closed) {
|
|
367
345
|
return db_instance;
|
|
368
|
-
}
|
|
369
|
-
const mongo = get_settings().mongo;
|
|
346
|
+
}
|
|
370
347
|
|
|
371
|
-
|
|
372
|
-
|
|
348
|
+
const { url, options } = get_settings().mongo || {};
|
|
349
|
+
if (!url) {
|
|
350
|
+
throw new Error("Mongo settings are missing url");
|
|
373
351
|
}
|
|
352
|
+
|
|
353
|
+
db_instance = new DB(url, options, callback);
|
|
354
|
+
return db_instance;
|
|
374
355
|
};
|
|
375
356
|
|
|
376
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Close the database connection
|
|
359
|
+
*/
|
|
360
|
+
const close_db = async () => {
|
|
361
|
+
if (db_instance) {
|
|
362
|
+
await db_instance.close();
|
|
363
|
+
db_instance = null;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Auto-create indexes based on entity metadata.
|
|
369
|
+
* Safe to call multiple times - MongoDB's createIndex is idempotent.
|
|
370
|
+
* Should be called after all entity metas are registered (after validate_all_metas).
|
|
371
|
+
*/
|
|
372
|
+
const ensure_entity_indexes = async () => {
|
|
373
|
+
const { get_all_metas, get_entity_meta } = require('../core/meta');
|
|
374
|
+
const db = get_db();
|
|
375
|
+
|
|
376
|
+
for (const collection of get_all_metas()) {
|
|
377
|
+
const meta = get_entity_meta(collection);
|
|
378
|
+
const col = db.col(collection);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// 1. Primary key compound index (unique)
|
|
382
|
+
if (meta.primary_keys?.length > 0) {
|
|
383
|
+
const key = meta.primary_keys.reduce((k, f) => ({ ...k, [f]: 1 }), {});
|
|
384
|
+
await col.createIndex(key, { unique: true, background: true });
|
|
385
|
+
log_debug(LOG_DB, `index created for [${collection}] primary_keys: ${JSON.stringify(key)}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 2. Search fields (individual indexes for query performance)
|
|
389
|
+
for (const field of meta.search_fields || []) {
|
|
390
|
+
if (!meta.primary_keys.includes(field.name)) {
|
|
391
|
+
await col.createIndex({ [field.name]: 1 }, { background: true });
|
|
392
|
+
log_debug(LOG_DB, `index created for [${collection}] search field: ${field.name}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 3. Ref label (for reference lookups)
|
|
397
|
+
if (meta.ref_label && !meta.primary_keys.includes(meta.ref_label)) {
|
|
398
|
+
await col.createIndex({ [meta.ref_label]: 1 }, { background: true });
|
|
399
|
+
log_debug(LOG_DB, `index created for [${collection}] ref_label: ${meta.ref_label}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 4. Ref fields (foreign key lookups)
|
|
403
|
+
for (const field of meta.ref_fields || []) {
|
|
404
|
+
if (!meta.primary_keys.includes(field.name)) {
|
|
405
|
+
await col.createIndex({ [field.name]: 1 }, { background: true });
|
|
406
|
+
log_debug(LOG_DB, `index created for [${collection}] ref field: ${field.name}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
} catch (err) {
|
|
410
|
+
log_error(LOG_DB, `index creation failed for [${collection}]: ${err.message}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
log_info(LOG_DB, `entity indexes ensured for ${get_all_metas().length} collections`);
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
module.exports = {
|
|
418
|
+
// ObjectId utilities
|
|
419
|
+
oid,
|
|
420
|
+
oid_query,
|
|
421
|
+
oid_queries,
|
|
422
|
+
bulk_update,
|
|
423
|
+
|
|
424
|
+
// Database access
|
|
425
|
+
get_db,
|
|
426
|
+
close_db,
|
|
427
|
+
ensure_entity_indexes,
|
|
428
|
+
|
|
429
|
+
// Logging functions
|
|
430
|
+
log_debug,
|
|
431
|
+
log_info,
|
|
432
|
+
log_warn,
|
|
433
|
+
log_error,
|
|
434
|
+
is_log_debug,
|
|
435
|
+
is_log_info,
|
|
436
|
+
is_log_warn,
|
|
437
|
+
is_log_error,
|
|
438
|
+
|
|
439
|
+
// Log categories
|
|
440
|
+
LOG_DB,
|
|
441
|
+
LOG_SYSTEM,
|
|
442
|
+
LOG_ENTITY,
|
|
443
|
+
|
|
444
|
+
// Session
|
|
445
|
+
get_session_user_id
|
|
446
|
+
};
|