hola-server 1.0.11 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +382 -1
  2. package/dist/config/index.d.ts +46 -0
  3. package/dist/config/index.d.ts.map +1 -0
  4. package/dist/config/index.js +55 -0
  5. package/dist/config/index.js.map +1 -0
  6. package/dist/core/array.d.ts +27 -0
  7. package/dist/core/array.d.ts.map +1 -0
  8. package/dist/core/array.js +66 -0
  9. package/dist/core/array.js.map +1 -0
  10. package/dist/core/bash.d.ts +51 -0
  11. package/dist/core/bash.d.ts.map +1 -0
  12. package/dist/core/bash.js +161 -0
  13. package/dist/core/bash.js.map +1 -0
  14. package/dist/core/chart.d.ts +11 -0
  15. package/dist/core/chart.d.ts.map +1 -0
  16. package/dist/core/chart.js +35 -0
  17. package/dist/core/chart.js.map +1 -0
  18. package/dist/core/date.d.ts +11 -0
  19. package/dist/core/date.d.ts.map +1 -0
  20. package/dist/core/date.js +18 -0
  21. package/dist/core/date.js.map +1 -0
  22. package/dist/core/encrypt.d.ts +18 -0
  23. package/dist/core/encrypt.d.ts.map +1 -0
  24. package/dist/core/encrypt.js +50 -0
  25. package/dist/core/encrypt.js.map +1 -0
  26. package/dist/core/file.d.ts +22 -0
  27. package/dist/core/file.d.ts.map +1 -0
  28. package/dist/core/file.js +21 -0
  29. package/dist/core/file.js.map +1 -0
  30. package/dist/core/lhs.d.ts +17 -0
  31. package/dist/core/lhs.d.ts.map +1 -0
  32. package/dist/core/lhs.js +30 -0
  33. package/dist/core/lhs.js.map +1 -0
  34. package/dist/core/meta.d.ts +200 -0
  35. package/dist/core/meta.d.ts.map +1 -0
  36. package/dist/core/meta.js +336 -0
  37. package/dist/core/meta.js.map +1 -0
  38. package/dist/core/number.d.ts +37 -0
  39. package/dist/core/number.d.ts.map +1 -0
  40. package/dist/core/number.js +99 -0
  41. package/dist/core/number.js.map +1 -0
  42. package/dist/core/obj.d.ts +9 -0
  43. package/dist/core/obj.d.ts.map +1 -0
  44. package/dist/core/obj.js +15 -0
  45. package/dist/core/obj.js.map +1 -0
  46. package/dist/core/random.d.ts +7 -0
  47. package/dist/core/random.d.ts.map +1 -0
  48. package/dist/core/random.js +7 -0
  49. package/dist/core/random.js.map +1 -0
  50. package/dist/core/role.d.ts +42 -0
  51. package/dist/core/role.d.ts.map +1 -0
  52. package/dist/core/role.js +81 -0
  53. package/dist/core/role.js.map +1 -0
  54. package/dist/core/thread.d.ts +7 -0
  55. package/dist/core/thread.d.ts.map +1 -0
  56. package/dist/core/thread.js +7 -0
  57. package/dist/core/thread.js.map +1 -0
  58. package/dist/core/type.d.ts +46 -0
  59. package/dist/core/type.d.ts.map +1 -0
  60. package/dist/core/type.js +281 -0
  61. package/dist/core/type.js.map +1 -0
  62. package/dist/core/url.d.ts +20 -0
  63. package/dist/core/url.d.ts.map +1 -0
  64. package/dist/core/url.js +24 -0
  65. package/dist/core/url.js.map +1 -0
  66. package/dist/core/validate.d.ts +11 -0
  67. package/dist/core/validate.d.ts.map +1 -0
  68. package/dist/core/validate.js +19 -0
  69. package/dist/core/validate.js.map +1 -0
  70. package/dist/db/db.d.ts +72 -0
  71. package/dist/db/db.d.ts.map +1 -0
  72. package/dist/db/db.js +225 -0
  73. package/dist/db/db.js.map +1 -0
  74. package/dist/db/entity.d.ts +77 -0
  75. package/dist/db/entity.d.ts.map +1 -0
  76. package/dist/db/entity.js +671 -0
  77. package/dist/db/entity.js.map +1 -0
  78. package/dist/db/gridfs.d.ts +29 -0
  79. package/dist/db/gridfs.d.ts.map +1 -0
  80. package/dist/db/gridfs.js +125 -0
  81. package/dist/db/gridfs.js.map +1 -0
  82. package/dist/db/index.d.ts +8 -0
  83. package/dist/db/index.d.ts.map +1 -0
  84. package/dist/db/index.js +8 -0
  85. package/dist/db/index.js.map +1 -0
  86. package/dist/errors/auth.d.ts +15 -0
  87. package/dist/errors/auth.d.ts.map +1 -0
  88. package/dist/errors/auth.js +21 -0
  89. package/dist/errors/auth.js.map +1 -0
  90. package/dist/errors/http.d.ts +15 -0
  91. package/dist/errors/http.d.ts.map +1 -0
  92. package/dist/errors/http.js +21 -0
  93. package/dist/errors/http.js.map +1 -0
  94. package/dist/errors/index.d.ts +18 -0
  95. package/dist/errors/index.d.ts.map +1 -0
  96. package/dist/errors/index.js +18 -0
  97. package/dist/errors/index.js.map +1 -0
  98. package/dist/errors/validation.d.ts +11 -0
  99. package/dist/errors/validation.d.ts.map +1 -0
  100. package/dist/errors/validation.js +15 -0
  101. package/dist/errors/validation.js.map +1 -0
  102. package/dist/http/code.d.ts +21 -0
  103. package/dist/http/code.d.ts.map +1 -0
  104. package/dist/http/code.js +27 -0
  105. package/dist/http/code.js.map +1 -0
  106. package/dist/index.d.ts +57 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +61 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/meta/index.d.ts +9 -0
  111. package/dist/meta/index.d.ts.map +1 -0
  112. package/dist/meta/index.js +11 -0
  113. package/dist/meta/index.js.map +1 -0
  114. package/dist/meta/router.d.ts +26 -0
  115. package/dist/meta/router.d.ts.map +1 -0
  116. package/dist/meta/router.js +258 -0
  117. package/dist/meta/router.js.map +1 -0
  118. package/dist/meta/schema.d.ts +41 -0
  119. package/dist/meta/schema.d.ts.map +1 -0
  120. package/dist/meta/schema.js +69 -0
  121. package/dist/meta/schema.js.map +1 -0
  122. package/dist/plugins/auth.d.ts +248 -0
  123. package/dist/plugins/auth.d.ts.map +1 -0
  124. package/dist/plugins/auth.js +121 -0
  125. package/dist/plugins/auth.js.map +1 -0
  126. package/dist/plugins/body.d.ts +47 -0
  127. package/dist/plugins/body.d.ts.map +1 -0
  128. package/dist/plugins/body.js +36 -0
  129. package/dist/plugins/body.js.map +1 -0
  130. package/dist/plugins/cors.d.ts +62 -0
  131. package/dist/plugins/cors.d.ts.map +1 -0
  132. package/dist/plugins/cors.js +17 -0
  133. package/dist/plugins/cors.js.map +1 -0
  134. package/dist/plugins/error.d.ts +51 -0
  135. package/dist/plugins/error.d.ts.map +1 -0
  136. package/dist/plugins/error.js +51 -0
  137. package/dist/plugins/error.js.map +1 -0
  138. package/dist/plugins/index.d.ts +9 -0
  139. package/dist/plugins/index.d.ts.map +1 -0
  140. package/dist/plugins/index.js +9 -0
  141. package/dist/plugins/index.js.map +1 -0
  142. package/dist/setting.d.ts +66 -0
  143. package/dist/setting.d.ts.map +1 -0
  144. package/dist/setting.js +27 -0
  145. package/dist/setting.js.map +1 -0
  146. package/dist/tool/gen_i18n.d.ts +10 -0
  147. package/dist/tool/gen_i18n.d.ts.map +1 -0
  148. package/dist/tool/gen_i18n.js +51 -0
  149. package/dist/tool/gen_i18n.js.map +1 -0
  150. package/dist/tool/vector_store.d.ts +72 -0
  151. package/dist/tool/vector_store.d.ts.map +1 -0
  152. package/dist/tool/vector_store.js +203 -0
  153. package/dist/tool/vector_store.js.map +1 -0
  154. package/package.json +38 -23
  155. package/core/array.js +0 -187
  156. package/core/bash.js +0 -345
  157. package/core/chart.js +0 -36
  158. package/core/cron.js +0 -10
  159. package/core/date.js +0 -55
  160. package/core/encrypt.js +0 -16
  161. package/core/file.js +0 -38
  162. package/core/lhs.js +0 -27
  163. package/core/meta.js +0 -359
  164. package/core/msg.js +0 -11
  165. package/core/number.js +0 -179
  166. package/core/obj.js +0 -22
  167. package/core/random.js +0 -9
  168. package/core/role.js +0 -116
  169. package/core/thread.js +0 -3
  170. package/core/type.js +0 -329
  171. package/core/url.js +0 -22
  172. package/core/validate.js +0 -32
  173. package/db/db.js +0 -376
  174. package/db/entity.js +0 -1252
  175. package/db/gridfs.js +0 -221
  176. package/http/code.js +0 -18
  177. package/http/context.js +0 -17
  178. package/http/cors.js +0 -15
  179. package/http/error.js +0 -21
  180. package/http/express.js +0 -75
  181. package/http/params.js +0 -57
  182. package/http/router.js +0 -72
  183. package/http/session.js +0 -50
  184. package/index.js +0 -36
  185. package/router/clone.js +0 -73
  186. package/router/create.js +0 -59
  187. package/router/delete.js +0 -53
  188. package/router/read.js +0 -177
  189. package/router/update.js +0 -107
  190. package/setting.js +0 -51
  191. package/test/core/date.js +0 -37
  192. package/test/core/encrypt.js +0 -14
  193. package/test/core/meta.js +0 -594
  194. package/test/core/number.js +0 -17
  195. package/test/crud/router.js +0 -99
  196. package/test/db/db.js +0 -72
  197. package/test/entity/create.js +0 -442
  198. package/test/entity/delete.js +0 -480
  199. package/test/entity/read.js +0 -285
  200. package/test/entity/update.js +0 -252
  201. package/test/router/role.js +0 -15
  202. package/test/router/user.js +0 -17
  203. package/tool/gen_i18n.js +0 -30
  204. package/tool/test.json +0 -25
@@ -0,0 +1,671 @@
1
+ /**
2
+ * Entity-level CRUD helpers and metadata-driven operations.
3
+ * @module db/entity
4
+ */
5
+ import { SUCCESS, ERROR, NO_PARAMS, INVALID_PARAMS, DUPLICATE_UNIQUE, NOT_FOUND, REF_NOT_FOUND, REF_NOT_UNIQUE, HAS_REF } from "../http/code.js";
6
+ import { validate_required_fields, has_value } from "../core/validate.js";
7
+ import { convert_type, convert_update_type, get_type } from "../core/type.js";
8
+ import { get_entity_meta, DELETE_MODE, filter_fields_by_role } from "../core/meta.js";
9
+ import { unique, map_array_to_obj } from "../core/array.js";
10
+ import { LOG_ENTITY, get_db, oid_query, oid_queries, log_debug, log_error, bulk_update } from "./db.js";
11
+ // Comparison operator mapping for search queries
12
+ const COMPARISON_OPERATORS = [
13
+ { prefix: ">=", op: "$gte", len: 2 },
14
+ { prefix: "<=", op: "$lte", len: 2 },
15
+ { prefix: ">", op: "$gt", len: 1 },
16
+ { prefix: "<", op: "$lt", len: 1 },
17
+ ];
18
+ // Numeric types where "0" should be treated as "no search value" (default empty state)
19
+ // Note: int_enum types are NOT included here because 0 is often a valid enum value (e.g., ACTIVE, ADMIN)
20
+ const NUMERIC_TYPES = ["number", "int", "uint", "float", "ufloat", "decimal", "percentage", "currency"];
21
+ /** Check if a search value should be included in the query */
22
+ const has_search_value = (value, type_name) => {
23
+ if (!has_value(value))
24
+ return false;
25
+ // For numeric types (but not enums), "0" or 0 without comparison operators means no search value
26
+ if (NUMERIC_TYPES.includes(type_name)) {
27
+ const raw = `${value}`.trim();
28
+ // Include if it has comparison operators or is not just "0"
29
+ if (raw === "0")
30
+ return false;
31
+ }
32
+ return true;
33
+ };
34
+ /** Convert search value type, keeping original on error. */
35
+ const convert_search_value = (type_name, search_value) => {
36
+ const { value, err } = get_type(type_name).convert(search_value);
37
+ return err ? search_value : value;
38
+ };
39
+ /** Create search object based on field type and value. */
40
+ const parse_search_value = (name, type_name, search_value) => {
41
+ const raw = `${search_value}`;
42
+ if (raw.includes(",")) {
43
+ const values = raw.split(",").map((v) => convert_search_value(type_name, v));
44
+ const op = type_name === "array" ? "$all" : "$in";
45
+ return { [name]: { [op]: values } };
46
+ }
47
+ for (const { prefix, op, len } of COMPARISON_OPERATORS) {
48
+ if (raw.startsWith(prefix)) {
49
+ return { [name]: { [op]: convert_search_value(type_name, raw.substring(len)) } };
50
+ }
51
+ }
52
+ if (type_name === "array")
53
+ return { [name]: { $in: [raw] } };
54
+ let value = convert_search_value(type_name, raw);
55
+ if (typeof value === "string")
56
+ value = new RegExp(value, "i");
57
+ return { [name]: value };
58
+ };
59
+ /** Apply ref_filter to query based on entity context. */
60
+ const apply_ref_filter = (query, ref_filter, ref_by_entity) => {
61
+ if (!ref_filter)
62
+ return query;
63
+ const filter = (ref_by_entity && ref_filter[ref_by_entity]) || ref_filter["*"] || (typeof ref_filter === "object" ? ref_filter : null);
64
+ return filter ? { ...query, ...filter } : query;
65
+ };
66
+ /** Log error with formatted message. */
67
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
+ const log_err = (msg, data = {}) => {
69
+ const parts = Object.entries(data)
70
+ .filter(([, v]) => v !== undefined)
71
+ .map(([k, v]) => `${k}:${JSON.stringify(v)}`);
72
+ log_error(LOG_ENTITY, parts.length ? `${msg} - ${parts.join(", ")}` : msg);
73
+ };
74
+ /** Execute a lifecycle hook and return error result if failed. */
75
+ const run_hook = async (hook, hook_name, ctx) => {
76
+ if (!hook)
77
+ return null;
78
+ const { code, err } = await hook(ctx);
79
+ if (err || code !== SUCCESS) {
80
+ log_err(`${hook_name} error`, { err, code });
81
+ return { code, err };
82
+ }
83
+ return null;
84
+ };
85
+ /** Validate reference and return error result if failed. */
86
+ const validate_refs = async (entity, obj) => {
87
+ if (!entity.meta.ref_fields)
88
+ return null;
89
+ const { code, err } = await entity.validate_ref(obj);
90
+ if (err || code !== SUCCESS) {
91
+ log_err("validate_ref error", { err, code });
92
+ return { code, err };
93
+ }
94
+ return null;
95
+ };
96
+ /** Parse positive integer with default. */
97
+ const parse_int = (value, defaultVal) => {
98
+ const parsed = parseInt(String(value));
99
+ return isNaN(parsed) || parsed <= 0 ? defaultVal : parsed;
100
+ };
101
+ /** Extract ref and link fields from field map. */
102
+ const extract_field_info = (fields_map, attr_names, allowed_names) => {
103
+ const attrs = { _id: 1 };
104
+ const ref_fields = [];
105
+ const link_fields = [];
106
+ attr_names.split(",").forEach((attr) => {
107
+ if (!allowed_names.includes(attr))
108
+ return;
109
+ attrs[attr] = 1;
110
+ const field = fields_map[attr];
111
+ if (field.link) {
112
+ link_fields.push(field);
113
+ attrs[field.link] = 1;
114
+ }
115
+ else if (field.ref) {
116
+ ref_fields.push(field);
117
+ }
118
+ });
119
+ return { attrs, ref_fields, link_fields };
120
+ };
121
+ export class Entity {
122
+ meta;
123
+ _db = null;
124
+ constructor(meta) {
125
+ this.meta = get_entity_meta(meta);
126
+ }
127
+ /** Get db instance lazily - ensures connection is established */
128
+ get db() {
129
+ if (!this._db) {
130
+ this._db = get_db();
131
+ }
132
+ return this._db;
133
+ }
134
+ col() {
135
+ return this.db.col(this.meta.collection);
136
+ }
137
+ async bulk_update(items, attrs) {
138
+ await bulk_update(this.col(), items, attrs);
139
+ }
140
+ async validate_ref(param_obj) {
141
+ const ref_fields = this.meta.ref_fields;
142
+ if (!ref_fields)
143
+ return { code: SUCCESS };
144
+ for (const field of ref_fields) {
145
+ const value = param_obj[field.name];
146
+ const ref_entity = new Entity(field.ref);
147
+ const resolve_ref = async (v) => {
148
+ const refs = await ref_entity.find_by_ref_value(v, { _id: 1 }, this.meta.collection);
149
+ if (refs.length === 0)
150
+ return { code: REF_NOT_FOUND, err: [field.name] };
151
+ if (refs.length > 1)
152
+ return { code: REF_NOT_UNIQUE, err: [field.name] };
153
+ return { id: `${refs[0]._id}` };
154
+ };
155
+ if (Array.isArray(value)) {
156
+ const ids = [];
157
+ for (const v of value) {
158
+ const result = await resolve_ref(v);
159
+ if (result.code)
160
+ return result;
161
+ ids.push(result.id);
162
+ }
163
+ param_obj[field.name] = ids;
164
+ }
165
+ else if (has_value(value)) {
166
+ const result = await resolve_ref(value);
167
+ if (result.code)
168
+ return result;
169
+ param_obj[field.name] = result.id;
170
+ }
171
+ }
172
+ return { code: SUCCESS };
173
+ }
174
+ async get_search_query(param_obj) {
175
+ const { search_fields } = this.meta;
176
+ if (!search_fields?.length)
177
+ return null;
178
+ // Warn about filter values for non-searchable fields (common mistake: search:false but expecting filter to work)
179
+ const searchable_names = new Set(search_fields.map((f) => f.name));
180
+ const all_field_names = new Set(this.meta.fields.map((f) => f.name));
181
+ for (const [key, value] of Object.entries(param_obj)) {
182
+ if (key.startsWith("_"))
183
+ continue; // Skip system params like _id, _user
184
+ if (!all_field_names.has(key))
185
+ continue; // Skip unknown fields
186
+ if (!searchable_names.has(key) && has_value(value)) {
187
+ log_error(LOG_ENTITY, `Filter ignored: field '${key}' has search:false in entity '${this.meta.collection}'. Set search:true to enable filtering.`);
188
+ }
189
+ }
190
+ const ref_names = this.meta.ref_fields.map((f) => f.name);
191
+ const and_array = [];
192
+ for (const field of search_fields) {
193
+ const value = param_obj[field.name];
194
+ const type_name = field.type || "string";
195
+ if (!has_search_value(value, type_name))
196
+ continue;
197
+ if (ref_names.includes(field.name)) {
198
+ const ref_entity = new Entity(field.ref);
199
+ const oids = await ref_entity.find_by_ref_value(value, { _id: 1 }, this.meta.collection);
200
+ if (oids.length > 0) {
201
+ const ids = oids.map((o) => `${o._id}`);
202
+ const op = oids.length === 1 ? null : `${value}`.includes(",") ? "$all" : "$in";
203
+ and_array.push(op ? { [field.name]: { [op]: ids } } : { [field.name]: ids[0] });
204
+ }
205
+ }
206
+ else {
207
+ and_array.push(parse_search_value(field.name, field.type || "string", value));
208
+ }
209
+ }
210
+ const id_param = param_obj._id?.trim();
211
+ if (id_param) {
212
+ const ids = id_param.split(",");
213
+ and_array.push(ids.length === 1 ? oid_query(ids[0]) : oid_queries(ids));
214
+ }
215
+ if (and_array.length > 0) {
216
+ const query = { $and: and_array };
217
+ log_debug(LOG_ENTITY, `search query:${JSON.stringify(query)}`);
218
+ return query;
219
+ }
220
+ return {};
221
+ }
222
+ async list_entity(query_params, query, param_obj, role) {
223
+ const { attr_names, page, limit, sort_by, desc } = query_params;
224
+ const sorts = sort_by.split(",");
225
+ const descs = desc.split(",");
226
+ const sort = sorts.reduce((s, field, i) => ({ ...s, [field]: descs[i] === "false" ? 1 : -1 }), {});
227
+ const list_fields = filter_fields_by_role(this.meta.list_fields, role);
228
+ const { attrs, ref_fields, link_fields } = extract_field_info(this.meta.fields_map, attr_names, list_fields.map((f) => f.name));
229
+ const search_query = await this.get_search_query(param_obj);
230
+ if (search_query === null) {
231
+ log_err("no search query", { param_obj });
232
+ return { code: INVALID_PARAMS, err: "no search query is set" };
233
+ }
234
+ const merged = { ...(query || {}), ...search_query };
235
+ const total = await this.count(merged);
236
+ const list = await this.find_page(merged, sort, page, limit, attrs);
237
+ const with_links = await this.read_link_attrs(list, link_fields);
238
+ const data = await this.convert_ref_attrs(with_links, ref_fields);
239
+ log_debug(LOG_ENTITY, `total:${total},data:${JSON.stringify(data)}`);
240
+ return { code: SUCCESS, total, data };
241
+ }
242
+ async _save_entity(param_obj, role, options) {
243
+ const { fields_key, before_hook, main_hook, after_hook, id_for_hook } = options;
244
+ const def = this.meta;
245
+ const fields = filter_fields_by_role(this.meta[fields_key], role);
246
+ const { obj, error_field_names } = convert_update_type(param_obj, fields);
247
+ if (error_field_names.length > 0) {
248
+ log_err("invalid fields", { fields: error_field_names });
249
+ return { code: INVALID_PARAMS, err: error_field_names };
250
+ }
251
+ // Preserve _user context for hooks to access (e.g., for setting ownership fields)
252
+ if (param_obj._user) {
253
+ obj._user = param_obj._user;
254
+ // Auto-set user_field from session user (works even if field has create: false)
255
+ if (this.meta.user_field && param_obj._user.sub) {
256
+ obj[this.meta.user_field] = param_obj._user.sub;
257
+ }
258
+ }
259
+ // Build hook context based on whether we have an id (clone) or not (create)
260
+ const hookCtx = id_for_hook ? { id: id_for_hook, entity: this, data: obj } : { entity: this, data: obj };
261
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
+ const before_err = await run_hook(def[before_hook], before_hook, hookCtx);
263
+ if (before_err)
264
+ return before_err;
265
+ const missing = validate_required_fields(obj, this.meta.required_field_names);
266
+ if (missing.length > 0) {
267
+ log_err("missing required fields", { fields: missing });
268
+ return { code: NO_PARAMS, err: missing };
269
+ }
270
+ if ((await this.count_by_primary_keys(obj)) > 0) {
271
+ return { code: DUPLICATE_UNIQUE, err: "entity already exist in db" };
272
+ }
273
+ const ref_err = await validate_refs(this, obj);
274
+ if (ref_err)
275
+ return ref_err;
276
+ if (def[main_hook]) {
277
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
278
+ const main_err = await run_hook(def[main_hook], main_hook, hookCtx);
279
+ if (main_err)
280
+ return main_err;
281
+ }
282
+ else {
283
+ // Clean up _user context before saving to DB
284
+ delete obj._user;
285
+ const db_obj = await this.create(obj);
286
+ if (!db_obj._id) {
287
+ log_err("create failed");
288
+ return { code: ERROR, err: "creating record is failed" };
289
+ }
290
+ }
291
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
292
+ const after_err = await run_hook(def[after_hook], after_hook, hookCtx);
293
+ if (after_err)
294
+ return after_err;
295
+ return { code: SUCCESS };
296
+ }
297
+ async create_entity(param_obj, role) {
298
+ return this._save_entity(param_obj, role, { fields_key: "create_fields", before_hook: "before_create", main_hook: "create", after_hook: "after_create" });
299
+ }
300
+ async clone_entity(_id, param_obj, role) {
301
+ return this._save_entity(param_obj, role, { fields_key: "clone_fields", before_hook: "before_clone", main_hook: "clone", after_hook: "after_clone", id_for_hook: _id });
302
+ }
303
+ async update_entity(_id, param_obj, role) {
304
+ const fields = filter_fields_by_role(this.meta.update_fields, role);
305
+ const { obj, error_field_names } = convert_update_type(param_obj, fields);
306
+ if (error_field_names.length > 0) {
307
+ log_err("update_entity invalid fields", { fields: error_field_names });
308
+ return { code: INVALID_PARAMS, err: error_field_names };
309
+ }
310
+ const updateCtx = { id: _id, entity: this, data: obj };
311
+ const before_err = await run_hook(this.meta.before_update, "before_update", updateCtx);
312
+ if (before_err)
313
+ return before_err;
314
+ const query = _id ? oid_query(_id) : this.primary_key_query(obj);
315
+ if (!query) {
316
+ log_err("invalid query", { _id, obj });
317
+ return { code: INVALID_PARAMS, err: _id ? ["_id"] : this.meta.primary_keys };
318
+ }
319
+ if ((await this.count(query)) !== 1) {
320
+ log_err("entity not found", { query });
321
+ return { code: NOT_FOUND, err: _id ? ["_id"] : this.meta.primary_keys };
322
+ }
323
+ const ref_err = await validate_refs(this, obj);
324
+ if (ref_err)
325
+ return ref_err;
326
+ if (this.meta.update) {
327
+ const update_err = await run_hook(this.meta.update, "update", updateCtx);
328
+ if (update_err)
329
+ return update_err;
330
+ }
331
+ else {
332
+ await this.update(query, obj);
333
+ }
334
+ const after_err = await run_hook(this.meta.after_update, "after_update", updateCtx);
335
+ if (after_err)
336
+ return after_err;
337
+ return { code: SUCCESS };
338
+ }
339
+ async batch_update_entity(_ids, param_obj, role) {
340
+ const fields = filter_fields_by_role(this.meta.update_fields, role);
341
+ const { obj, error_field_names } = convert_update_type(param_obj, fields);
342
+ if (error_field_names.length > 0) {
343
+ log_err("batch_update invalid fields", { fields: error_field_names });
344
+ return { code: INVALID_PARAMS, err: error_field_names };
345
+ }
346
+ const query = oid_queries(_ids);
347
+ if (!query) {
348
+ log_err("batch_update invalid ids", { _ids });
349
+ return { code: INVALID_PARAMS, err: ["_ids"] };
350
+ }
351
+ const ref_err = await validate_refs(this, obj);
352
+ if (ref_err)
353
+ return ref_err;
354
+ const batchCtx = { ids: _ids, entity: this, data: obj };
355
+ if (this.meta.batch_update) {
356
+ const batch_err = await run_hook(this.meta.batch_update, "batch_update", batchCtx);
357
+ if (batch_err)
358
+ return batch_err;
359
+ }
360
+ else {
361
+ const result = await this.update(query, obj);
362
+ if (result.ok !== 1) {
363
+ log_err("batch update failed", { query, obj, result });
364
+ return { code: ERROR, err: "batch update record is failed" };
365
+ }
366
+ }
367
+ const after_err = await run_hook(this.meta.after_batch_update, "after_batch_update", batchCtx);
368
+ if (after_err)
369
+ return after_err;
370
+ return { code: SUCCESS };
371
+ }
372
+ async delete_entity(id_array) {
373
+ const query = oid_queries(id_array);
374
+ if (!query) {
375
+ log_err("delete_entity invalid ids", { id_array });
376
+ return { code: INVALID_PARAMS, err: ["ids"] };
377
+ }
378
+ const deleteCtx = { entity: this, ids: id_array };
379
+ const before_err = await run_hook(this.meta.before_delete, "before_delete", deleteCtx);
380
+ if (before_err)
381
+ return before_err;
382
+ if (this.meta.delete) {
383
+ const delete_err = await run_hook(this.meta.delete, "delete", deleteCtx);
384
+ if (delete_err)
385
+ return delete_err;
386
+ }
387
+ else {
388
+ const refs = await this.check_refer_entity(id_array);
389
+ if (refs.length > 0) {
390
+ const unique_refs = [...new Set(refs)];
391
+ log_err("has references", { refs: unique_refs });
392
+ return { code: HAS_REF, err: unique_refs };
393
+ }
394
+ await this.delete(query);
395
+ for (const ref_by_meta of this.meta.ref_by_metas) {
396
+ const ref_fields = ref_by_meta.ref_fields.filter((f) => f.ref === this.meta.collection);
397
+ for (const field of ref_fields) {
398
+ if (field.delete === DELETE_MODE.cascade) {
399
+ const ref_entity = new Entity(ref_by_meta.collection);
400
+ await ref_entity.delete_refer_entity(field.name, id_array);
401
+ }
402
+ }
403
+ }
404
+ }
405
+ const after_err = await run_hook(this.meta.after_delete, "after_delete", deleteCtx);
406
+ if (after_err)
407
+ return after_err;
408
+ return { code: SUCCESS };
409
+ }
410
+ async read_property(_id, attr_names, role) {
411
+ const query = oid_query(_id);
412
+ if (!query) {
413
+ log_err("read_property invalid id", { _id });
414
+ return { code: INVALID_PARAMS, err: ["_id"] };
415
+ }
416
+ const property_fields = filter_fields_by_role(this.meta.property_fields, role);
417
+ const field_names = property_fields.map((f) => f.name);
418
+ const attrs = { _id: 1 };
419
+ attr_names.split(",").forEach((attr) => {
420
+ if (field_names.includes(attr))
421
+ attrs[attr] = 1;
422
+ });
423
+ const results = await this.find(query, attrs);
424
+ if (results?.length === 1) {
425
+ log_debug(LOG_ENTITY, `read_property query:${JSON.stringify(query)},result:${JSON.stringify(results[0])}`);
426
+ return { code: SUCCESS, data: results[0] };
427
+ }
428
+ return { code: NOT_FOUND, err: ["_id"] };
429
+ }
430
+ async read_entity(_id, attr_names, role) {
431
+ const query = oid_query(_id);
432
+ if (!query) {
433
+ log_err("read_entity invalid id", { _id });
434
+ return { code: INVALID_PARAMS, err: ["_id"] };
435
+ }
436
+ if (!attr_names) {
437
+ log_err("read_entity invalid attr_names", { attr_names });
438
+ return { code: INVALID_PARAMS, err: ["attr_names"] };
439
+ }
440
+ const property_fields = filter_fields_by_role(this.meta.property_fields, role);
441
+ const { attrs, ref_fields, link_fields } = extract_field_info(this.meta.fields_map, attr_names, property_fields.map((f) => f.name));
442
+ const results = await this.find(query, attrs);
443
+ if (results?.length !== 1)
444
+ return { code: NOT_FOUND, err: ["_id"] };
445
+ const readCtx = { id: _id, entity: this, attrNames: attr_names, result: results[0] };
446
+ const after_err = await run_hook(this.meta.after_read, "after_read", readCtx);
447
+ if (after_err)
448
+ return after_err;
449
+ const with_links = await this.read_link_attrs(results, link_fields);
450
+ const converted = await this.convert_ref_attrs(with_links, ref_fields);
451
+ if (converted?.length === 1) {
452
+ log_debug(LOG_ENTITY, `read_entity query:${JSON.stringify(query)},result:${JSON.stringify(converted[0])}`);
453
+ return { code: SUCCESS, data: converted[0] };
454
+ }
455
+ return { code: NOT_FOUND, err: ["_id"] };
456
+ }
457
+ primary_key_query(param_obj) {
458
+ if (!this.meta.primary_keys.every((key) => has_value(param_obj[key])))
459
+ return null;
460
+ const { obj, error_field_names } = convert_type(param_obj, this.meta.primary_key_fields);
461
+ if (error_field_names.length > 0)
462
+ return null;
463
+ return this.meta.primary_keys.reduce((q, key) => ({ ...q, [key]: obj[key] }), {});
464
+ }
465
+ count_by_primary_keys(obj) {
466
+ return this.count(this.primary_key_query(obj));
467
+ }
468
+ // Database operations
469
+ create(obj) {
470
+ return this.db.create(this.meta.collection, obj);
471
+ }
472
+ update(query, obj) {
473
+ return this.db.update(this.meta.collection, query, obj);
474
+ }
475
+ delete(query) {
476
+ return this.db.delete(this.meta.collection, query);
477
+ }
478
+ find(query, attr) {
479
+ return this.db.find(this.meta.collection, query, attr);
480
+ }
481
+ find_one(query, attr) {
482
+ return this.db.find_one(this.meta.collection, query, attr);
483
+ }
484
+ find_sort(query, sort, attr) {
485
+ return this.db.find_sort(this.meta.collection, query, sort, attr);
486
+ }
487
+ find_page(query, sort, page, limit, attr) {
488
+ return this.db.find_page(this.meta.collection, query, sort, page, limit, attr);
489
+ }
490
+ count(query) {
491
+ return this.db.count(this.meta.collection, query);
492
+ }
493
+ sum(query, field) {
494
+ return this.db.sum(this.meta.collection, query, field);
495
+ }
496
+ pull(query, ele) {
497
+ return this.db.pull(this.meta.collection, query, ele);
498
+ }
499
+ push(query, ele) {
500
+ return this.db.push(this.meta.collection, query, ele);
501
+ }
502
+ add_to_set(query, ele) {
503
+ return this.db.add_to_set(this.meta.collection, query, ele);
504
+ }
505
+ delete_by_id(id) {
506
+ const query = Array.isArray(id) ? oid_queries(id) : oid_query(id);
507
+ return this.db.delete(this.meta.collection, query);
508
+ }
509
+ find_by_ref_value(value, attr, ref_by_entity) {
510
+ let query = Array.isArray(value) ? oid_queries(value) : oid_query(value);
511
+ if (!query) {
512
+ const ref_label = this.meta.ref_label;
513
+ if (Array.isArray(value)) {
514
+ query = { [ref_label]: { $in: value } };
515
+ }
516
+ else if (String(value).includes(",")) {
517
+ query = { [ref_label]: { $in: String(value).split(",") } };
518
+ }
519
+ else {
520
+ query = { [ref_label]: value };
521
+ }
522
+ }
523
+ return this.find(apply_ref_filter(query, this.meta.ref_filter, ref_by_entity), attr);
524
+ }
525
+ async check_refer_entity(id_array) {
526
+ const refs = [];
527
+ for (const ref_by_meta of this.meta.ref_by_metas) {
528
+ const ref_entity = new Entity(ref_by_meta.collection);
529
+ const ref_fields = ref_by_meta.ref_fields.filter((f) => f.ref === this.meta.collection);
530
+ for (const field of ref_fields) {
531
+ if (field.delete === DELETE_MODE.keep)
532
+ continue;
533
+ const attr = ref_by_meta.ref_label ? { [ref_by_meta.ref_label]: 1 } : {};
534
+ const entities = await ref_entity.get_refer_entities(field.name, id_array, attr);
535
+ if (!entities?.length)
536
+ continue;
537
+ if (field.delete === DELETE_MODE.cascade) {
538
+ const cascade_refs = await ref_entity.check_refer_entity(entities.map((o) => `${o._id}`));
539
+ if (cascade_refs?.length)
540
+ refs.push(...cascade_refs);
541
+ }
542
+ else {
543
+ const label_key = ref_by_meta.ref_label || "_id";
544
+ refs.push(...entities.map((o) => `${this.meta.collection}<-${ref_by_meta.collection}:${o[label_key]}`));
545
+ }
546
+ }
547
+ }
548
+ return refs;
549
+ }
550
+ async get_refer_entities(field_name, id_array, attr) {
551
+ return this.find({ [field_name]: { $in: id_array } }, attr);
552
+ }
553
+ async delete_refer_entity(field_name, id_array) {
554
+ const entities = await this.get_refer_entities(field_name, id_array, {});
555
+ await this.delete_entity(entities.map((o) => `${o._id}`));
556
+ }
557
+ async convert_ref_attrs(elements, ref_fields = this.meta.ref_fields) {
558
+ if (!elements?.length || !ref_fields?.length)
559
+ return elements;
560
+ for (const field of ref_fields) {
561
+ let ids = [];
562
+ for (const obj of elements) {
563
+ const value = obj[field.name];
564
+ if (Array.isArray(value))
565
+ ids.push(...value);
566
+ else if (value)
567
+ ids.push(value);
568
+ }
569
+ ids = unique(ids);
570
+ const ref_entity = new Entity(field.ref);
571
+ const labels = await ref_entity.get_ref_labels(ids);
572
+ const label_map = map_array_to_obj(labels, "_id", ref_entity.meta.ref_label);
573
+ for (const obj of elements) {
574
+ const value = obj[field.name];
575
+ obj[`${field.name}_id`] = value;
576
+ obj[field.name] = Array.isArray(value) ? value.map((v) => label_map[v]) : value ? label_map[value] : value;
577
+ }
578
+ }
579
+ return elements;
580
+ }
581
+ async read_link_attrs(elements, link_fields) {
582
+ if (!elements?.length || !link_fields?.length)
583
+ return elements;
584
+ const entity_info = {};
585
+ for (const field of link_fields) {
586
+ const link_field = this.meta.fields_map[field.link];
587
+ const entity = link_field.ref;
588
+ if (!entity_info[entity])
589
+ entity_info[entity] = { attrs: [], filters: [] };
590
+ entity_info[entity].attrs.push(field.name);
591
+ if (!entity_info[entity].filters.includes(link_field.name)) {
592
+ entity_info[entity].filters.push(link_field.name);
593
+ }
594
+ }
595
+ for (const [entity_name, { attrs, filters }] of Object.entries(entity_info)) {
596
+ const entity = new Entity(entity_name);
597
+ const ids = unique(elements.flatMap((o) => filters.map((f) => o[f])).filter(Boolean));
598
+ const query = oid_queries(ids);
599
+ if (!query)
600
+ continue;
601
+ const attr_obj = {};
602
+ const ref_fields = [];
603
+ for (const attr of attrs) {
604
+ attr_obj[attr] = 1;
605
+ const field = entity.meta.fields_map[attr];
606
+ if (!field.link && field.ref)
607
+ ref_fields.push(field);
608
+ }
609
+ const items = await entity.find(query, attr_obj);
610
+ if (!items?.length)
611
+ continue;
612
+ await entity.convert_ref_attrs(items, ref_fields);
613
+ for (let i = 0; i < elements.length; i++) {
614
+ for (const filter of filters) {
615
+ const id = elements[i][filter];
616
+ const link_item = items.find((o) => `${o._id}` === `${id}`);
617
+ if (link_item) {
618
+ const { _id, ...data } = link_item;
619
+ elements[i] = { ...elements[i], ...data };
620
+ }
621
+ }
622
+ }
623
+ }
624
+ return elements;
625
+ }
626
+ get_filtered_ref_labels(ref_by_entity, client_query, user_id) {
627
+ let query = {};
628
+ if (this.meta.user_field && user_id)
629
+ query[this.meta.user_field] = user_id;
630
+ let search_query = {};
631
+ if (client_query?.trim() && this.meta.search_fields?.length) {
632
+ for (const part of client_query.split(",")) {
633
+ const [field_name, value] = part.split(":");
634
+ if (field_name && value) {
635
+ const field = this.meta.search_fields.find((f) => f.name === field_name);
636
+ if (field)
637
+ search_query = parse_search_value(field_name, field.type || "string", value);
638
+ }
639
+ }
640
+ }
641
+ query = apply_ref_filter(query, this.meta.ref_filter, ref_by_entity);
642
+ const ref_label = this.meta.ref_label;
643
+ return this.find_sort({ ...search_query, ...query }, { [ref_label]: 1 }, { [ref_label]: 1 });
644
+ }
645
+ get_ref_labels(id_array) {
646
+ return this.find(oid_queries(id_array), { [this.meta.ref_label]: 1 });
647
+ }
648
+ find_by_oid(id, attr) {
649
+ const query = oid_query(id);
650
+ return query ? this.find_one(query, attr) : Promise.resolve(null);
651
+ }
652
+ find_one_ref_entity(field_name, value, attr) {
653
+ const field = this.meta.fields.find((f) => f.name === field_name);
654
+ if (!field) {
655
+ throw new Error(`field not found: ${field_name} in ${this.meta.collection}`);
656
+ }
657
+ if (!field.ref) {
658
+ throw new Error(`field is not ref: ${field_name} in ${this.meta.collection}`);
659
+ }
660
+ const ref_entity = new Entity(field.ref);
661
+ const query = oid_query(value) || { [ref_entity.meta.ref_label]: value };
662
+ return ref_entity.find_one(query, attr);
663
+ }
664
+ oid_query(_id) {
665
+ return oid_query(_id);
666
+ }
667
+ oid_queries(_ids) {
668
+ return oid_queries(_ids);
669
+ }
670
+ }
671
+ //# sourceMappingURL=entity.js.map