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/core/meta.js CHANGED
@@ -1,359 +1,283 @@
1
- const { is_undefined } = require('./validate');
1
+ /**
2
+ * @fileoverview Meta programming core - Entity definition and validation.
3
+ * @module core/meta
4
+ *
5
+ * Field attributes:
6
+ * - name: Field name (required).
7
+ * - type: Data type (string, int, float, etc., default: "string").
8
+ * - required: Whether field is required (default: false).
9
+ * - default: Default value for the field. Value must be valid for the field type.
10
+ * - ref: Reference to another entity (collection name).
11
+ * - link: Link to another field in this entity (must be a field of type 'ref').
12
+ * This field's value will be auto-populated from the referenced entity.
13
+ * Only supports { name, link, list } attributes.
14
+ * - delete: Deletion behavior for ref fields.
15
+ * - "keep": Keep this record when referenced entity is deleted.
16
+ * - "cascade": Delete this record when referenced entity is deleted.
17
+ * - Default: undefined (no action).
18
+ * - create: Show in create form (default: true).
19
+ * - list: Show in table list (default: true).
20
+ * - search: Show in search form (default: true).
21
+ * - update: Allow update (default: true).
22
+ * - clone: Include in clone (default: true).
23
+ * - sys: System field (server-side only, not sent to client unless explicitly needed).
24
+ * - secure: Hidden from client entirely (e.g., password hash).
25
+ * - group: User group sharing control (field name containing group ID).
26
+ * - view: Form view identifier - controls which form view(s) display this field.
27
+ * - "*": All views (default for editable fields).
28
+ * - "view_name": Specific view only.
29
+ * Only applicable to editable fields (create !== false or update !== false).
30
+ * Multiple form views can reference different subsets of fields for different contexts.
31
+ */
32
+
2
33
  const { get_type } = require('./type');
3
34
  const { validate_meta_role } = require('./role');
4
35
 
5
36
  const meta_manager = {};
6
- /**
7
- * create: this field can be shown in create form
8
- * list: this field can be shown in table list
9
- * search: this field can be shown in search form
10
- * update: if is false, in update form, it will be readonly status
11
- * delete: delete is only used for ref field, it decide when the ref entity will be deleted, how to handle this entity,no value, will not let the refered entity be deleted, keep: keep this entity(no data consistency), cascade: also delete this entity also,
12
- * sys: this field is used to control the user can set the value or not. sys field can only be set in the server side(before callback is good place to do this)
13
- * create is false, this attribute can be shown in property list but sys property can't be shown in property list
14
- * secure: secure properties will not be read by client, this is useful for password
15
- * group: this is used to control user sharing entities, this means the entity is shared by user group, this is only valid for user field
16
- * view: this is used to control for edit form, for one entity, may have many forms to edit the entity, so use view to seperate them, this can be string or array
17
- *
18
- * routes: configure customer defined routes
19
- * link property: field link property link to entity field and the field should ref to an entity.
20
- * and the field name should be the same with the ref entity field name and shouldn't make as required and no other property
21
- *
22
- *
23
- */
24
- const field_attrs = ["name", "type", "required", "ref", "link", "delete", "create", "list", "search", "update", "clone", "sys", "secure", "group", "view"];
25
- const meta_attrs = ["collection", "roles", "primary_keys", "fields", "creatable", "readable", "updatable", "deleteable", "cloneable", "after_read", "list_query",
26
- "before_create", "after_create", "before_clone", "after_clone", "before_update", "after_update", "before_delete", "after_delete", "create", "clone", "update", "batch_update", "after_batch_update", "delete",
27
- "ref_label", "ref_filter", "route", "user_field"];
28
-
29
- const DELETE_MODE = Object.freeze({
30
- all: ["keep", "cascade"],
31
- keep: "keep",
32
- cascade: "cascade"
33
- });
37
+
38
+ const FIELD_ATTRS = ["name", "type", "required", "default", "ref", "link", "delete", "create", "list", "search", "update", "clone", "sys", "secure", "group", "view"];
39
+ const LINK_FIELD_ATTRS = ["name", "link", "list"];
40
+ const CALLBACK_NAMES = ["after_read", "list_query", "before_create", "before_clone", "before_update", "before_delete",
41
+ "after_create", "after_clone", "after_update", "after_delete", "create", "clone", "update", "batch_update", "after_batch_update", "delete"];
42
+ const OPERATION_FLAGS = ["creatable", "readable", "updatable", "deleteable", "cloneable", "importable", "exportable"];
43
+ const MODE_MAP = { creatable: "c", readable: "rs", updatable: "u", deleteable: "db", cloneable: "o", importable: "i", exportable: "e" };
44
+
45
+ const META_ATTRS = ["collection", "roles", "primary_keys", "fields", ...OPERATION_FLAGS, ...CALLBACK_NAMES, "ref_label", "ref_filter", "route", "user_field"];
46
+ const DELETE_MODE = Object.freeze({ all: ["keep", "cascade"], keep: "keep", cascade: "cascade" });
47
+
48
+ /** Convert fields array to name-keyed map. */
49
+ const to_fields_map = (fields) => fields.reduce((m, f) => { m[f.name] = f; return m; }, {});
50
+
51
+ /** Format error message with meta context. */
52
+ const meta_error = (collection, msg, field = null) => {
53
+ const prefix = field ? `meta:${collection},field:${field.name}` : `meta:${collection}`;
54
+ return new Error(`${prefix} ${msg}`);
55
+ };
34
56
 
35
57
  /**
36
- * Validate the field attributes and keep them correct(also set default value)
37
- * @param {entity meta} meta
38
- * @param {meta attribute field} field
58
+ * Validate and normalize field definition.
59
+ * @param {Object} meta - Entity meta.
60
+ * @param {Object} field - Field definition.
61
+ * @throws {Error} If field configuration is invalid.
39
62
  */
40
63
  const validate_field = (meta, field) => {
41
- if (!field.name) {
42
- throw new Error("name attr is required for field:" + field + ", and meta:" + meta.collection);
43
- }
64
+ if (!field.name) throw meta_error(meta.collection, `name attr required for field:${JSON.stringify(field)}`);
44
65
 
45
- if (field.type) {
46
- get_type(field.type);
47
- } else {
48
- if (!field.link) {
49
- field.type = "string"
50
- }
51
- }
66
+ if (field.type) get_type(field.type);
67
+ else if (!field.link) field.type = "string";
52
68
 
53
- if (meta.primary_keys.includes(field.name)) {
54
- field.required = true;
69
+ if (meta.primary_keys.includes(field.name)) field.required = true;
70
+
71
+ // Validate default value against field type
72
+ if (field.default !== undefined && field.type) {
73
+ const type = get_type(field.type);
74
+ const result = type.convert(field.default);
75
+ if (result.err) throw meta_error(meta.collection, `invalid default value [${field.default}] for type [${field.type}]`, field);
55
76
  }
56
77
 
57
78
  if (field.ref && !field.link) {
58
79
  const ref_meta = meta_manager[field.ref];
59
- if (!ref_meta) {
60
- throw new Error("meta:" + meta.collection + ",field:" + field.name + " refers invalid meta:" + field.ref + "]");
61
- }
80
+ if (!ref_meta) throw meta_error(meta.collection, `refers invalid meta:${field.ref}`, field);
81
+ if (!ref_meta.ref_label) throw meta_error(meta.collection, `refers meta:${field.ref} without ref_label`, field);
62
82
 
63
- if (!ref_meta.ref_label) {
64
- throw new Error("meta:" + meta.collection + ",field:" + field.name + " refers an meta:" + field.ref + " without ref_label");
65
- }
66
-
67
- const ref_by_collections = ref_meta.ref_by_metas.map(m => m.collection);
68
- if (!ref_by_collections.includes(this.collection)) {
69
- ref_meta.ref_by_metas.push(meta_manager[meta.collection]);
70
- }
83
+ const ref_by_collections = ref_meta.ref_by_metas.map((m) => m.collection);
84
+ if (!ref_by_collections.includes(this.collection)) ref_meta.ref_by_metas.push(meta_manager[meta.collection]);
71
85
  }
72
86
 
73
87
  if (field.delete) {
74
- if (!field.ref) {
75
- throw new Error("meta:" + meta.collection + ",field:" + field.name + " doesn't let define delete in none ref field.");
76
- }
77
-
78
- const all_modes = DELETE_MODE.all;
79
- if (!all_modes.includes(field.delete)) {
80
- throw new Error("meta:" + meta.collection + ",field:" + field.name + " has invalid delete:" + field.delete + ", valid values:" + JSON.stringify(all_modes));
81
- }
88
+ if (!field.ref) throw meta_error(meta.collection, `delete not allowed on non-ref field`, field);
89
+ if (!DELETE_MODE.all.includes(field.delete)) throw meta_error(meta.collection, `invalid delete:${field.delete}, valid:${JSON.stringify(DELETE_MODE.all)}`, field);
82
90
  }
83
91
 
84
92
  if (field.link) {
85
- const keys = Object.keys(field);
86
- const support_keys_for_links = ["name", "link", "list"]
87
- keys.forEach(key => {
88
- if (!support_keys_for_links.includes(key)) {
89
- throw new Error("Link field just supports name, link,list property. The attribute [" + key + "] isn't supported for LINK field:" + JSON.stringify(field) + " and meta:" + meta.collection);
90
- }
91
- });
93
+ const invalid_keys = Object.keys(field).filter((k) => !LINK_FIELD_ATTRS.includes(k));
94
+ if (invalid_keys.length) throw meta_error(meta.collection, `Link field only supports ${LINK_FIELD_ATTRS.join(",")}. Unsupported:${invalid_keys.join(",")}`);
92
95
  }
93
96
 
94
- const editable = (field.create != false) || (field.update != false);
95
-
96
- if (field.view) {
97
- if (!editable) {
98
- throw new Error("view can just defined for editable (create/update) field only. Field:" + JSON.stringify(field) + "] and meta:" + meta.collection);
99
- }
100
- } else {
101
- //no view defined for create/update field, then set view to default value "0"
102
- if (editable) {
103
- field.view = "0";
104
- }
105
- }
97
+ const editable = (field.create !== false) || (field.update !== false);
98
+ if (field.view && !editable) throw meta_error(meta.collection, `view only for editable fields`);
99
+ if (!field.view && editable) field.view = "*";
106
100
 
107
- const keys = Object.keys(field);
108
- keys.forEach(key => {
109
- if (!field_attrs.includes(key)) {
110
- throw new Error("The attribute [" + key + "] isn't supported now for field:" + JSON.stringify(field) + " and meta:" + meta.collection);
111
- }
112
- });
113
- }
101
+ const invalid_attrs = Object.keys(field).filter((k) => !FIELD_ATTRS.includes(k));
102
+ if (invalid_attrs.length) throw meta_error(meta.collection, `Unsupported attribute [${invalid_attrs.join(",")}] for field:${JSON.stringify(field)}`);
103
+ };
114
104
 
115
105
  /**
116
- * Validate all the fields
117
- * @param {entity meta} meta
118
- * @param {meta fields} fields
106
+ * Validate all fields in meta definition.
107
+ * @param {Object} meta - Entity meta.
108
+ * @param {Object[]} fields - Field definitions.
109
+ * @returns {boolean} True if valid.
110
+ * @throws {Error} If validation fails.
119
111
  */
120
112
  const validate_fields = (meta, fields) => {
121
- const fields_map = fields.reduce((map, field) => { map[field.name] = field; return map; }, {});
122
- const check_duplicate_field_names = [];
113
+ const fields_map = to_fields_map(fields);
114
+ const seen_names = new Set();
123
115
 
124
- fields.forEach(field => {
116
+ for (const field of fields) {
125
117
  validate_field(meta, field);
126
- if (check_duplicate_field_names.includes(field.name)) {
127
- throw new Error("Duplicate field defined [" + JSON.stringify(field) + "] for meta:" + meta.collection);
128
- } else {
129
- check_duplicate_field_names.push(field.name);
130
- }
118
+
119
+ if (seen_names.has(field.name)) throw meta_error(meta.collection, `Duplicate field [${JSON.stringify(field)}]`);
120
+ seen_names.add(field.name);
121
+
131
122
  if (field.link) {
132
123
  const link_field = fields_map[field.link];
133
- if (!link_field) {
134
- throw new Error("link field [" + JSON.stringify(field) + "] should link to one field defined in meta:" + meta.collection);
135
- } else {
136
- if (!link_field.ref) {
137
- throw new Error("link field [" + JSON.stringify(field) + "] link to field [" + JSON.stringify(link_field) + "] should ref to one entity in meta:" + meta.collection);
138
- }
139
- const entity = get_entity_meta(link_field.ref);
140
- const link_entity_field = entity.fields_map[field.name];
141
- if (!link_entity_field) {
142
- throw new Error("link field [" + JSON.stringify(field) + "] should link to one field defined in meta:" + entity.collection);
143
- }
124
+ if (!link_field) throw meta_error(meta.collection, `link field [${JSON.stringify(field)}] should link to field`);
125
+ if (!link_field.ref) throw meta_error(meta.collection, `link field [${JSON.stringify(field)}] target [${JSON.stringify(link_field)}] must ref entity`);
144
126
 
145
- //set type to link field type
146
- field.type = link_entity_field.type;
147
- field.required = false;
148
- field.create = false;
149
- field.search = false;
150
- field.update = false;
151
- field.clone = false;
152
- field.delete = "cascade";
153
-
154
- if (link_entity_field.ref) {
155
- field.ref = link_entity_field.ref;
156
- }
157
- }
158
- }
159
- });
127
+ const entity = get_entity_meta(link_field.ref);
128
+ const link_entity_field = entity.fields_map[field.name];
129
+ if (!link_entity_field) throw meta_error(entity.collection, `link field [${JSON.stringify(field)}] should link to field`);
160
130
 
131
+ Object.assign(field, { type: link_entity_field.type, required: false, create: false, search: false, update: false, clone: false, delete: "cascade" });
132
+ if (link_entity_field.ref) field.ref = link_entity_field.ref;
133
+ }
134
+ }
161
135
  return true;
162
- }
136
+ };
163
137
 
164
- /**
165
- * Validate all the metas information after loading all the meta information
166
- */
167
- const validate_all_metas = () => {
168
- const metas = Object.keys(meta_manager);
169
- metas.forEach(meta_name => {
170
- const meta = meta_manager[meta_name];
171
- meta.validate_meta_info();
172
- });
173
- }
138
+ /** Validate all registered metas after loading. */
139
+ const validate_all_metas = () => Object.values(meta_manager).forEach((m) => m.validate_meta_info());
174
140
 
175
141
  /**
176
- * Set function name for the entity
177
- * @param {entity meta} entity_meta
178
- * @param {cb function name} cb_name
179
- * @param {callback function} cb
142
+ * Set callback function on entity meta.
143
+ * @param {Object} entity_meta - Entity meta object.
144
+ * @param {string} cb_name - Callback name.
145
+ * @param {Function} cb - Callback function.
146
+ * @throws {Error} If callback is not a function.
180
147
  */
181
148
  const set_callback = (entity_meta, cb_name, cb) => {
182
- if (cb) {
183
- if (cb instanceof Function) {
184
- entity_meta[cb_name] = cb;
185
- } else {
186
- throw new Error("callback [" + cb_name + "] configured for meta:" + meta.collection + " isn't function object");
187
- }
188
- }
189
- }
149
+ if (!cb) return;
150
+ if (!(cb instanceof Function)) throw new Error(`callback [${cb_name}] for meta:${entity_meta.collection} isn't function`);
151
+ entity_meta[cb_name] = cb;
152
+ };
190
153
 
191
- /**
192
- * Get entity meta object
193
- * @param {meta collection name} collection
194
- * @returns
195
- */
196
- const get_entity_meta = (collection) => {
197
- return meta_manager[collection];
198
- }
154
+ /** Get entity meta by collection name. */
155
+ const get_entity_meta = (collection) => meta_manager[collection];
199
156
 
200
- /**
201
- * Get all the meta name
202
- * @returns
203
- */
204
- const get_all_metas = () => {
205
- return Object.keys(meta_manager);
206
- }
157
+ /** Get all registered meta collection names. */
158
+ const get_all_metas = () => Object.keys(meta_manager);
207
159
 
208
160
 
209
161
  /**
210
- * Wrap the meta info from user side:
211
- * 1) validate the meta structure and keep it is valid
212
- * 2) set the default values of the meta
162
+ * Entity Meta wrapper class.
163
+ * Validates meta structure and sets default values.
213
164
  */
214
165
  class EntityMeta {
215
-
166
+ /**
167
+ * Create EntityMeta instance.
168
+ * @param {Object} meta - Meta definition object.
169
+ * @throws {Error} If duplicate meta registered.
170
+ */
216
171
  constructor(meta) {
217
172
  this.meta = meta;
218
- this.collection = this.meta.collection;
219
- this.roles = this.meta.roles;
220
-
221
- this.creatable = is_undefined(meta.creatable) ? false : meta.creatable;
222
- this.readable = is_undefined(meta.readable) ? false : meta.readable;
223
- this.updatable = is_undefined(meta.updatable) ? false : meta.updatable;
224
- this.deleteable = is_undefined(meta.deleteable) ? false : meta.deleteable;
225
- this.cloneable = is_undefined(meta.cloneable) ? false : meta.cloneable;
226
- this.importable = is_undefined(meta.importable) ? false : meta.importable;
227
- this.exportable = is_undefined(meta.exportable) ? false : meta.exportable;
173
+ this.collection = meta.collection;
174
+ this.roles = meta.roles;
175
+ this.primary_keys = meta.primary_keys;
176
+ this.user_field = meta.user_field;
177
+ this.ref_label = meta.ref_label;
178
+ this.ref_filter = meta.ref_filter;
179
+
180
+ // Set operation flags with defaults
181
+ OPERATION_FLAGS.forEach((flag) => { this[flag] = meta[flag] ?? false; });
228
182
  this.editable = this.creatable || this.updatable;
229
183
 
230
- //b:batch mode, c:create, d:delete, e:export, i:import, o:clone, p:page, r: refresh, s:search, u:update
231
- const modes = [];
232
- this.creatable && (modes.push("c"));
233
- //prefer the infinite scrolling mode, so doesn't include p mode
234
- this.readable && (modes.push("rs"));
235
- this.updatable && (modes.push("u"));
236
- this.deleteable && (modes.push("db"));
237
- this.cloneable && (modes.push("o"));
238
- this.importable && (modes.push("i"));
239
- this.exportable && (modes.push("e"));
240
- this.mode = modes.join("");
241
-
242
- this.ref_label = this.meta.ref_label;
243
- this.ref_filter = this.meta.ref_filter;
244
- this.ref_fields = this.meta.fields.filter(field => field.ref);
245
- this.ref_by_metas = [];
184
+ // Build mode string
185
+ this.mode = OPERATION_FLAGS.filter((f) => this[f]).map((f) => MODE_MAP[f]).join("");
246
186
 
247
- this.link_fields = this.meta.fields.filter(field => field.link);
248
- this.fields_map = meta.fields.reduce((map, field) => { map[field.name] = field; return map; }, {});
187
+ // Field organization
249
188
  this.fields = meta.fields;
250
- this.primary_keys = meta.primary_keys;
251
- this.field_names = this.fields.map(field => field.name);
252
- this.user_field = meta.user_field;
189
+ this.fields_map = to_fields_map(meta.fields);
190
+ this.field_names = this.fields.map((f) => f.name);
191
+ this._init_field_subsets(meta);
253
192
 
254
- this.client_fields = this.fields.filter(field => field.sys != true);
255
- this.property_fields = this.fields.filter(field => field.sys != true && field.secure != true);
256
- this.create_fields = this.fields.filter(field => field.create != false && field.sys != true);
257
- this.update_fields = this.fields.filter(field => field.create != false && field.update != false && field.sys != true);
258
- this.search_fields = this.fields.filter(field => field.search != false && field.sys != true);
259
- this.clone_fields = this.fields.filter(field => field.clone != false && field.sys != true);
260
- this.list_fields = this.fields.filter(field => field.list != false && field.sys != true && field.secure != true);
261
- this.primary_key_fields = this.fields.filter(field => meta.primary_keys.includes(field.name));
262
- this.required_field_names = this.fields.filter(field => field.required == true || this.primary_keys.includes(field.name)).map(field => field.name);
263
-
264
- this.file_fields = meta.fields.filter(f => f.type === 'file');
265
- this.upload_fields = this.file_fields && this.file_fields.length > 0 ? this.file_fields.map(f => ({ name: f.name })) : [];
266
-
267
- set_callback(this, "after_read", meta.after_read);
268
- set_callback(this, "list_query", meta.list_query);
269
- set_callback(this, "before_create", meta.before_create);
270
- set_callback(this, "before_clone", meta.before_clone);
271
- set_callback(this, "before_update", meta.before_update);
272
- set_callback(this, "before_delete", meta.before_delete);
273
- set_callback(this, "after_create", meta.after_create);
274
- set_callback(this, "after_clone", meta.after_clone);
275
- set_callback(this, "after_update", meta.after_update);
276
- set_callback(this, "after_delete", meta.after_delete);
277
- set_callback(this, "create", meta.create);
278
- set_callback(this, "clone", meta.clone);
279
- set_callback(this, "update", meta.update);
280
- set_callback(this, "batch_update", meta.batch_update);
281
- set_callback(this, "after_batch_update", meta.after_batch_update);
282
- set_callback(this, "delete", meta.delete);
283
-
284
- if (meta_manager[meta.collection]) {
285
- throw new Error("Duplicate meta info:" + this.collection);
286
- } else {
287
- meta_manager[meta.collection] = this;
288
- }
193
+ // Reference handling
194
+ this.ref_fields = this.fields.filter((f) => f.ref);
195
+ this.link_fields = this.fields.filter((f) => f.link);
196
+ this.ref_by_metas = [];
197
+
198
+ // Set callbacks and register meta
199
+ CALLBACK_NAMES.forEach((cb) => set_callback(this, cb, meta[cb]));
200
+ if (meta_manager[meta.collection]) throw new Error(`Duplicate meta info:${this.collection}`);
201
+ meta_manager[meta.collection] = this;
289
202
  }
290
203
 
291
- validate_meta_info() {
292
- if (!this.collection) {
293
- throw new Error("no collection defined for meta:" + this.meta);
294
- }
204
+ /** Initialize field subsets for different operations. @private */
205
+ _init_field_subsets(meta) {
206
+ const not_sys = (f) => f.sys !== true;
207
+ const not_secure = (f) => f.secure !== true;
208
+
209
+ this.client_fields = this.fields.filter(not_sys);
210
+ this.property_fields = this.fields.filter((f) => not_sys(f) && not_secure(f));
211
+ this.create_fields = this.fields.filter((f) => f.create !== false && not_sys(f));
212
+ this.update_fields = this.fields.filter((f) => f.create !== false && f.update !== false && not_sys(f));
213
+ this.search_fields = this.fields.filter((f) => f.search !== false && not_sys(f));
214
+ this.clone_fields = this.fields.filter((f) => f.clone !== false && not_sys(f));
215
+ this.list_fields = this.fields.filter((f) => f.list !== false && not_sys(f) && not_secure(f));
216
+ this.primary_key_fields = this.fields.filter((f) => meta.primary_keys.includes(f.name));
217
+ this.required_field_names = this.fields.filter((f) => f.required === true || this.primary_keys.includes(f.name)).map((f) => f.name);
218
+ this.file_fields = this.fields.filter((f) => f.type === 'file');
219
+ this.upload_fields = this.file_fields.map((f) => ({ name: f.name }));
220
+ }
295
221
 
296
- const keys = Object.keys(this.meta);
297
- keys.forEach(key => {
298
- if (!meta_attrs.includes(key)) {
299
- throw new Error("The attribute [" + key + "] isn't supported now for meta:" + this.meta.collection);
300
- }
301
- });
302
-
303
- if (!this.primary_keys) {
304
- throw new Error("no primary_keys configured for meta:" + this.collection);
305
- } else if (!Array.isArray(this.primary_keys)) {
306
- throw new Error("primary_keys of meta [" + this.collection + "] should be array");
307
- } else {
308
- this.primary_keys.forEach(key => {
309
- if (!this.field_names.includes(key)) {
310
- throw new Error("wrong primary_key " + key + " configured in meta:" + this.collection);
311
- }
312
- });
313
- }
222
+ /**
223
+ * Validate meta information.
224
+ * @returns {boolean} True if valid.
225
+ * @throws {Error} If validation fails.
226
+ */
227
+ validate_meta_info() {
228
+ if (!this.collection) throw new Error(`no collection defined for meta:${JSON.stringify(this.meta)}`);
314
229
 
315
- if (this.roles) {
316
- if (!Array.isArray(this.roles)) {
317
- throw new Error("roles of meta [" + this.collection + "] should be array");
318
- }
230
+ const invalid_attrs = Object.keys(this.meta).filter((k) => !META_ATTRS.includes(k));
231
+ if (invalid_attrs.length) throw meta_error(this.collection, `Unsupported attribute [${invalid_attrs.join(",")}]`);
319
232
 
320
- this.roles.forEach(role => {
321
- const role_config = role.split(":");
322
- if (!(role_config.length == 2 || role_config.length == 3)) {
323
- throw new Error("wrong role config [" + role + "] in meta [" + this.collection + "]. You should use : to seperate the role name with mode.");
324
- }
233
+ this._validate_primary_keys();
234
+ this._validate_roles();
235
+ this._validate_field_exists("ref_label", this.ref_label);
236
+ this._validate_field_exists("user_field", this.user_field);
237
+ if (this.ref_filter && this.ref_filter.constructor !== Object) {
238
+ throw meta_error(this.collection, `ref_filter should be object`);
239
+ }
325
240
 
326
- const role_name = role_config[0];
327
- if (!validate_meta_role(role_name)) {
328
- throw new Error("role [" + role_name + "] in meta [" + this.collection + "] not defined in setting's role config.");
329
- }
241
+ return validate_fields(this.meta, this.fields);
242
+ }
330
243
 
331
- const role_mode = role_config[1];
332
- if (role_mode != "*") {
333
- for (let i = 0; i < role_mode.length; i++) {
334
- const mode = role_mode.charAt(i);
335
- if (!this.mode.includes(mode)) {
336
- throw new Error("role [" + role_name + "] in meta [" + this.collection + "] with mode [" + mode + "] doesn't comply with entity mode [" + this.mode + "]");
337
- }
338
- }
339
- }
340
- });
244
+ /** Validate primary keys configuration. @private */
245
+ _validate_primary_keys() {
246
+ if (!this.primary_keys) throw meta_error(this.collection, `no primary_keys defined`);
247
+ if (!Array.isArray(this.primary_keys)) throw meta_error(this.collection, `primary_keys should be array`);
248
+ for (const key of this.primary_keys) {
249
+ if (!this.field_names.includes(key)) throw meta_error(this.collection, `wrong primary_key ${key}`);
341
250
  }
251
+ }
342
252
 
343
- if (this.ref_label && !this.field_names.includes(this.ref_label)) {
344
- throw new Error("ref_label [" + this.ref_label + "] configured in meta:" + this.collection + " not found in field names");
253
+ /** Validate a field name exists in fields. @private */
254
+ _validate_field_exists(attr_name, value) {
255
+ if (value && !this.field_names.includes(value)) {
256
+ throw meta_error(this.collection, `${attr_name} [${value}] not found in fields`);
345
257
  }
258
+ }
346
259
 
347
- if (this.user_field && !this.field_names.includes(this.user_field)) {
348
- throw new Error("user_field [" + this.user_field + "] configured in meta:" + this.collection + " not found in field names");
349
- }
260
+ /** Validate roles configuration. @private */
261
+ _validate_roles() {
262
+ if (!this.roles) return;
263
+ if (!Array.isArray(this.roles)) throw meta_error(this.collection, `roles should be array`);
350
264
 
351
- if (this.ref_filter && (this.ref_filter.constructor != Object)) {
352
- throw new Error("ref_filter [" + this.ref_filter + "] configured in meta:" + this.collection + " should be object");
353
- }
265
+ for (const role of this.roles) {
266
+ const parts = role.split(":");
267
+ if (parts.length < 2 || parts.length > 3) {
268
+ throw meta_error(this.collection, `wrong role config [${role}]. Use : to separate role name with mode.`);
269
+ }
354
270
 
355
- return validate_fields(this.meta, this.fields);
271
+ const [role_name, role_mode] = parts;
272
+ if (!validate_meta_role(role_name)) throw meta_error(this.collection, `role [${role_name}] not defined in settings`);
273
+
274
+ if (role_mode !== "*") {
275
+ for (const mode of role_mode) {
276
+ if (!this.mode.includes(mode)) throw meta_error(this.collection, `role [${role_name}] mode [${mode}] doesn't match entity mode [${this.mode}]`);
277
+ }
278
+ }
279
+ }
356
280
  }
357
281
  }
358
282
 
359
- module.exports = { EntityMeta, validate_all_metas, get_entity_meta, get_all_metas, DELETE_MODE }
283
+ module.exports = { EntityMeta, validate_all_metas, get_entity_meta, get_all_metas, DELETE_MODE };
package/core/msg.js CHANGED
@@ -1,11 +1,24 @@
1
+ /**
2
+ * @fileoverview Message sending utility functions using wxmnode.
3
+ * @module core/msg
4
+ */
5
+
1
6
  const wxm = require('wxmnode');
2
7
 
3
- const init_wxm = (name, password) => {
4
- wxm.init(name, password);
5
- }
8
+ /**
9
+ * Initialize wxm with credentials.
10
+ * @param {string} name - Account name.
11
+ * @param {string} password - Account password.
12
+ */
13
+ const init_wxm = (name, password) => wxm.init(name, password);
6
14
 
7
- const send_msg = async (content, type, detail) => {
8
- return await wxm.sendMsg(content, type, detail);
9
- }
15
+ /**
16
+ * Send message via wxm.
17
+ * @param {string} content - Message content.
18
+ * @param {string} type - Message type.
19
+ * @param {Object} detail - Additional message details.
20
+ * @returns {Promise<Object>} Message send result.
21
+ */
22
+ const send_msg = async (content, type, detail) => await wxm.sendMsg(content, type, detail);
10
23
 
11
- module.exports = { init_wxm, send_msg }
24
+ module.exports = { init_wxm, send_msg };