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.
Files changed (83) hide show
  1. package/README.md +196 -1
  2. package/core/array.js +79 -142
  3. package/core/bash.js +208 -259
  4. package/core/chart.js +26 -16
  5. package/core/cron.js +14 -3
  6. package/core/date.js +15 -44
  7. package/core/encrypt.js +19 -9
  8. package/core/file.js +42 -29
  9. package/core/lhs.js +32 -6
  10. package/core/meta.js +213 -289
  11. package/core/msg.js +20 -7
  12. package/core/number.js +105 -103
  13. package/core/obj.js +15 -12
  14. package/core/random.js +9 -6
  15. package/core/role.js +69 -77
  16. package/core/thread.js +12 -2
  17. package/core/type.js +300 -261
  18. package/core/url.js +20 -12
  19. package/core/validate.js +29 -26
  20. package/db/db.js +297 -227
  21. package/db/entity.js +631 -963
  22. package/db/gridfs.js +120 -166
  23. package/design/add_default_field_attr.md +56 -0
  24. package/http/context.js +22 -8
  25. package/http/cors.js +25 -8
  26. package/http/error.js +27 -9
  27. package/http/express.js +70 -41
  28. package/http/params.js +70 -42
  29. package/http/router.js +51 -40
  30. package/http/session.js +59 -36
  31. package/index.js +85 -9
  32. package/package.json +2 -2
  33. package/router/clone.js +28 -36
  34. package/router/create.js +21 -26
  35. package/router/delete.js +24 -28
  36. package/router/read.js +137 -123
  37. package/router/update.js +38 -56
  38. package/setting.js +22 -6
  39. package/skills/array.md +155 -0
  40. package/skills/bash.md +91 -0
  41. package/skills/chart.md +54 -0
  42. package/skills/code.md +422 -0
  43. package/skills/context.md +177 -0
  44. package/skills/date.md +58 -0
  45. package/skills/express.md +255 -0
  46. package/skills/file.md +60 -0
  47. package/skills/lhs.md +54 -0
  48. package/skills/meta.md +1023 -0
  49. package/skills/msg.md +30 -0
  50. package/skills/number.md +88 -0
  51. package/skills/obj.md +36 -0
  52. package/skills/params.md +206 -0
  53. package/skills/random.md +22 -0
  54. package/skills/role.md +59 -0
  55. package/skills/session.md +281 -0
  56. package/skills/storage.md +743 -0
  57. package/skills/thread.md +22 -0
  58. package/skills/type.md +547 -0
  59. package/skills/url.md +34 -0
  60. package/skills/validate.md +48 -0
  61. package/test/cleanup/close-db.js +5 -0
  62. package/test/core/array.js +226 -0
  63. package/test/core/chart.js +51 -0
  64. package/test/core/file.js +59 -0
  65. package/test/core/lhs.js +44 -0
  66. package/test/core/number.js +167 -12
  67. package/test/core/obj.js +47 -0
  68. package/test/core/random.js +24 -0
  69. package/test/core/thread.js +20 -0
  70. package/test/core/type.js +216 -0
  71. package/test/core/validate.js +67 -0
  72. package/test/db/db-ops.js +99 -0
  73. package/test/db/pipe_test.txt +0 -0
  74. package/test/db/test_case_design.md +528 -0
  75. package/test/db/test_db_class.js +613 -0
  76. package/test/db/test_entity_class.js +414 -0
  77. package/test/db/test_gridfs_class.js +234 -0
  78. package/test/entity/create.js +1 -1
  79. package/test/entity/delete-mixed.js +156 -0
  80. package/test/entity/ref-filter.js +63 -0
  81. package/tool/gen_i18n.js +55 -21
  82. package/test/crud/router.js +0 -99
  83. package/test/router/user.js +0 -17
package/db/entity.js CHANGED
@@ -1,826 +1,684 @@
1
+ /**
2
+ * @fileoverview Entity-level CRUD helpers and metadata-driven operations.
3
+ * @module db/entity
4
+ */
5
+
1
6
  const { SUCCESS, ERROR, NO_PARAMS, INVALID_PARAMS, DUPLICATE_KEY, NOT_FOUND, REF_NOT_FOUND, REF_NOT_UNIQUE, HAS_REF } = require('../http/code');
2
7
  const { validate_required_fields, has_value } = require('../core/validate');
3
8
  const { required_params } = require('../http/params');
4
9
  const { convert_type, convert_update_type, get_type } = require('../core/type');
5
10
  const { get_entity_meta, DELETE_MODE } = require('../core/meta');
6
11
  const { unique, map_array_to_obj } = require('../core/array');
7
- const { LOG_ENTITY, get_db, oid_query, oid_queries, is_log_debug, is_log_error, log_debug, log_error, get_session_userid, bulk_update } = require('./db');
12
+ const { LOG_ENTITY, get_db, oid_query, oid_queries, log_debug, log_error, get_session_user_id, bulk_update } = require('./db');
13
+
14
+ // Comparison operator mapping for search queries
15
+ const COMPARISON_OPERATORS = [
16
+ { prefix: '>=', op: '$gte', len: 2 },
17
+ { prefix: '<=', op: '$lte', len: 2 },
18
+ { prefix: '>', op: '$gt', len: 1 },
19
+ { prefix: '<', op: '$lt', len: 1 }
20
+ ];
8
21
 
9
22
  /**
10
- * Convert search value type, if there is error, keep it
11
- * @param {field type} type_name
12
- * @param {search value} search_value
13
- * @returns
23
+ * Convert search value type, keeping original on error.
24
+ * @param {string} type_name - Field type
25
+ * @param {*} search_value - Search value
26
+ * @returns {*} Converted value or original on error
14
27
  */
15
- const convert_search_value_by_type = (type_name, search_value) => {
16
- const type = get_type(type_name);
17
- const { value, err } = type.convert(search_value);
18
- if (err) {
19
- return search_value;
20
- } else {
21
- return value;
22
- }
23
- }
28
+ const convert_search_value = (type_name, search_value) => {
29
+ const { value, err } = get_type(type_name).convert(search_value);
30
+ return err ? search_value : value;
31
+ };
24
32
 
25
33
  /**
26
- * Create search object based on the field type and value
27
- * @param {field name} name
28
- * @param {field type name} type_name
29
- * @param {search value} search_value
30
- * @returns
34
+ * Create search object based on field type and value.
35
+ * @param {string} name - Field name
36
+ * @param {string} type_name - Field type name
37
+ * @param {*} search_value - Search value
38
+ * @returns {Object} MongoDB query object
31
39
  */
32
- const parse_search_value = function (name, type_name, search_value) {
33
- search_value = search_value + "";
34
- if (search_value.startsWith(">=")) {
35
- const value = search_value.substring(2);
36
- return { [name]: { "$gte": convert_search_value_by_type(type_name, value) } };
37
- } else if (search_value.startsWith("<=")) {
38
- const value = search_value.substring(2);
39
- return { [name]: { "$lte": convert_search_value_by_type(type_name, value) } };
40
- } else if (search_value.startsWith(">")) {
41
- const value = search_value.substring(1);
42
- return { [name]: { "$gt": convert_search_value_by_type(type_name, value) } };
43
- } else if (search_value.startsWith("<")) {
44
- const value = search_value.substring(1);
45
- return { [name]: { "$lt": convert_search_value_by_type(type_name, value) } };
46
- } else if (type_name === "array") {
47
- return { [name]: { "$in": [search_value] } };
48
- } else {
49
- let value = convert_search_value_by_type(type_name, search_value);
50
- if (typeof value === "string") {
51
- value = new RegExp(value, 'i');
40
+ const parse_search_value = (name, type_name, search_value) => {
41
+ const raw = `${search_value}`;
42
+
43
+ // Handle comma-separated values
44
+ if (raw.includes(',')) {
45
+ const values = raw.split(',').map(v => convert_search_value(type_name, v));
46
+ const op = type_name === 'array' ? '$all' : '$in';
47
+ return { [name]: { [op]: values } };
48
+ }
49
+
50
+ // Handle comparison operators
51
+ for (const { prefix, op, len } of COMPARISON_OPERATORS) {
52
+ if (raw.startsWith(prefix)) {
53
+ return { [name]: { [op]: convert_search_value(type_name, raw.substring(len)) } };
52
54
  }
53
- return { [name]: value }
54
55
  }
56
+
57
+ // Handle array type
58
+ if (type_name === 'array') {
59
+ return { [name]: { $in: [raw] } };
60
+ }
61
+
62
+ // Default: convert value, use regex for strings
63
+ let value = convert_search_value(type_name, raw);
64
+ if (typeof value === 'string') {
65
+ value = new RegExp(value, 'i');
66
+ }
67
+ return { [name]: value };
68
+ };
69
+
70
+ /**
71
+ * Apply ref_filter to query based on entity context.
72
+ * @param {Object} query - Existing query
73
+ * @param {Object} ref_filter - Filter configuration
74
+ * @param {string} ref_by_entity - Referring entity name
75
+ * @returns {Object} Query with filter applied
76
+ */
77
+ const apply_ref_filter = (query, ref_filter, ref_by_entity) => {
78
+ if (!ref_filter) return query;
79
+
80
+ const filter = (ref_by_entity && ref_filter[ref_by_entity])
81
+ || ref_filter['*']
82
+ || (typeof ref_filter === 'object' ? ref_filter : null);
83
+
84
+ return filter ? { ...query, ...filter } : query;
85
+ };
86
+
87
+ /**
88
+ * Log error with formatted message.
89
+ * @param {string} msg - Error message
90
+ * @param {Object} data - Additional data to log
91
+ */
92
+ const log_err = (msg, data = {}) => {
93
+ const parts = Object.entries(data)
94
+ .filter(([, v]) => v !== undefined)
95
+ .map(([k, v]) => `${k}:${JSON.stringify(v)}`);
96
+ log_error(LOG_ENTITY, parts.length ? `${msg} - ${parts.join(', ')}` : msg);
97
+ };
98
+
99
+ /**
100
+ * Execute a lifecycle hook and return error result if failed.
101
+ * @param {Function} hook - Hook function to execute
102
+ * @param {string} hook_name - Name for logging
103
+ * @param {...any} args - Arguments to pass to hook
104
+ * @returns {Object|null} Error result or null if successful
105
+ */
106
+ const run_hook = async (hook, hook_name, ...args) => {
107
+ if (!hook) return null;
108
+ const { code, err } = await hook(...args);
109
+ if (err || code !== SUCCESS) {
110
+ log_err(`${hook_name} error`, { err, code });
111
+ return { code, err };
112
+ }
113
+ return null;
114
+ };
115
+
116
+ /**
117
+ * Validate reference and return error result if failed.
118
+ * @param {Entity} entity - Entity instance
119
+ * @param {Object} obj - Object to validate
120
+ * @returns {Object|null} Error result or null if successful
121
+ */
122
+ const validate_refs = async (entity, obj) => {
123
+ if (!entity.meta.ref_fields) return null;
124
+ const { code, err } = await entity.validate_ref(obj);
125
+ if (err || code !== SUCCESS) {
126
+ log_err('validate_ref error', { err, code });
127
+ return { code, err };
128
+ }
129
+ return null;
130
+ };
131
+
132
+ /**
133
+ * Parse positive integer with default.
134
+ * @param {*} value - Value to parse
135
+ * @param {number} defaultVal - Default value
136
+ * @returns {number} Parsed integer or default
137
+ */
138
+ const parse_int = (value, defaultVal) => {
139
+ const parsed = parseInt(value);
140
+ return isNaN(parsed) || parsed <= 0 ? defaultVal : parsed;
141
+ };
142
+
143
+ /**
144
+ * Extract ref and link fields from field map based on allowed names.
145
+ * @param {Object} fields_map - Map of field name to field config
146
+ * @param {string[]} attr_names - Attribute names to check
147
+ * @param {string[]} allowed_names - Allowed field names
148
+ * @returns {Object} Object with attrs, ref_fields, and link_fields
149
+ */
150
+ const extract_field_info = (fields_map, attr_names, allowed_names) => {
151
+ const attrs = { _id: 1 };
152
+ const ref_fields = [];
153
+ const link_fields = [];
154
+
155
+ attr_names.split(',').forEach(attr => {
156
+ if (!allowed_names.includes(attr)) return;
157
+
158
+ attrs[attr] = 1;
159
+ const field = fields_map[attr];
160
+ if (field.link) {
161
+ link_fields.push(field);
162
+ attrs[field.link] = 1;
163
+ } else if (field.ref) {
164
+ ref_fields.push(field);
165
+ }
166
+ });
167
+
168
+ return { attrs, ref_fields, link_fields };
55
169
  };
56
170
 
57
171
  class Entity {
58
172
  /**
59
- * @param {entity meta obj} meta
173
+ * @param {Object|string} meta - Entity meta object or collection name
60
174
  */
61
175
  constructor(meta) {
62
- this.meta = (typeof meta === 'string' || meta instanceof String) ? get_entity_meta(meta) : meta;
176
+ this.meta = typeof meta === 'string' ? get_entity_meta(meta) : meta;
63
177
  this.db = get_db();
64
178
  }
65
179
 
66
- /**
67
- *
68
- * @returns mongo underlying collection
69
- */
70
- get_col() {
71
- return this.db.get_col(this.meta.collection);
180
+ /** @returns {Object} MongoDB collection */
181
+ col() {
182
+ return this.db.col(this.meta.collection);
72
183
  }
73
184
 
74
185
  /**
75
- * Execute bulk update using the items
76
- * @param {the items to execute bulk update} items
77
- * @param {the attributes used as search criteria} attrs
78
- * @returns
186
+ * Execute bulk update using the items.
187
+ * @param {Object[]} items - Items to update
188
+ * @param {string[]} attrs - Attributes for search criteria
79
189
  */
80
190
  async bulk_update(items, attrs) {
81
- const col = this.get_col();
82
- await bulk_update(col, items, attrs);
191
+ await bulk_update(this.col(), items, attrs);
83
192
  }
84
193
 
85
194
  /**
86
- * validate the ref value, if success, return code:SUCCESS
87
- * and if the value is ref_label then convert ref_label to objectid
88
- * @param {param object} param_obj
89
- * @returns code and err
195
+ * Validate ref value and convert ref_label to objectid.
196
+ * @param {Object} param_obj - Parameter object
197
+ * @returns {Object} Result with code and optional err
90
198
  */
91
199
  async validate_ref(param_obj) {
92
200
  const ref_fields = this.meta.ref_fields;
93
- if (ref_fields) {
94
- for (let i = 0; i < ref_fields.length; i++) {
95
- const field = ref_fields[i];
96
- const value = param_obj[field.name];
97
- const ref_entity = new Entity(get_entity_meta(field.ref));
201
+ if (!ref_fields) return { code: SUCCESS };
98
202
 
99
- if (Array.isArray(value)) {
100
- const array = [];
101
- for (let j = 0; j < value.length; j++) {
102
- const v = value[j];
103
- const ref_entities = await ref_entity.find_by_ref_value(v, { "_id": 1 }, this.meta.collection);
104
-
105
- if (ref_entities.length == 0) {
106
- return { code: REF_NOT_FOUND, err: [field.name] };
107
- } else if (ref_entities.length > 1) {
108
- return { code: REF_NOT_UNIQUE, err: [field.name] };
109
- } else if (ref_entities.length == 1) {
110
- array.push(ref_entities[0]["_id"] + "");
111
- }
112
- }
113
- param_obj[field.name] = array;
203
+ for (const field of ref_fields) {
204
+ const value = param_obj[field.name];
205
+ const ref_entity = new Entity(get_entity_meta(field.ref));
114
206
 
115
- } else if (has_value(value)) {
116
- const ref_entities = await ref_entity.find_by_ref_value(value, { "_id": 1 }, this.meta.collection);
207
+ const resolve_ref = async (v) => {
208
+ const refs = await ref_entity.find_by_ref_value(v, { _id: 1 }, this.meta.collection);
209
+ if (refs.length === 0) return { code: REF_NOT_FOUND, err: [field.name] };
210
+ if (refs.length > 1) return { code: REF_NOT_UNIQUE, err: [field.name] };
211
+ return { id: `${refs[0]._id}` };
212
+ };
117
213
 
118
- if (ref_entities.length == 0) {
119
- return { code: REF_NOT_FOUND, err: [field.name] };
120
- } else if (ref_entities.length > 1) {
121
- return { code: REF_NOT_UNIQUE, err: [field.name] };
122
- } else if (ref_entities.length == 1) {
123
- param_obj[field.name] = ref_entities[0]["_id"] + "";
124
- }
214
+ if (Array.isArray(value)) {
215
+ const ids = [];
216
+ for (const v of value) {
217
+ const result = await resolve_ref(v);
218
+ if (result.code) return result;
219
+ ids.push(result.id);
125
220
  }
221
+ param_obj[field.name] = ids;
222
+ } else if (has_value(value)) {
223
+ const result = await resolve_ref(value);
224
+ if (result.code) return result;
225
+ param_obj[field.name] = result.id;
126
226
  }
127
227
  }
128
228
  return { code: SUCCESS };
129
229
  }
130
230
 
131
231
  /**
132
- * Create search query object using params from client side
133
- * @param {search field value from client side} param_obj
232
+ * Create search query from client params.
233
+ * @param {Object} param_obj - Search parameters
234
+ * @returns {Object|null} Query object or null if no search fields
134
235
  */
135
236
  async get_search_query(param_obj) {
136
- const search_fields = this.meta.search_fields;
137
- if (search_fields && search_fields.length > 0) {
138
- const refer_field_names = this.meta.ref_fields.map(f => f.name);
139
- const query = {};
140
- const and_array = [];
141
- for (let i = 0; i < search_fields.length; i++) {
142
- const search_field = search_fields[i];
143
- const value = param_obj[search_field.name];
144
- if (has_value(value)) {
145
- if (refer_field_names.includes(search_field.name)) {
146
- //refer field
147
- const refer_entity = new Entity(get_entity_meta(search_field.ref));
148
- const oids = await refer_entity.find_by_ref_value(value, { _id: 1 }, this.meta.collection);
149
- if (oids.length == 1) {
150
- and_array.push({ [search_field.name]: oids.map(o => o._id + "")[0] });
151
- } else if (oids.length > 1) {
152
- and_array.push({ [search_field.name]: { "$in": oids.map(o => o._id + "") } });
153
- }
154
- } else {
155
- and_array.push(parse_search_value(search_field.name, search_field.type, value));
156
- }
157
- }
158
- }
237
+ const { search_fields } = this.meta;
238
+ if (!search_fields?.length) return null;
159
239
 
160
- if (param_obj["_id"] && param_obj["_id"].trim().length > 0) {
161
- const ids = param_obj["_id"].split(",");
162
- if (ids.length == 1) {
163
- and_array.push(oid_query(ids[0]));
164
- } else if (ids.length > 1) {
165
- and_array.push(oid_queries(ids));
166
- }
167
- }
240
+ const ref_names = this.meta.ref_fields.map(f => f.name);
241
+ const and_array = [];
168
242
 
169
- if (and_array.length > 0) {
170
- query["$and"] = and_array;
243
+ for (const field of search_fields) {
244
+ const value = param_obj[field.name];
245
+ if (!has_value(value)) continue;
171
246
 
172
- if (is_log_debug()) {
173
- log_debug(LOG_ENTITY, "search query:" + JSON.stringify(query));
247
+ if (ref_names.includes(field.name)) {
248
+ const ref_entity = new Entity(get_entity_meta(field.ref));
249
+ const oids = await ref_entity.find_by_ref_value(value, { _id: 1 }, this.meta.collection);
250
+ if (oids.length > 0) {
251
+ const ids = oids.map(o => `${o._id}`);
252
+ const op = oids.length === 1 ? null : (`${value}`.includes(',') ? '$all' : '$in');
253
+ and_array.push(op ? { [field.name]: { [op]: ids } } : { [field.name]: ids[0] });
174
254
  }
175
-
176
- return query;
177
255
  } else {
178
- return {};
256
+ and_array.push(parse_search_value(field.name, field.type, value));
179
257
  }
180
- } else {
181
- return null;
182
258
  }
259
+
260
+ // Handle _id parameter
261
+ const id_param = param_obj._id?.trim();
262
+ if (id_param) {
263
+ const ids = id_param.split(',');
264
+ and_array.push(ids.length === 1 ? oid_query(ids[0]) : oid_queries(ids));
265
+ }
266
+
267
+ if (and_array.length > 0) {
268
+ const query = { $and: and_array };
269
+ log_debug(LOG_ENTITY, `search query:${JSON.stringify(query)}`);
270
+ return query;
271
+ }
272
+ return {};
183
273
  }
184
274
 
185
275
  /**
186
- * if query is set, it will use this as search query, otherwise create search object from param_obj
187
- * @param {query object to search} query
188
- * @param {search object and all the search attributes object} param_obj
189
- * @returns
276
+ * List entities with pagination.
277
+ * @param {Object} query_params - Query parameters
278
+ * @param {Object} query - Additional query filter
279
+ * @param {Object} param_obj - Search parameters
280
+ * @param {string} view - View filter
281
+ * @returns {Object} Result with code, total, and data
190
282
  */
191
283
  async list_entity(query_params, query, param_obj, view) {
192
- const error_required_field_names = validate_required_fields(query_params, ["attr_names", "sort_by", "desc"]);
193
- if (error_required_field_names.length > 0) {
194
- if (is_log_error()) {
195
- log_error(LOG_ENTITY, "error required fields:" + JSON.stringify(error_required_field_names));
196
- }
197
-
198
- return { code: NO_PARAMS, err: error_required_field_names };
284
+ const missing = validate_required_fields(query_params, ['attr_names', 'sort_by', 'desc']);
285
+ if (missing.length > 0) {
286
+ log_err('missing required fields', { fields: missing });
287
+ return { code: NO_PARAMS, err: missing };
199
288
  }
200
289
 
201
290
  const { attr_names, page, limit, sort_by, desc } = query_params;
202
- const sort = {};
203
- const sorts = sort_by.split(",");
204
- const descs = desc.split(",");
205
- sorts.forEach(function (value, index) {
206
- sort[value] = descs[index] === "false" ? 1 : -1;
207
- });
208
291
 
209
- const list_fields = view && view !== "*" ? this.meta.list_fields.filter(field => Array.isArray(field.view) ? this.contain_view(field.view, view) || field.view.includes("*") : view.includes(field.view) || field.view === "*") : this.meta.list_fields;
210
- const list_field_names = list_fields.map(f => f.name);
211
-
212
- const ref_fields = [];
213
- const link_fields = [];
214
-
215
- const attrs = {};
216
- const fields_map = this.meta.fields_map;
217
- attr_names.split(",").forEach((attr) => {
218
- if (list_field_names.includes(attr)) {
219
- attrs[attr] = 1;
220
- const field = fields_map[attr];
221
- if (field.link) {
222
- link_fields.push(field);
223
- attrs[field.link] = 1;
224
- } else if (field.ref) {
225
- ref_fields.push(field);
226
- }
227
- }
228
- });
292
+ // Build sort object
293
+ const sorts = sort_by.split(',');
294
+ const descs = desc.split(',');
295
+ const sort = sorts.reduce((s, field, i) => ({ ...s, [field]: descs[i] === 'false' ? 1 : -1 }), {});
229
296
 
230
- let page_int = parseInt(page);
231
- page_int = isNaN(page_int) || page_int <= 0 ? 1 : page_int;
232
- let page_limit = parseInt(limit);
233
- page_limit = isNaN(page_limit) || page_limit <= 0 ? 10 : page_limit;
297
+ const list_fields = this.filter_fields_by_view(this.meta.list_fields, view);
298
+ const { attrs, ref_fields, link_fields } = extract_field_info(
299
+ this.meta.fields_map,
300
+ attr_names,
301
+ list_fields.map(f => f.name)
302
+ );
234
303
 
235
- const search_query = { ...query, ...await this.get_search_query(param_obj) };
236
- if (!search_query) {
237
- if (is_log_error()) {
238
- log_error(LOG_ENTITY, "no search query is set for param:" + JSON.stringify(param_obj));
239
- }
304
+ const page_int = parse_int(page, 1);
305
+ const page_limit = parse_int(limit, 10);
240
306
 
241
- return { code: INVALID_PARAMS, err: "no search query is set" };
307
+ const search_query = await this.get_search_query(param_obj);
308
+ if (search_query === null) {
309
+ log_err('no search query', { param_obj });
310
+ return { code: INVALID_PARAMS, err: 'no search query is set' };
242
311
  }
243
312
 
244
- const total = await this.count(search_query);
245
- const list = await this.find_page(search_query, sort, page_int, page_limit, attrs);
246
- const list_link = await this.read_link_attrs(list, link_fields);
247
- const data = await this.convert_ref_attrs(list_link, ref_fields);
248
-
249
- if (is_log_debug()) {
250
- log_debug(LOG_ENTITY, "total:" + total + ",data:" + JSON.stringify(data));
251
- }
313
+ const merged = { ...(query || {}), ...search_query };
314
+ const total = await this.count(merged);
315
+ const list = await this.find_page(merged, sort, page_int, page_limit, attrs);
316
+ const with_links = await this.read_link_attrs(list, link_fields);
317
+ const data = await this.convert_ref_attrs(with_links, ref_fields);
252
318
 
253
- return { code: SUCCESS, total: total, data: data };
319
+ log_debug(LOG_ENTITY, `total:${total},data:${JSON.stringify(data)}`);
320
+ return { code: SUCCESS, total, data };
254
321
  }
255
322
 
256
323
  /**
257
- * Validate the param object and invoke the logic to save it to db
258
- * @param {param obj from user input} param_obj
259
- * @param {which view to create the entity} view
260
- * @returns object with code and err
261
- */
262
- async create_entity(param_obj, view) {
263
- const fields = view && view !== "*" ? this.meta.create_fields.filter(field => Array.isArray(field.view) ? field.view.includes(view) || field.view.includes("*") : field.view === view || field.view === "*") : this.meta.create_fields;
324
+ * Common logic for create/clone entity operations.
325
+ * @private
326
+ */
327
+ async _save_entity(param_obj, view, options) {
328
+ const { fields_key, before_hook, main_hook, after_hook, id_for_hook } = options;
329
+
330
+ const fields = this.filter_fields_by_view(this.meta[fields_key], view);
264
331
  const { obj, error_field_names } = convert_type(param_obj, fields);
265
332
  if (error_field_names.length > 0) {
266
- if (is_log_error()) {
267
- log_error(LOG_ENTITY, "error fields:" + JSON.stringify(error_field_names));
268
- }
269
-
333
+ log_err('invalid fields', { fields: error_field_names });
270
334
  return { code: INVALID_PARAMS, err: error_field_names };
271
335
  }
272
336
 
273
- if (this.meta.before_create) {
274
- const { code, err } = await this.meta.before_create(this, obj);
275
- if (err || code != SUCCESS) {
276
- if (is_log_error()) {
277
- log_error(LOG_ENTITY, "before_create error:" + JSON.stringify(err) + ", with code:" + code);
278
- }
279
- return { code: code, err: err };
280
- }
281
- }
337
+ // Before hook
338
+ const before_args = id_for_hook ? [id_for_hook, this, obj] : [this, obj];
339
+ const before_err = await run_hook(this.meta[before_hook], before_hook, ...before_args);
340
+ if (before_err) return before_err;
282
341
 
283
- const error_required_field_names = validate_required_fields(obj, this.meta.required_field_names);
284
- if (error_required_field_names.length > 0) {
285
- if (is_log_error()) {
286
- log_error(LOG_ENTITY, "error required fields:" + JSON.stringify(error_required_field_names));
287
- }
288
- return { code: NO_PARAMS, err: error_required_field_names };
342
+ // Validate required fields
343
+ const missing = validate_required_fields(obj, this.meta.required_field_names);
344
+ if (missing.length > 0) {
345
+ log_err('missing required fields', { fields: missing });
346
+ return { code: NO_PARAMS, err: missing };
289
347
  }
290
348
 
291
- const entity_count = await this.count_by_primary_keys(obj);
292
- if (entity_count > 0) {
293
- return { code: DUPLICATE_KEY, err: "entity already exist in db" };
349
+ // Check for duplicates
350
+ if (await this.count_by_primary_keys(obj) > 0) {
351
+ return { code: DUPLICATE_KEY, err: 'entity already exist in db' };
294
352
  }
295
353
 
296
- if (this.meta.ref_fields) {
297
- const { code, err } = await this.validate_ref(obj);
298
- if (err || code != SUCCESS) {
299
- if (is_log_error()) {
300
- log_error(LOG_ENTITY, "validate_ref error:" + JSON.stringify(err) + ", with code:" + code);
301
- }
302
- return { code: code, err: err };
303
- }
304
- }
354
+ // Validate refs
355
+ const ref_err = await validate_refs(this, obj);
356
+ if (ref_err) return ref_err;
305
357
 
306
- if (this.meta.create) {
307
- const { code, err } = await this.meta.create(this, obj);
308
- if (err || code != SUCCESS) {
309
- if (is_log_error()) {
310
- log_error(LOG_ENTITY, "create error:" + JSON.stringify(err) + ", with code:" + code);
311
- }
312
- return { code: code, err: err };
313
- }
358
+ // Main operation
359
+ if (this.meta[main_hook]) {
360
+ const main_args = id_for_hook ? [id_for_hook, this, obj] : [this, obj];
361
+ const main_err = await run_hook(this.meta[main_hook], main_hook, ...main_args);
362
+ if (main_err) return main_err;
314
363
  } else {
315
364
  const db_obj = await this.create(obj);
316
- if (!db_obj["_id"]) {
317
- if (is_log_error()) {
318
- log_error(LOG_ENTITY, "create error:" + JSON.stringify(err) + ", with code:" + code);
319
- }
320
- return { code: ERROR, err: "creating record is failed" };
365
+ if (!db_obj._id) {
366
+ log_err('create failed');
367
+ return { code: ERROR, err: 'creating record is failed' };
321
368
  }
322
369
  }
323
370
 
324
- if (this.meta.after_create) {
325
- const { code, err } = await this.meta.after_create(this, obj);
326
- if (err || code != SUCCESS) {
327
- if (is_log_error()) {
328
- log_error(LOG_ENTITY, "after_create error:" + JSON.stringify(err) + ", with code:" + code);
329
- }
330
- return { code: code, err: err };
331
- }
332
- }
371
+ // After hook
372
+ const after_args = id_for_hook ? [id_for_hook, this, obj] : [this, obj];
373
+ const after_err = await run_hook(this.meta[after_hook], after_hook, ...after_args);
374
+ if (after_err) return after_err;
333
375
 
334
376
  return { code: SUCCESS };
335
377
  }
336
378
 
337
379
  /**
338
- * Validate the param object and invoke the logic to clone the entity and sae it to db
339
- * @param {param obj from user input} param_obj
340
- * @returns object with code and err
341
- */
342
- async clone_entity(_id, param_obj, view) {
343
- const fields = view && view !== "*" ? this.meta.clone_fields.filter(field => Array.isArray(field.view) ? field.view.includes(view) || field.view.includes("*") : field.view === view || field.view === "*") : this.meta.clone_fields;
344
- const { obj, error_field_names } = convert_type(param_obj, fields);
345
- if (error_field_names.length > 0) {
346
- if (is_log_error()) {
347
- log_error(LOG_ENTITY, "error fields:" + JSON.stringify(error_field_names));
348
- }
349
-
350
- return { code: INVALID_PARAMS, err: error_field_names };
351
- }
352
-
353
- if (this.meta.before_clone) {
354
- const { code, err } = await this.meta.before_clone(_id, this, obj);
355
- if (err || code != SUCCESS) {
356
- if (is_log_error()) {
357
- log_error(LOG_ENTITY, "before_clone error:" + JSON.stringify(err) + ", with code:" + code);
358
- }
359
- return { code: code, err: err };
360
- }
361
- }
362
-
363
- const error_required_field_names = validate_required_fields(obj, this.meta.required_field_names);
364
- if (error_required_field_names.length > 0) {
365
- if (is_log_error()) {
366
- log_error(LOG_ENTITY, "error required fields:" + JSON.stringify(error_required_field_names));
367
- }
368
- return { code: NO_PARAMS, err: error_required_field_names };
369
- }
370
-
371
- const entity_count = await this.count_by_primary_keys(obj);
372
- if (entity_count > 0) {
373
- return { code: DUPLICATE_KEY, err: "entity already exist in db" };
374
- }
375
-
376
- if (this.meta.ref_fields) {
377
- const { code, err } = await this.validate_ref(obj);
378
- if (err || code != SUCCESS) {
379
- if (is_log_error()) {
380
- log_error(LOG_ENTITY, "validate_ref error:" + JSON.stringify(err) + ", with code:" + code);
381
- }
382
- return { code: code, err: err };
383
- }
384
- }
385
-
386
- if (this.meta.clone) {
387
- const { code, err } = await this.meta.clone(_id, this, obj);
388
- if (err || code != SUCCESS) {
389
- if (is_log_error()) {
390
- log_error(LOG_ENTITY, "clone error:" + JSON.stringify(err) + ", with code:" + code);
391
- }
392
- return { code: code, err: err };
393
- }
394
- } else {
395
- const db_obj = await this.create(obj);
396
- if (!db_obj["_id"]) {
397
- if (is_log_error()) {
398
- log_error(LOG_ENTITY, "create error:" + JSON.stringify(err) + ", with code:" + code);
399
- }
400
- return { code: ERROR, err: "creating record is failed" };
401
- }
402
- }
403
-
404
- if (this.meta.after_clone) {
405
- const { code, err } = await this.meta.after_clone(_id, this, obj);
406
- if (err || code != SUCCESS) {
407
- if (is_log_error()) {
408
- log_error(LOG_ENTITY, "after_clone error:" + JSON.stringify(err) + ", with code:" + code);
409
- }
410
- return { code: code, err: err };
411
- }
412
- }
413
-
414
- return { code: SUCCESS };
380
+ * Create a new entity.
381
+ * @param {Object} param_obj - Entity data
382
+ * @param {string} view - View filter
383
+ * @returns {Object} Result with code
384
+ */
385
+ async create_entity(param_obj, view) {
386
+ return this._save_entity(param_obj, view, {
387
+ fields_key: 'create_fields',
388
+ before_hook: 'before_create',
389
+ main_hook: 'create',
390
+ after_hook: 'after_create'
391
+ });
415
392
  }
416
393
 
394
+ /**
395
+ * Clone an existing entity.
396
+ * @param {string} _id - Source entity ID
397
+ * @param {Object} param_obj - New entity data
398
+ * @param {string} view - View filter
399
+ * @returns {Object} Result with code
400
+ */
401
+ async clone_entity(_id, param_obj, view) {
402
+ return this._save_entity(param_obj, view, {
403
+ fields_key: 'clone_fields',
404
+ before_hook: 'before_clone',
405
+ main_hook: 'clone',
406
+ after_hook: 'after_clone',
407
+ id_for_hook: _id
408
+ });
409
+ }
417
410
 
418
411
  /**
419
- * Validate the param object and invoke the logic to update entity
420
- * @param {object id of the entity} _id object id of the entity, if it is null, then use primary key
421
- * @param {param object from user input} param_obj
422
- * @param {which view to update the entity} view
423
- *
412
+ * Update an existing entity.
413
+ * @param {string} _id - Entity ID (null to use primary key)
414
+ * @param {Object} param_obj - Update data
415
+ * @param {string} view - View filter
416
+ * @returns {Object} Result with code
424
417
  */
425
418
  async update_entity(_id, param_obj, view) {
426
- const fields = view && view !== "*" ? this.meta.update_fields.filter(field => Array.isArray(field.view) ? field.view.includes(view) || field.view.includes("*") : field.view === view || field.view === "*") : this.meta.update_fields;
427
-
419
+ const fields = this.filter_fields_by_view(this.meta.update_fields, view);
428
420
  const { obj, error_field_names } = convert_update_type(param_obj, fields);
429
421
  if (error_field_names.length > 0) {
430
- if (is_log_error()) {
431
- log_error(LOG_ENTITY, "update_entity error fields:" + JSON.stringify(error_field_names));
432
- }
422
+ log_err('update_entity invalid fields', { fields: error_field_names });
433
423
  return { code: INVALID_PARAMS, err: error_field_names };
434
424
  }
435
425
 
436
- if (this.meta.before_update) {
437
- const { code, err } = await this.meta.before_update(_id, this, obj);
438
- if (err || code != SUCCESS) {
439
- if (is_log_error()) {
440
- log_error(LOG_ENTITY, "before_update error:" + JSON.stringify(err) + ", with code:" + code);
441
- }
442
- return { code: code, err: err };
443
- }
444
- }
426
+ const before_err = await run_hook(this.meta.before_update, 'before_update', _id, this, obj);
427
+ if (before_err) return before_err;
445
428
 
446
429
  const query = _id ? oid_query(_id) : this.primary_key_query(obj);
447
- if (query == null) {
448
- if (is_log_error()) {
449
- log_error(LOG_ENTITY, "error query _id:" + _id + ", with obj:" + JSON.stringify(obj));
450
- }
451
- return { code: INVALID_PARAMS, err: _id ? ["_id"] : this.meta.primary_keys };
430
+ if (!query) {
431
+ log_err('invalid query', { _id, obj });
432
+ return { code: INVALID_PARAMS, err: _id ? ['_id'] : this.meta.primary_keys };
452
433
  }
453
434
 
454
- const total = await this.count(query);
455
- if (total != 1) {
456
- if (is_log_error()) {
457
- log_error(LOG_ENTITY, "update_entity not found with query:" + JSON.stringify(query) + ", and total:" + total);
458
- }
459
- return { code: NOT_FOUND, err: _id ? ["_id"] : this.meta.primary_keys };
435
+ if (await this.count(query) !== 1) {
436
+ log_err('entity not found', { query });
437
+ return { code: NOT_FOUND, err: _id ? ['_id'] : this.meta.primary_keys };
460
438
  }
461
439
 
462
- if (this.meta.ref_fields) {
463
- const { code, err } = await this.validate_ref(obj);
464
- if (err || code != SUCCESS) {
465
- if (is_log_error()) {
466
- log_error(LOG_ENTITY, "update_entity validate_ref error:" + JSON.stringify(err) + ", with code:" + code);
467
- }
468
- return { code: code, err: err };
469
- }
470
- }
440
+ const ref_err = await validate_refs(this, obj);
441
+ if (ref_err) return ref_err;
471
442
 
472
443
  if (this.meta.update) {
473
- const { code, err } = await this.meta.update(_id, this, obj);
474
- if (err || code != SUCCESS) {
475
- if (is_log_error()) {
476
- log_error(LOG_ENTITY, "meta update error:" + JSON.stringify(err) + ", with code:" + code + ",_id:" + _id + ",obj:" + JSON.stringify(obj));
477
- }
478
- return { code: code, err: err };
479
- }
444
+ const update_err = await run_hook(this.meta.update, 'update', _id, this, obj);
445
+ if (update_err) return update_err;
480
446
  } else {
481
447
  const result = await this.update(query, obj);
482
- if (result.ok != 1) {
483
- if (is_log_error()) {
484
- log_error(LOG_ENTITY, "update record is failed with query:" + JSON.stringify(query) + ",obj:" + JSON.stringify(obj) + ",result:" + JSON.stringify(result));
485
- }
486
- return { code: ERROR, err: "update record is failed" };
448
+ if (result.ok !== 1) {
449
+ log_err('update failed', { query, obj, result });
450
+ return { code: ERROR, err: 'update record is failed' };
487
451
  }
488
452
  }
489
453
 
490
- if (this.meta.after_update) {
491
- const { code, err } = await this.meta.after_update(_id, this, obj);
492
- if (err || code != SUCCESS) {
493
- if (is_log_error()) {
494
- log_error(LOG_ENTITY, "after_update is failed with _id:" + JSON.stringify(_id) + ",obj:" + JSON.stringify(obj) + ",err:" + JSON.stringify(err) + ",code:" + code);
495
- }
496
- return { code: code, err: err };
497
- }
498
- }
454
+ const after_err = await run_hook(this.meta.after_update, 'after_update', _id, this, obj);
455
+ if (after_err) return after_err;
499
456
 
500
457
  return { code: SUCCESS };
501
458
  }
502
459
 
503
460
  /**
504
- * Validate the param object and invoke the logic to batch update entity
505
- * @param {object id array of the entity} _ids
506
- * @param {param object from user input} param_obj
507
- *
461
+ * Batch update multiple entities.
462
+ * @param {string[]} _ids - Entity IDs
463
+ * @param {Object} param_obj - Update data
464
+ * @param {string} view - View filter
465
+ * @returns {Object} Result with code
508
466
  */
509
467
  async batch_update_entity(_ids, param_obj, view) {
510
- const update_fields = view && view !== "*" ? this.meta.update_fields.filter(field => Array.isArray(field.view) ? this.contain_view(field.view, view) || field.view.includes("*") : view.includes(field.view) || field.view === "*") : this.meta.update_fields;
511
- const { obj, error_field_names } = convert_update_type(param_obj, update_fields);
468
+ const fields = this.filter_fields_by_view(this.meta.update_fields, view);
469
+ const { obj, error_field_names } = convert_update_type(param_obj, fields);
512
470
  if (error_field_names.length > 0) {
513
- if (is_log_error()) {
514
- log_error(LOG_ENTITY, "batch_update_entity error fields:" + JSON.stringify(error_field_names));
515
- }
471
+ log_err('batch_update invalid fields', { fields: error_field_names });
516
472
  return { code: INVALID_PARAMS, err: error_field_names };
517
473
  }
518
474
 
519
475
  const query = oid_queries(_ids);
520
- if (query == null) {
521
- if (is_log_error()) {
522
- log_error(LOG_ENTITY, "batch_update_entity invalid ids:" + JSON.stringify(_ids));
523
- }
524
- return { code: INVALID_PARAMS, err: ["_ids"] };
476
+ if (!query) {
477
+ log_err('batch_update invalid ids', { _ids });
478
+ return { code: INVALID_PARAMS, err: ['_ids'] };
525
479
  }
526
480
 
527
- if (this.meta.ref_fields) {
528
- const { code, err } = await this.validate_ref(obj);
529
- if (err || code != SUCCESS) {
530
- if (is_log_error()) {
531
- log_error(LOG_ENTITY, "batch_update_entity validate_ref error:" + JSON.stringify(err) + ", with code:" + code);
532
- }
533
- return { code: code, err: err };
534
- }
535
- }
481
+ const ref_err = await validate_refs(this, obj);
482
+ if (ref_err) return ref_err;
536
483
 
537
484
  if (this.meta.batch_update) {
538
- const { code, err } = await this.meta.batch_update(_ids, this, obj);
539
- if (err || code != SUCCESS) {
540
- if (is_log_error()) {
541
- log_error(LOG_ENTITY, "batch_update_entity batch_update error:" + JSON.stringify(err) + ", with code:" + code);
542
- }
543
- return { code: code, err: err };
544
- }
485
+ const batch_err = await run_hook(this.meta.batch_update, 'batch_update', _ids, this, obj);
486
+ if (batch_err) return batch_err;
545
487
  } else {
546
488
  const result = await this.update(query, obj);
547
- if (result.ok != 1) {
548
- if (is_log_error()) {
549
- log_error(LOG_ENTITY, "batch_update_entity update record is failed with query:" + JSON.stringify(query) + ",obj:" + JSON.stringify(obj) + ",result:" + JSON.stringify(result));
550
- }
551
- return { code: ERROR, err: "batch update record is failed" };
489
+ if (result.ok !== 1) {
490
+ log_err('batch update failed', { query, obj, result });
491
+ return { code: ERROR, err: 'batch update record is failed' };
552
492
  }
553
493
  }
554
494
 
555
- if (this.meta.after_batch_update) {
556
- const { code, err } = await this.meta.after_batch_update(_ids, this, obj);
557
- if (err || code != SUCCESS) {
558
- if (is_log_error()) {
559
- log_error(LOG_ENTITY, "after_batch_update error:" + JSON.stringify(err) + ", with code:" + code);
560
- }
561
- return { code: code, err: err };
562
- }
563
- }
495
+ const after_err = await run_hook(this.meta.after_batch_update, 'after_batch_update', _ids, this, obj);
496
+ if (after_err) return after_err;
564
497
 
565
498
  return { code: SUCCESS };
566
499
  }
567
500
 
568
- contain_view(array, view) {
569
- for (let i = 0; i < array.length; i++) {
570
- if (view.includes(array[i])) {
571
- return true;
501
+ /**
502
+ * Filter fields by view.
503
+ * @param {Object[]} fields - Fields to filter
504
+ * @param {string} view - View name
505
+ * @returns {Object[]} Filtered fields
506
+ */
507
+ filter_fields_by_view(fields, view) {
508
+ if (!view || view === '*') return fields;
509
+
510
+ return fields.filter(field => {
511
+ const fv = field.view;
512
+ if (Array.isArray(fv)) {
513
+ return fv.includes('*') || fv.some(v => view.includes(v));
572
514
  }
573
- }
574
- return false;
515
+ if (typeof fv === 'string') {
516
+ return fv === '*' || view.includes(fv);
517
+ }
518
+ return true;
519
+ });
575
520
  }
576
521
 
577
522
  /**
578
- * Use objectid to read entity properties. Validate the param object and invoke the logic to read entity properties.
579
- * This method doesn't convert ref property, so all the ref properties are objectid of the ref entity.
580
- * It also donesn't inclue link property. This is used for form view to do create/update the entity.
581
- * @param {object id of the entity} _id object id of the entity
582
- * @param {attr names to retrieve} attr_names
583
- *
523
+ * Read entity properties without ref conversion.
524
+ * @param {string} _id - Entity ID
525
+ * @param {string} attr_names - Comma-separated attribute names
526
+ * @param {string} view - View filter
527
+ * @returns {Object} Result with code and data
584
528
  */
585
529
  async read_property(_id, attr_names, view) {
586
530
  const query = oid_query(_id);
587
- if (query == null) {
588
- if (is_log_error()) {
589
- log_error(LOG_ENTITY, "read_property invalid id:" + _id);
590
- }
591
- return { code: INVALID_PARAMS, err: ["_id"] };
531
+ if (!query) {
532
+ log_err('read_property invalid id', { _id });
533
+ return { code: INVALID_PARAMS, err: ['_id'] };
592
534
  }
593
535
 
594
- const property_fields = view && view !== "*" ? this.meta.property_fields.filter(field => Array.isArray(field.view) ? this.contain_view(field.view, view) || field.view.includes("*") : view.includes(field.view) || field.view === "*") : this.meta.property_fields;
536
+ const property_fields = this.filter_fields_by_view(this.meta.property_fields, view);
595
537
  const field_names = property_fields.map(f => f.name);
596
538
  const attrs = { _id: 1 };
597
- attr_names.split(",").forEach((attr) => {
598
- if (field_names.includes(attr)) {
599
- attrs[attr] = 1;
600
- }
539
+ attr_names.split(',').forEach(attr => {
540
+ if (field_names.includes(attr)) attrs[attr] = 1;
601
541
  });
602
542
 
603
543
  const results = await this.find(query, attrs);
604
- if (results && results.length == 1) {
605
- if (is_log_debug()) {
606
- log_debug("read_property with query:" + JSON.stringify(query) + ",attrs:" + JSON.stringify(attrs) + ",result:" + JSON.stringify(results));
607
- }
544
+ if (results?.length === 1) {
545
+ log_debug(LOG_ENTITY, `read_property query:${JSON.stringify(query)},result:${JSON.stringify(results[0])}`);
608
546
  return { code: SUCCESS, data: results[0] };
609
- } else {
610
- return { code: NOT_FOUND, err: ["_id"] };
611
547
  }
548
+ return { code: NOT_FOUND, err: ['_id'] };
612
549
  }
613
550
 
614
551
  /**
615
- * Use objectid to read entity properties. Validate the param object and invoke the logic to read entity properties.
616
- * It will convert object ref attributes to ref_label property of the ref entity and also read link attributes.
617
- * @param {object id of the entity} _id object id of the entity
618
- * @param {attr names to retrieve} attr_names
619
- *
552
+ * Read entity with ref conversion and links.
553
+ * @param {string} _id - Entity ID
554
+ * @param {string} attr_names - Comma-separated attribute names
555
+ * @param {string} view - View filter
556
+ * @returns {Object} Result with code and data
620
557
  */
621
558
  async read_entity(_id, attr_names, view) {
622
559
  const query = oid_query(_id);
623
- if (query == null) {
624
- if (is_log_error()) {
625
- log_error(LOG_ENTITY, "read_entity invalid id:" + _id);
626
- }
627
- return { code: INVALID_PARAMS, err: ["_id"] };
560
+ if (!query) {
561
+ log_err('read_entity invalid id', { _id });
562
+ return { code: INVALID_PARAMS, err: ['_id'] };
628
563
  }
629
564
 
630
565
  if (!attr_names) {
631
- if (is_log_error()) {
632
- log_error(LOG_ENTITY, "read_entity invalid attr_names:" + attr_names);
633
- }
634
- return { code: INVALID_PARAMS, err: ["attr_names"] };
566
+ log_err('read_entity invalid attr_names', { attr_names });
567
+ return { code: INVALID_PARAMS, err: ['attr_names'] };
635
568
  }
636
569
 
637
- const property_fields = view && view !== "*" ? this.meta.property_fields.filter(field => Array.isArray(field.view) ? this.contain_view(field.view, view) || field.view.includes("*") : view.includes(field.view) || field.view === "*") : this.meta.property_fields;
638
- const field_names = property_fields.map(f => f.name);
639
-
640
- const ref_fields = [];
641
- const link_fields = [];
642
- const attrs = { _id: 1 };
643
-
644
- const fields_map = this.meta.fields_map;
645
- attr_names.split(",").forEach((attr) => {
646
- if (field_names.includes(attr)) {
647
- attrs[attr] = 1;
648
- const field = fields_map[attr];
649
- if (field.link) {
650
- link_fields.push(field);
651
- attrs[field.link] = 1;
652
- } else if (field.ref) {
653
- ref_fields.push(field);
654
- }
655
- }
656
- });
570
+ const property_fields = this.filter_fields_by_view(this.meta.property_fields, view);
571
+ const { attrs, ref_fields, link_fields } = extract_field_info(
572
+ this.meta.fields_map,
573
+ attr_names,
574
+ property_fields.map(f => f.name)
575
+ );
657
576
 
658
577
  const results = await this.find(query, attrs);
659
- if (results && results.length == 1) {
660
- if (this.meta.after_read) {
661
- const { code, err } = await this.meta.after_read(_id, this, attr_names, results[0]);
662
- if (err || code != SUCCESS) {
663
- if (is_log_error()) {
664
- log_error(LOG_ENTITY, "after_read error:" + JSON.stringify(err) + ", with code:" + code);
665
- }
666
- return { code: code, err: err };
667
- }
668
- }
578
+ if (results?.length !== 1) {
579
+ return { code: NOT_FOUND, err: ['_id'] };
580
+ }
669
581
 
670
- const list_link = await this.read_link_attrs(results, link_fields);
671
- const converted = await this.convert_ref_attrs(list_link, ref_fields);
582
+ const after_err = await run_hook(this.meta.after_read, 'after_read', _id, this, attr_names, results[0]);
583
+ if (after_err) return after_err;
672
584
 
673
- if (converted && converted.length == 1) {
674
- if (is_log_debug()) {
675
- log_debug("read_entity with query:" + JSON.stringify(query) + ",attrs:" + JSON.stringify(attrs) + ",converted:" + JSON.stringify(converted));
676
- }
677
- return { code: SUCCESS, data: converted[0] };
678
- }
679
- }
585
+ const with_links = await this.read_link_attrs(results, link_fields);
586
+ const converted = await this.convert_ref_attrs(with_links, ref_fields);
680
587
 
681
- return { code: NOT_FOUND, err: ["_id"] };
588
+ if (converted?.length === 1) {
589
+ log_debug(LOG_ENTITY, `read_entity query:${JSON.stringify(query)},result:${JSON.stringify(converted[0])}`);
590
+ return { code: SUCCESS, data: converted[0] };
591
+ }
592
+ return { code: NOT_FOUND, err: ['_id'] };
682
593
  }
683
594
 
684
595
  /**
685
- * Delete the objects using id array
686
- * @param {array of objectid} id_array
596
+ * Delete entities by ID array.
597
+ * @param {string[]} id_array - Entity IDs
598
+ * @returns {Object} Result with code
687
599
  */
688
600
  async delete_entity(id_array) {
689
601
  const query = oid_queries(id_array);
690
- if (query == null) {
691
- if (is_log_error()) {
692
- log_error(LOG_ENTITY, "delete_entity invalid id_array:" + JSON.stringify(id_array));
693
- }
694
- return { code: INVALID_PARAMS, err: ["ids"] };
602
+ if (!query) {
603
+ log_err('delete_entity invalid ids', { id_array });
604
+ return { code: INVALID_PARAMS, err: ['ids'] };
695
605
  }
696
606
 
697
- if (this.meta.before_delete) {
698
- const { code, err } = await this.meta.before_delete(this, id_array);
699
- if (err || code != SUCCESS) {
700
- if (is_log_error()) {
701
- log_error(LOG_ENTITY, "before_delete error:" + JSON.stringify(err) + ", with code:" + code);
702
- }
703
- return { code: code, err: err };
704
- }
705
- }
607
+ const before_err = await run_hook(this.meta.before_delete, 'before_delete', this, id_array);
608
+ if (before_err) return before_err;
706
609
 
707
610
  if (this.meta.delete) {
708
- const { code, err } = await this.meta.delete(this, id_array);
709
- if (err || code != SUCCESS) {
710
- if (is_log_error()) {
711
- log_error(LOG_ENTITY, "delete error:" + JSON.stringify(err) + ", with code:" + code);
712
- }
713
- return { code: code, err: err };
714
- }
611
+ const delete_err = await run_hook(this.meta.delete, 'delete', this, id_array);
612
+ if (delete_err) return delete_err;
715
613
  } else {
716
- //check all the ref by array first
717
- const has_refer_by_array = await this.check_refer_entity(id_array);
718
- if (has_refer_by_array.length > 0) {
719
- const array = [...new Set(has_refer_by_array)];
720
- if (is_log_error()) {
721
- log_error(LOG_ENTITY, "has_refer_by_array:" + JSON.stringify(array));
722
- }
723
- return { code: HAS_REF, err: array };
614
+ // Check references
615
+ const refs = await this.check_refer_entity(id_array);
616
+ if (refs.length > 0) {
617
+ const unique_refs = [...new Set(refs)];
618
+ log_err('has references', { refs: unique_refs });
619
+ return { code: HAS_REF, err: unique_refs };
724
620
  }
725
621
 
726
622
  const result = await this.delete(query);
727
- if (result.ok != 1) {
728
- if (is_log_error()) {
729
- log_error(LOG_ENTITY, "delete records is failed with query:" + JSON.stringify(query) + ", result:" + JSON.stringify(result));
730
- }
731
- return { code: ERROR, err: "delete record is failed" };
732
- }
733
- //delete other ref_by entity based on delete mode
734
- for (let i = 0; i < this.meta.ref_by_metas.length; i++) {
735
- const ref_by_meta = this.meta.ref_by_metas[i];
736
- const ref_fields = ref_by_meta.ref_fields.filter(field => field.ref == this.meta.collection);
737
- for (let j = 0; j < ref_fields.length; j++) {
738
- const ref_field = ref_fields[j];
739
- if (ref_field.delete == DELETE_MODE.cascade) {
740
- const refer_by_entity = new Entity(ref_by_meta);
741
- await refer_by_entity.delete_refer_entity(ref_field.name, id_array)
623
+ if (result.ok !== 1) {
624
+ log_err('delete failed', { query, result });
625
+ return { code: ERROR, err: 'delete record is failed' };
626
+ }
627
+
628
+ // Cascade delete
629
+ for (const ref_by_meta of this.meta.ref_by_metas) {
630
+ const ref_fields = ref_by_meta.ref_fields.filter(f => f.ref === this.meta.collection);
631
+ for (const field of ref_fields) {
632
+ if (field.delete === DELETE_MODE.cascade) {
633
+ const ref_entity = new Entity(ref_by_meta);
634
+ await ref_entity.delete_refer_entity(field.name, id_array);
742
635
  }
743
636
  }
744
637
  }
745
638
  }
746
639
 
747
- if (this.meta.after_delete) {
748
- const { code, err } = await this.meta.after_delete(this, id_array);
749
- if (err || code != SUCCESS) {
750
- if (is_log_error()) {
751
- log_error(LOG_ENTITY, "after_delete error:" + JSON.stringify(err) + ", with code:" + code);
752
- }
753
- return { code: code, err: err };
754
- }
755
- }
640
+ const after_err = await run_hook(this.meta.after_delete, 'after_delete', this, id_array);
641
+ if (after_err) return after_err;
756
642
 
757
643
  return { code: SUCCESS };
758
644
  }
759
645
 
760
646
  /**
761
- * Construct the primary key query object
762
- * @param {param object} param_obj
763
- * @returns
647
+ * Build primary key query.
648
+ * @param {Object} param_obj - Parameters
649
+ * @returns {Object|null} Query object or null
764
650
  */
765
651
  primary_key_query(param_obj) {
766
- const params = required_params(param_obj, this.meta.primary_keys);
767
- if (params === null) {
768
- return null;
769
- }
652
+ if (!required_params(param_obj, this.meta.primary_keys)) return null;
770
653
 
771
654
  const { obj, error_field_names } = convert_type(param_obj, this.meta.primary_key_fields);
772
- if (error_field_names.length > 0) {
773
- return null;
774
- }
655
+ if (error_field_names.length > 0) return null;
775
656
 
776
- const query = {};
777
- this.meta.primary_keys.forEach(function (key) {
778
- query[key] = obj[key];
779
- });
780
- return query;
657
+ return this.meta.primary_keys.reduce((q, key) => ({ ...q, [key]: obj[key] }), {});
781
658
  }
782
659
 
783
- /**
784
- * Get the count value by primary key
785
- * @param {object used to create query} obj
786
- * @returns the count value by primary key
787
- */
660
+ /** Count by primary key. */
788
661
  count_by_primary_keys(obj) {
789
662
  return this.count(this.primary_key_query(obj));
790
663
  }
791
664
 
792
- /**
793
- * Insert Object to db
794
- * @param {inserted object} obj
795
- * @returns
796
- */
797
- create(obj) {
798
- return this.db.create(this.meta.collection, obj);
799
- }
800
-
801
- /**
802
- * Update the object, upsert:true, multi:true
803
- * @param {*} query
804
- * @param {*} obj
805
- * @returns
806
- */
807
- update(query, obj) {
808
- return this.db.update(this.meta.collection, query, obj);
809
- }
665
+ // Database operations (delegate to db)
666
+ create(obj) { return this.db.create(this.meta.collection, obj); }
667
+ update(query, obj) { return this.db.update(this.meta.collection, query, obj); }
668
+ delete(query) { return this.db.delete(this.meta.collection, query); }
669
+ find(query, attr) { return this.db.find(this.meta.collection, query, attr); }
670
+ find_one(query, attr) { return this.db.find_one(this.meta.collection, query, attr); }
671
+ find_sort(query, sort, attr) { return this.db.find_sort(this.meta.collection, query, sort, attr); }
672
+ find_page(query, sort, page, limit, attr) { return this.db.find_page(this.meta.collection, query, sort, page, limit, attr); }
673
+ count(query) { return this.db.count(this.meta.collection, query); }
674
+ sum(query, field) { return this.db.sum(this.meta.collection, query, field); }
675
+ pull(query, ele) { return this.db.pull(this.meta.collection, query, ele); }
676
+ push(query, ele) { return this.db.push(this.meta.collection, query, ele); }
677
+ add_to_set(query, ele) { return this.db.add_to_set(this.meta.collection, query, ele); }
810
678
 
811
679
  /**
812
- * Remove the records from mongodb
813
- * @param {query to execute delete op} query
814
- * @returns
815
- */
816
- delete(query) {
817
- return this.db.delete(this.meta.collection, query);
818
- }
819
-
820
- /**
821
- * Remove the records from mongodb
822
- * @param {*} id can be array or string
823
- * @returns
680
+ * Delete by ID (single or array).
681
+ * @param {string|string[]} id - ID or array of IDs
824
682
  */
825
683
  delete_by_id(id) {
826
684
  const query = Array.isArray(id) ? oid_queries(id) : oid_query(id);
@@ -828,242 +686,182 @@ class Entity {
828
686
  }
829
687
 
830
688
  /**
831
- * Find the objects that are refered by other entity
832
- * @param {ref value} value
833
- * @param {the attributes to load from db} attr
834
- * @returns array of the objects that are found
689
+ * Find by ref value (objectid or ref_label).
690
+ * @param {*} value - Ref value
691
+ * @param {Object} attr - Attributes to load
692
+ * @param {string} ref_by_entity - Referring entity
693
+ * @returns {Promise<Object[]>} Found entities
835
694
  */
836
695
  find_by_ref_value(value, attr, ref_by_entity) {
837
696
  let query = Array.isArray(value) ? oid_queries(value) : oid_query(value);
838
- if (query == null) {
697
+
698
+ if (!query) {
699
+ const ref_label = this.meta.ref_label;
839
700
  if (Array.isArray(value)) {
840
- query = { [this.meta.ref_label]: { "$in": value } };
701
+ query = { [ref_label]: { $in: value } };
702
+ } else if (value.includes(',')) {
703
+ query = { [ref_label]: { $in: value.split(',') } };
841
704
  } else {
842
- if (value.includes(",")) {
843
- const values = value.split(",");
844
- query = { [this.meta.ref_label]: { "$in": values } };
845
- } else {
846
- query = { [this.meta.ref_label]: value };
847
- }
848
- }
849
- }
850
-
851
- if (this.meta.ref_filter && ref_by_entity) {
852
- if (this.meta.ref_filter[ref_by_entity]) {
853
- query = { ...query, ...this.meta.ref_filter[ref_by_entity] };
854
- } else if (this.meta.ref_filter["*"]) {
855
- query = { ...query, ...this.meta.ref_filter["*"] };
705
+ query = { [ref_label]: value };
856
706
  }
857
707
  }
858
708
 
859
- return this.find(query, attr);
709
+ return this.find(apply_ref_filter(query, this.meta.ref_filter, ref_by_entity), attr);
860
710
  }
861
711
 
862
712
  /**
863
- * Get the ref entity by the ref value
864
- * @param {field name of the ref} field_name
865
- * @param {the value of the field} value
866
- * @param {which attrs to show} attr
867
- * @returns
713
+ * Find one ref entity by field name and value.
714
+ * @param {string} field_name - Field name
715
+ * @param {*} value - Value
716
+ * @param {Object} attr - Attributes to load
717
+ * @returns {Promise<Object>} Found entity
868
718
  */
869
719
  find_one_ref_entity(field_name, value, attr) {
870
- const fields = this.meta.fields.filter(field => field.name == field_name);
871
- if (fields.length == 1) {
872
- const field = fields[0];
873
- if (field.ref) {
874
- const ref_meta = get_entity_meta(field.ref);
875
- const ref_entity = new Entity(ref_meta);
876
- let query = oid_query(value);
877
- if (query == null) {
878
- query = { [ref_meta.ref_label]: value };
879
- }
880
- return ref_entity.find_one(query, attr);
881
-
882
- } else {
883
- throw new Error("the field:" + field_name + " is not ref field,in entity:" + this.meta.collection);
884
- }
885
- } else {
886
- throw new Error("not found the field by name:" + field_name + ",in entity:" + this.meta.collection);
720
+ const field = this.meta.fields.find(f => f.name === field_name);
721
+ if (!field) {
722
+ throw new Error(`field not found: ${field_name} in ${this.meta.collection}`);
723
+ }
724
+ if (!field.ref) {
725
+ throw new Error(`field is not ref: ${field_name} in ${this.meta.collection}`);
887
726
  }
727
+
728
+ const ref_meta = get_entity_meta(field.ref);
729
+ const ref_entity = new Entity(ref_meta);
730
+ const query = oid_query(value) || { [ref_meta.ref_label]: value };
731
+ return ref_entity.find_one(query, attr);
888
732
  }
889
733
 
890
734
  /**
891
- *
892
- * @param {the id array used to check} id_array
893
- * @returns refer by entities
735
+ * Check for referring entities.
736
+ * @param {string[]} id_array - Entity IDs
737
+ * @returns {Promise<string[]>} Reference descriptions
894
738
  */
895
739
  async check_refer_entity(id_array) {
896
- const has_refer_by_array = [];
897
- for (let i = 0; i < this.meta.ref_by_metas.length; i++) {
898
- const ref_by_meta = this.meta.ref_by_metas[i];
899
- const refer_by_entity = new Entity(ref_by_meta);
900
- const ref_fields = ref_by_meta.ref_fields.filter(field => field.ref == this.meta.collection);
901
-
902
- for (let j = 0; j < ref_fields.length; j++) {
903
- const ref_field = ref_fields[j];
904
- if (ref_field.delete != DELETE_MODE.keep) {
905
- const attr = {};
906
- if (ref_by_meta.ref_label) {
907
- attr[ref_by_meta.ref_label] = 1;
908
- }
740
+ const refs = [];
909
741
 
910
- const entities = await refer_by_entity.get_refer_entities(ref_field.name, id_array, attr);
911
- if (entities && entities.length > 0) {
912
- if (ref_field.delete == DELETE_MODE.cascade) {
913
- const ref_array = await refer_by_entity.check_refer_entity(entities.map(o => o._id + ""));
914
- if (ref_array && ref_array.length > 0) {
915
- has_refer_by_array.push(...ref_array);
916
- }
917
- } else {
918
- if (ref_by_meta.ref_label) {
919
- has_refer_by_array.push(...entities.map(o => this.meta.collection + "<-" + ref_by_meta.collection + ":" + o[ref_by_meta.ref_label]));
920
- } else {
921
- has_refer_by_array.push(...entities.map(o => this.meta.collection + "<-" + ref_by_meta.collection + ":" + o["_id"] + ""));
922
- }
923
- }
924
- }
742
+ for (const ref_by_meta of this.meta.ref_by_metas) {
743
+ const ref_entity = new Entity(ref_by_meta);
744
+ const ref_fields = ref_by_meta.ref_fields.filter(f => f.ref === this.meta.collection);
745
+
746
+ for (const field of ref_fields) {
747
+ if (field.delete === DELETE_MODE.keep) continue;
748
+
749
+ const attr = ref_by_meta.ref_label ? { [ref_by_meta.ref_label]: 1 } : {};
750
+ const entities = await ref_entity.get_refer_entities(field.name, id_array, attr);
751
+
752
+ if (!entities?.length) continue;
753
+
754
+ if (field.delete === DELETE_MODE.cascade) {
755
+ const cascade_refs = await ref_entity.check_refer_entity(entities.map(o => `${o._id}`));
756
+ if (cascade_refs?.length) refs.push(...cascade_refs);
757
+ } else {
758
+ const label_key = ref_by_meta.ref_label || '_id';
759
+ refs.push(...entities.map(o => `${this.meta.collection}<-${ref_by_meta.collection}:${o[label_key]}`));
925
760
  }
926
761
  }
927
762
  }
928
- return has_refer_by_array;
763
+ return refs;
929
764
  }
930
765
 
931
- /**
932
- * check whether this entity has refered the entity_id value
933
- * @param {entity in this field name} field_name
934
- * @param {entity object id array} id_array
935
- * @returns true if has refered
936
- */
766
+ /** Get entities referring to given IDs. */
937
767
  async get_refer_entities(field_name, id_array, attr) {
938
- const query = { [field_name]: { "$in": id_array } };
939
- return await this.find(query, attr);
768
+ return this.find({ [field_name]: { $in: id_array } }, attr);
940
769
  }
941
770
 
942
- /**
943
- * delete refer entities
944
- * @param {entity in this field name} field_name
945
- * @param {entity object id array} id_array
946
- */
771
+ /** Delete entities referring to given IDs. */
947
772
  async delete_refer_entity(field_name, id_array) {
948
773
  const entities = await this.get_refer_entities(field_name, id_array, {});
949
- await this.delete_entity(entities.map(o => o._id + ""));
774
+ await this.delete_entity(entities.map(o => `${o._id}`));
950
775
  }
951
776
 
952
777
  /**
953
- * Convert ref element object id to ref_label
954
- * @param {element of object} elements
955
- * @param {*} passed_ref_fields, if not pass, use all meta ref_fields
956
- * @returns
778
+ * Convert ref objectids to ref_labels.
779
+ * @param {Object[]} elements - Elements to convert
780
+ * @param {Object[]} ref_fields - Ref fields configuration
781
+ * @returns {Promise<Object[]>} Converted elements
957
782
  */
958
- async convert_ref_attrs(elements, passed_ref_fields) {
959
- const ref_fields = passed_ref_fields ? passed_ref_fields : this.meta.ref_fields;
960
- if (elements && ref_fields && ref_fields.length > 0) {
961
- for (let i = 0; i < ref_fields.length; i++) {
962
- const ref_field = ref_fields[i];
963
- let id_array = [];
964
- for (let j = 0; j < elements.length; j++) {
965
- const obj = elements[j];
966
- const value = obj[ref_field.name];
967
- if (Array.isArray(value)) {
968
- id_array = id_array.concat(value);
969
- } else if (value) {
970
- id_array.push(value);
971
- }
972
- }
973
- id_array = unique(id_array);
974
-
975
- const ref_meta = get_entity_meta(ref_field.ref);
976
- const ref_entity = new Entity(ref_meta);
977
- const ref_labels = await ref_entity.get_ref_labels(id_array);
978
- const id_key = "_id";
979
- const label_map_obj = map_array_to_obj(ref_labels, id_key, ref_meta.ref_label);
980
- for (let j = 0; j < elements.length; j++) {
981
- const obj = elements[j];
982
- const value = obj[ref_field.name];
983
- obj[ref_field.name + id_key] = value;
984
-
985
- if (Array.isArray(value)) {
986
- obj[ref_field.name] = value.map(v => label_map_obj[v]);
987
- } else if (value) {
988
- obj[ref_field.name] = label_map_obj[value];
989
- }
990
- }
783
+ async convert_ref_attrs(elements, ref_fields = this.meta.ref_fields) {
784
+ if (!elements?.length || !ref_fields?.length) return elements;
785
+
786
+ for (const field of ref_fields) {
787
+ // Collect all IDs
788
+ let ids = [];
789
+ for (const obj of elements) {
790
+ const value = obj[field.name];
791
+ if (Array.isArray(value)) ids.push(...value);
792
+ else if (value) ids.push(value);
793
+ }
794
+ ids = unique(ids);
795
+
796
+ // Get labels
797
+ const ref_meta = get_entity_meta(field.ref);
798
+ const ref_entity = new Entity(ref_meta);
799
+ const labels = await ref_entity.get_ref_labels(ids);
800
+ const label_map = map_array_to_obj(labels, '_id', ref_meta.ref_label);
801
+
802
+ // Apply labels
803
+ for (const obj of elements) {
804
+ const value = obj[field.name];
805
+ obj[`${field.name}_id`] = value;
806
+ obj[field.name] = Array.isArray(value)
807
+ ? value.map(v => label_map[v])
808
+ : (value ? label_map[value] : value);
991
809
  }
992
810
  }
993
811
  return elements;
994
812
  }
995
813
 
996
814
  /**
997
- * read the link attrs
998
- * @param {*} elements
999
- * @returns
815
+ * Read link attributes.
816
+ * @param {Object[]} elements - Elements to process
817
+ * @param {Object[]} link_fields - Link field configurations
818
+ * @returns {Promise<Object[]>} Elements with link data
1000
819
  */
1001
820
  async read_link_attrs(elements, link_fields) {
1002
- if (elements && link_fields && link_fields.length > 0) {
1003
- //key is entity name, value is array of fields
1004
- const entity_attr_map = link_fields.reduce((map, field) => {
1005
- const link_field = this.meta.fields_map[field.link];
1006
- if (map[link_field.ref]) {
1007
- map[link_field.ref].push(field.name);
1008
- } else {
1009
- map[link_field.ref] = [field.name];
1010
- }
1011
- return map;
1012
- }, {});
1013
-
1014
- //key entity, value: attr property used to query
1015
- const entity_filter_map = link_fields.reduce((map, field) => {
1016
- const link_field = this.meta.fields_map[field.link];
1017
- if (map[link_field.ref]) {
1018
- (!map[link_field.ref].includes(link_field.name)) && map[link_field.ref].push(link_field.name);
1019
- } else {
1020
- map[link_field.ref] = [link_field.name];
1021
- }
1022
- return map;
1023
- }, {});
1024
-
1025
- const entities = Object.keys(entity_filter_map);
1026
- for (let i = 0; i < entities.length; i++) {
1027
- const meta = get_entity_meta(entities[i]);
1028
- const entity = new Entity(meta);
1029
- let id_array = [];
1030
- for (let j = 0; j < elements.length; j++) {
1031
- const obj = elements[j];
1032
- const linked_attrs = entity_filter_map[entities[i]];
1033
- for (let k = 0; k < linked_attrs.length; k++) {
1034
- const id = obj[linked_attrs[k]];
1035
- id_array.push(id);
1036
- }
1037
- }
1038
- id_array = unique(id_array);
1039
- const query = oid_queries(id_array);
1040
- const attr_fields = entity_attr_map[entities[i]];
1041
- const attrs = {};
1042
- const ref_fields = [];
1043
- attr_fields.forEach((attr) => {
1044
- attrs[attr] = 1;
1045
- const field = meta.fields_map[attr];
1046
- if (!field.link && field.ref) {
1047
- ref_fields.push(field);
1048
- }
1049
- });
1050
-
1051
- const ref_entity_items = await entity.find(query, attrs);
1052
- if (ref_entity_items && ref_entity_items.length > 0) {
1053
- await entity.convert_ref_attrs(ref_entity_items, ref_fields);
1054
-
1055
- for (let j = 0; j < elements.length; j++) {
1056
- const obj = elements[j];
1057
- const linked_attrs = entity_filter_map[entities[i]];
1058
- for (let k = 0; k < linked_attrs.length; k++) {
1059
- const id = obj[linked_attrs[k]];
1060
- const [link_obj] = ref_entity_items.filter(o => o._id + "" == id);
1061
- if (link_obj) {
1062
- const copy_obj = { ...link_obj };
1063
- delete copy_obj["_id"];
1064
- elements[j] = { ...obj, ...copy_obj };
1065
- }
1066
- }
821
+ if (!elements?.length || !link_fields?.length) return elements;
822
+
823
+ // Group by linked entity
824
+ const entity_info = link_fields.reduce((acc, field) => {
825
+ const link_field = this.meta.fields_map[field.link];
826
+ const entity = link_field.ref;
827
+ if (!acc[entity]) acc[entity] = { attrs: [], filters: [] };
828
+ acc[entity].attrs.push(field.name);
829
+ if (!acc[entity].filters.includes(link_field.name)) {
830
+ acc[entity].filters.push(link_field.name);
831
+ }
832
+ return acc;
833
+ }, {});
834
+
835
+ for (const [entity_name, { attrs, filters }] of Object.entries(entity_info)) {
836
+ const meta = get_entity_meta(entity_name);
837
+ const entity = new Entity(meta);
838
+
839
+ // Collect IDs
840
+ const ids = unique(elements.flatMap(o => filters.map(f => o[f])).filter(Boolean));
841
+ const query = oid_queries(ids);
842
+
843
+ // Build attrs and ref_fields
844
+ const attr_obj = {};
845
+ const ref_fields = [];
846
+ for (const attr of attrs) {
847
+ attr_obj[attr] = 1;
848
+ const field = meta.fields_map[attr];
849
+ if (!field.link && field.ref) ref_fields.push(field);
850
+ }
851
+
852
+ const items = await entity.find(query, attr_obj);
853
+ if (!items?.length) continue;
854
+
855
+ await entity.convert_ref_attrs(items, ref_fields);
856
+
857
+ // Merge link data
858
+ for (let i = 0; i < elements.length; i++) {
859
+ for (const filter of filters) {
860
+ const id = elements[i][filter];
861
+ const link_item = items.find(o => `${o._id}` === `${id}`);
862
+ if (link_item) {
863
+ const { _id, ...data } = link_item;
864
+ elements[i] = { ...elements[i], ...data };
1067
865
  }
1068
866
  }
1069
867
  }
@@ -1072,181 +870,51 @@ class Entity {
1072
870
  }
1073
871
 
1074
872
  /**
1075
- * get ref labels of the object, use ref_filter
1076
- * @returns
1077
- */
873
+ * Get ref labels with filter.
874
+ * @param {string} ref_by_entity - Referring entity
875
+ * @param {string} client_query - Client query string
876
+ * @returns {Promise<Object[]>} Filtered labels
877
+ */
1078
878
  get_filtered_ref_labels(ref_by_entity, client_query) {
1079
879
  let query = {};
880
+
1080
881
  if (this.meta.user_field) {
1081
- query[this.meta.user_field] = get_session_userid();
882
+ query[this.meta.user_field] = get_session_user_id();
1082
883
  }
1083
884
 
1084
- const search_fields = this.meta.search_fields;
885
+ // Parse client query
1085
886
  let search_query = {};
1086
- //client query: key:value, key:value
1087
- if (client_query && client_query.trim().length > 0 && search_fields && search_fields.length > 0) {
1088
- const queries = client_query.split(",");
1089
- for (let i = 0; i < queries.length; i++) {
1090
- const query_values = queries[i].split(":");
1091
- if (query_values && query_values.length == 2) {
1092
- const field_name = query_values[0];
1093
- const [search_field] = search_fields.filter(f => f.name == field_name);
1094
- search_field && (search_query = parse_search_value(field_name, search_field.type, query_values[1]))
887
+ if (client_query?.trim() && this.meta.search_fields?.length) {
888
+ for (const part of client_query.split(',')) {
889
+ const [field_name, value] = part.split(':');
890
+ if (field_name && value) {
891
+ const field = this.meta.search_fields.find(f => f.name === field_name);
892
+ if (field) {
893
+ search_query = parse_search_value(field_name, field.type, value);
894
+ }
1095
895
  }
1096
896
  }
1097
897
  }
1098
898
 
1099
- if (this.meta.ref_filter && ref_by_entity) {
1100
- if (this.meta.ref_filter[ref_by_entity]) {
1101
- query = { ...query, ...this.meta.ref_filter[ref_by_entity] };
1102
- } else if (this.meta.ref_filter["*"]) {
1103
- query = { ...query, ...this.meta.ref_filter["*"] };
1104
- }
1105
- }
1106
- return this.find_sort({ ...search_query, ...query }, { [this.meta.ref_label]: 1 }, { [this.meta.ref_label]: 1 });
899
+ query = apply_ref_filter(query, this.meta.ref_filter, ref_by_entity);
900
+ const ref_label = this.meta.ref_label;
901
+ return this.find_sort({ ...search_query, ...query }, { [ref_label]: 1 }, { [ref_label]: 1 });
1107
902
  }
1108
903
 
1109
- /**
1110
- * get ref labels of the object
1111
- * @param {id array of objectid} id_array
1112
- * @returns
1113
- */
904
+ /** Get ref labels by ID array. */
1114
905
  get_ref_labels(id_array) {
1115
- const query = oid_queries(id_array);
1116
- return this.find(query, { [this.meta.ref_label]: 1 });
906
+ return this.find(oid_queries(id_array), { [this.meta.ref_label]: 1 });
1117
907
  }
1118
908
 
1119
- /**
1120
- * get entity by object id
1121
- * @param {object id} id
1122
- * @param {the attributes to load from db} attr
1123
- */
909
+ /** Find by objectid. */
1124
910
  find_by_oid(id, attr) {
1125
911
  const query = oid_query(id);
1126
- if (query == null) {
1127
- return null;
1128
- }
1129
- return this.find_one(query, attr);
1130
- }
1131
-
1132
- /**
1133
- * Search the db using query
1134
- * @param {search criteria} query
1135
- * @param {the attributes to load from db} attr
1136
- * @returns
1137
- */
1138
- find(query, attr) {
1139
- return this.db.find(this.meta.collection, query, attr);
1140
- }
1141
-
1142
- /**
1143
- * Find one record from db
1144
- * @param {search criteria} query
1145
- * @param {the attributes to load from db} attr
1146
- * @returns
1147
- */
1148
- find_one(query, attr) {
1149
- return this.db.find_one(this.meta.collection, query, attr);
1150
- }
1151
-
1152
- /**
1153
- * Find the records from db using sort to do sorting
1154
- * @param {search criteria} query
1155
- * @param {sort object to sort the result} sort
1156
- * @param {the attributes of the object to load from db} attr
1157
- * @returns
1158
- */
1159
- find_sort(query, sort, attr) {
1160
- return this.db.find_sort(this.meta.collection, query, sort, attr);
1161
- }
1162
-
1163
- /**
1164
- * Find the page records
1165
- * @param {search criteria} query
1166
- * @param {sort object to sort the results} sort
1167
- * @param {the page index to load} page
1168
- * @param {page size } limit
1169
- * @param {the attributes of the object to load from db} attr
1170
- * @returns
1171
- */
1172
- find_page(query, sort, page, limit, attr) {
1173
- return this.db.find_page(this.meta.collection, query, sort, page, limit, attr);
1174
- }
1175
-
1176
- /**
1177
- * The count number of the query
1178
- * @param {search criteria} query
1179
- * @returns
1180
- */
1181
- count(query) {
1182
- return this.db.count(this.meta.collection, query);
1183
- }
1184
-
1185
- /**
1186
- * Calculate the sum value based on the field and query criteria
1187
- * @param {search criteria} query
1188
- * @param {field name to calculate sum} field
1189
- * @returns
1190
- */
1191
- sum(query, field) {
1192
- return this.db.sum(this.meta.collection, query, field);
912
+ return query ? this.find_one(query, attr) : null;
1193
913
  }
1194
914
 
1195
- /**
1196
- * Pull the object from array
1197
- * @param {search criteria} query
1198
- * @param {object pulled from the array} ele
1199
- * @returns
1200
- */
1201
- pull(query, ele) {
1202
- return this.db.pull(this.meta.collection, query, ele);
1203
- }
1204
-
1205
- /**
1206
- * Push the object to array
1207
- * @param {search criteria} query
1208
- * @param {object push to the array} ele
1209
- * @returns
1210
- */
1211
- push(query, ele) {
1212
- return this.db.push(this.meta.collection, query, ele);
1213
- }
1214
-
1215
- /**
1216
- * add the object to set
1217
- * @param {search criteria} query
1218
- * @param {object added to the set} ele
1219
- * @returns
1220
- */
1221
- add_to_set(query, ele) {
1222
- return this.db.add_to_set(this.meta.collection, query, ele);
1223
- };
1224
-
1225
- /**
1226
- * Get the mongodb collection with this entity
1227
- * @returns
1228
- */
1229
- col() {
1230
- return this.db.col(this.meta.collection);
1231
- };
1232
-
1233
- /**
1234
- * Add oid query to entity
1235
- * @param {object id} _id
1236
- * @returns
1237
- */
1238
- oid_query(_id) {
1239
- return oid_query(_id);
1240
- }
1241
-
1242
- /**
1243
- * Object ID array
1244
- * @param {object id array} _ids
1245
- * @returns
1246
- */
1247
- oid_queries(_ids) {
1248
- return oid_queries(_ids);
1249
- }
915
+ // Proxy methods for oid utilities
916
+ oid_query(_id) { return oid_query(_id); }
917
+ oid_queries(_ids) { return oid_queries(_ids); }
1250
918
  }
1251
919
 
1252
920
  module.exports = { Entity };