hola-server 1.0.10 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -1
- package/core/array.js +79 -142
- package/core/bash.js +208 -259
- package/core/chart.js +26 -16
- package/core/cron.js +14 -3
- package/core/date.js +15 -44
- package/core/encrypt.js +19 -9
- package/core/file.js +42 -29
- package/core/lhs.js +32 -6
- package/core/meta.js +213 -289
- package/core/msg.js +20 -7
- package/core/number.js +105 -103
- package/core/obj.js +15 -12
- package/core/random.js +9 -6
- package/core/role.js +69 -77
- package/core/thread.js +12 -2
- package/core/type.js +300 -261
- package/core/url.js +20 -12
- package/core/validate.js +29 -26
- package/db/db.js +297 -227
- package/db/entity.js +631 -963
- package/db/gridfs.js +120 -166
- package/design/add_default_field_attr.md +56 -0
- package/http/context.js +22 -8
- package/http/cors.js +25 -8
- package/http/error.js +27 -9
- package/http/express.js +70 -41
- package/http/params.js +70 -42
- package/http/router.js +51 -40
- package/http/session.js +59 -36
- package/index.js +85 -9
- package/package.json +2 -2
- package/router/clone.js +28 -36
- package/router/create.js +21 -26
- package/router/delete.js +24 -28
- package/router/read.js +137 -123
- package/router/update.js +38 -56
- package/setting.js +22 -6
- package/skills/array.md +155 -0
- package/skills/bash.md +91 -0
- package/skills/chart.md +54 -0
- package/skills/code.md +422 -0
- package/skills/context.md +177 -0
- package/skills/date.md +58 -0
- package/skills/express.md +255 -0
- package/skills/file.md +60 -0
- package/skills/lhs.md +54 -0
- package/skills/meta.md +1023 -0
- package/skills/msg.md +30 -0
- package/skills/number.md +88 -0
- package/skills/obj.md +36 -0
- package/skills/params.md +206 -0
- package/skills/random.md +22 -0
- package/skills/role.md +59 -0
- package/skills/session.md +281 -0
- package/skills/storage.md +743 -0
- package/skills/thread.md +22 -0
- package/skills/type.md +547 -0
- package/skills/url.md +34 -0
- package/skills/validate.md +48 -0
- package/test/cleanup/close-db.js +5 -0
- package/test/core/array.js +226 -0
- package/test/core/chart.js +51 -0
- package/test/core/file.js +59 -0
- package/test/core/lhs.js +44 -0
- package/test/core/number.js +167 -12
- package/test/core/obj.js +47 -0
- package/test/core/random.js +24 -0
- package/test/core/thread.js +20 -0
- package/test/core/type.js +216 -0
- package/test/core/validate.js +67 -0
- package/test/db/db-ops.js +99 -0
- package/test/db/pipe_test.txt +0 -0
- package/test/db/test_case_design.md +528 -0
- package/test/db/test_db_class.js +613 -0
- package/test/db/test_entity_class.js +414 -0
- package/test/db/test_gridfs_class.js +234 -0
- package/test/entity/create.js +1 -1
- package/test/entity/delete-mixed.js +156 -0
- package/test/entity/ref-filter.js +63 -0
- package/tool/gen_i18n.js +55 -21
- package/test/crud/router.js +0 -99
- package/test/router/user.js +0 -17
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
# Storage System Skill
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Hola framework provides a comprehensive dual storage system built on MongoDB:
|
|
6
|
+
|
|
7
|
+
1. **Entity System** (`hola-server/db/entity.js`): High-level, metadata-driven document storage with automatic validation, type conversion, reference resolution, and lifecycle hooks.
|
|
8
|
+
2. **GridFS System** (`hola-server/db/gridfs.js`): Large file storage for handling binary content like images, documents, and media files.
|
|
9
|
+
|
|
10
|
+
Both systems integrate seamlessly to provide complete data persistence for Hola applications.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. Entity System
|
|
15
|
+
|
|
16
|
+
### 1.1 Overview
|
|
17
|
+
|
|
18
|
+
The `Entity` class provides a meta-driven abstraction layer over MongoDB collections. It automatically handles:
|
|
19
|
+
|
|
20
|
+
- **Type Conversion**: Converts field values based on meta type definitions
|
|
21
|
+
- **Validation**: Required fields, type validation, reference validation
|
|
22
|
+
- **Reference Resolution**: Converts between user-friendly labels and database IDs
|
|
23
|
+
- **Link Population**: Auto-fetches related data from referenced entities
|
|
24
|
+
- **Lifecycle Hooks**: Custom logic at key points (before/after create, update, delete)
|
|
25
|
+
- **Cascade Operations**: Automatic cleanup of related records
|
|
26
|
+
|
|
27
|
+
### 1.2 Import
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
const { Entity } = require("hola-server/db/entity");
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### 1.3 Constructor
|
|
34
|
+
|
|
35
|
+
> **Note:** Throughout this documentation, we use hypothetical entities like "product", "user", and "category" for illustrative purposes. Replace these with your actual entity collection names defined in your meta definitions.
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
// Method 1: Create from meta object
|
|
39
|
+
const meta = get_entity_meta("your_entity_name");
|
|
40
|
+
const entity = new Entity(meta);
|
|
41
|
+
|
|
42
|
+
// Method 2: Create from collection name (recommended)
|
|
43
|
+
const entity = new Entity("your_entity_name");
|
|
44
|
+
|
|
45
|
+
// Example with hypothetical "product" entity
|
|
46
|
+
const productEntity = new Entity("product");
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 2. Entity CRUD Operations
|
|
52
|
+
|
|
53
|
+
### 2.1 Create Entity
|
|
54
|
+
|
|
55
|
+
**Method:** `create_entity(param_obj, view)`
|
|
56
|
+
|
|
57
|
+
Creates a new document with full validation and hooks.
|
|
58
|
+
|
|
59
|
+
**Parameters:**
|
|
60
|
+
- `param_obj` (Object): Entity data from client
|
|
61
|
+
- `view` (string): View filter (typically `"*"` for all fields)
|
|
62
|
+
|
|
63
|
+
**Returns:** `{code, err?}`
|
|
64
|
+
|
|
65
|
+
**Process Flow:**
|
|
66
|
+
1. Filter fields by view
|
|
67
|
+
2. Convert types using `convert_type()`
|
|
68
|
+
3. Run `before_create` hook
|
|
69
|
+
4. Validate required fields
|
|
70
|
+
5. Check for duplicate primary keys
|
|
71
|
+
6. Validate reference fields
|
|
72
|
+
7. Run `create` hook or insert document
|
|
73
|
+
8. Run `after_create` hook
|
|
74
|
+
|
|
75
|
+
**Example:**
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
const productEntity = new Entity("product");
|
|
79
|
+
|
|
80
|
+
const result = await productEntity.create_entity({
|
|
81
|
+
name: "iPhone 15",
|
|
82
|
+
price: 999.99,
|
|
83
|
+
category: "Electronics", // Will be resolved to category ObjectId
|
|
84
|
+
stock: 50
|
|
85
|
+
}, "*");
|
|
86
|
+
|
|
87
|
+
if (result.code === SUCCESS) {
|
|
88
|
+
console.log("Product created successfully");
|
|
89
|
+
} else {
|
|
90
|
+
console.error("Creation failed:", result.err);
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Common Error Codes:**
|
|
95
|
+
- `NO_PARAMS`: Missing required fields
|
|
96
|
+
- `INVALID_PARAMS`: Type conversion failed
|
|
97
|
+
- `DUPLICATE_KEY`: Primary key already exists
|
|
98
|
+
- `REF_NOT_FOUND`: Referenced entity doesn't exist
|
|
99
|
+
- `REF_NOT_UNIQUE`: Multiple entities match reference label
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
### 2.2 Read Entity
|
|
104
|
+
|
|
105
|
+
**Method:** `read_entity(_id, attr_names, view)`
|
|
106
|
+
|
|
107
|
+
Reads a single document with automatic reference and link resolution.
|
|
108
|
+
|
|
109
|
+
**Parameters:**
|
|
110
|
+
- `_id` (string): Entity ObjectId
|
|
111
|
+
- `attr_names` (string): Comma-separated field names to fetch
|
|
112
|
+
- `view` (string): View filter
|
|
113
|
+
|
|
114
|
+
**Returns:** `{code, data?, err?}`
|
|
115
|
+
|
|
116
|
+
**Process Flow:**
|
|
117
|
+
1. Validate _id parameter
|
|
118
|
+
2. Filter property fields by view
|
|
119
|
+
3. Extract requested attributes, ref fields, and link fields
|
|
120
|
+
4. Find document
|
|
121
|
+
5. Run `after_read` hook
|
|
122
|
+
6. Populate link fields from referenced entities
|
|
123
|
+
7. Convert ref ObjectIds to ref_labels
|
|
124
|
+
|
|
125
|
+
**Example:**
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
const result = await productEntity.read_entity(
|
|
129
|
+
"507f1f77bcf86cd799439011",
|
|
130
|
+
"name,price,category,category_code",
|
|
131
|
+
"*"
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
if (result.code === SUCCESS) {
|
|
135
|
+
console.log(result.data);
|
|
136
|
+
// {
|
|
137
|
+
// _id: "507f1f77bcf86cd799439011",
|
|
138
|
+
// name: "iPhone 15",
|
|
139
|
+
// price: 999.99,
|
|
140
|
+
// category: "Electronics", // Converted from ObjectId to label
|
|
141
|
+
// category_code: "ELEC-001" // Link field from category entity
|
|
142
|
+
// }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Read Property vs Read Entity:**
|
|
147
|
+
|
|
148
|
+
Use `read_property()` when you don't need reference conversion (faster):
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
const result = await productEntity.read_property(
|
|
152
|
+
"507f1f77bcf86cd799439011",
|
|
153
|
+
"name,price",
|
|
154
|
+
"*"
|
|
155
|
+
);
|
|
156
|
+
// Returns raw data without ref/link processing
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### 2.3 Update Entity
|
|
162
|
+
|
|
163
|
+
**Method:** `update_entity(_id, param_obj, view)`
|
|
164
|
+
|
|
165
|
+
Updates an existing document.
|
|
166
|
+
|
|
167
|
+
**Parameters:**
|
|
168
|
+
- `_id` (string|null): Entity ObjectId, or null to use primary keys from param_obj
|
|
169
|
+
- `param_obj` (Object): Update data
|
|
170
|
+
- `view` (string): View filter
|
|
171
|
+
|
|
172
|
+
**Returns:** `{code, err?}`
|
|
173
|
+
|
|
174
|
+
**Process Flow:**
|
|
175
|
+
1. Filter update fields by view
|
|
176
|
+
2. Convert types using `convert_update_type()` (preserves empty values)
|
|
177
|
+
3. Run `before_update` hook
|
|
178
|
+
4. Build query (by _id or primary keys)
|
|
179
|
+
5. Verify entity exists (count must be 1)
|
|
180
|
+
6. Validate reference fields
|
|
181
|
+
7. Run `update` hook or perform update
|
|
182
|
+
8. Run `after_update` hook
|
|
183
|
+
|
|
184
|
+
**Example:**
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
// Update by ID
|
|
188
|
+
const result = await productEntity.update_entity(
|
|
189
|
+
"507f1f77bcf86cd799439011",
|
|
190
|
+
{ price: 899.99, stock: 45 },
|
|
191
|
+
"*"
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Update by primary key (if _id is null)
|
|
195
|
+
const result2 = await productEntity.update_entity(
|
|
196
|
+
null,
|
|
197
|
+
{ sku: "IPHONE-15", price: 899.99 }, // sku is primary key
|
|
198
|
+
"*"
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Update vs Batch Update:**
|
|
203
|
+
|
|
204
|
+
For updating multiple entities at once, use `batch_update_entity()`:
|
|
205
|
+
|
|
206
|
+
```javascript
|
|
207
|
+
const ids = ["507f...", "608a...", "709b..."];
|
|
208
|
+
await productEntity.batch_update_entity(ids, { discount: 0.1 }, "*");
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### 2.4 Delete Entity
|
|
214
|
+
|
|
215
|
+
**Method:** `delete_entity(id_array)`
|
|
216
|
+
|
|
217
|
+
Deletes one or more documents with reference checking and cascade delete.
|
|
218
|
+
|
|
219
|
+
**Parameters:**
|
|
220
|
+
- `id_array` (string[]): Array of entity ObjectIds
|
|
221
|
+
|
|
222
|
+
**Returns:** `{code, err?}`
|
|
223
|
+
|
|
224
|
+
**Process Flow:**
|
|
225
|
+
1. Validate IDs
|
|
226
|
+
2. Run `before_delete` hook
|
|
227
|
+
3. Check for referring entities (unless delete mode allows it)
|
|
228
|
+
4. Run `delete` hook or perform deletion
|
|
229
|
+
5. Process cascade deletes for related entities
|
|
230
|
+
6. Run `after_delete` hook
|
|
231
|
+
|
|
232
|
+
**Example:**
|
|
233
|
+
|
|
234
|
+
```javascript
|
|
235
|
+
const result = await productEntity.delete_entity([
|
|
236
|
+
"507f1f77bcf86cd799439011"
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
if (result.code === HAS_REF) {
|
|
240
|
+
console.error("Cannot delete: referenced by", result.err);
|
|
241
|
+
// e.g., ["product<-order:ORD-001", "product<-cart:CART-123"]
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Cascade Delete Behavior:**
|
|
246
|
+
|
|
247
|
+
Configured in field definition:
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
fields: [
|
|
251
|
+
{
|
|
252
|
+
name: "category",
|
|
253
|
+
ref: "category",
|
|
254
|
+
delete: "keep" // Keep products when category is deleted
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "created_by",
|
|
258
|
+
ref: "user",
|
|
259
|
+
delete: "cascade" // Delete products when user is deleted
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
### 2.5 List Entity
|
|
267
|
+
|
|
268
|
+
**Method:** `list_entity(query_params, query, param_obj, view)`
|
|
269
|
+
|
|
270
|
+
Lists entities with pagination, sorting, search, and filtering.
|
|
271
|
+
|
|
272
|
+
**Parameters:**
|
|
273
|
+
- `query_params` (Object): Pagination and sorting config
|
|
274
|
+
- `attr_names` (string): Comma-separated fields to fetch
|
|
275
|
+
- `page` (number): Page number (1-indexed)
|
|
276
|
+
- `limit` (number): Results per page
|
|
277
|
+
- `sort_by` (string): Comma-separated sort fields
|
|
278
|
+
- `desc` (string): Comma-separated boolean flags ("true"/"false")
|
|
279
|
+
- `query` (Object): Additional MongoDB query filter
|
|
280
|
+
- `param_obj` (Object): Search parameters (field values to match)
|
|
281
|
+
- `view` (string): View filter
|
|
282
|
+
|
|
283
|
+
**Returns:** `{code, total, data, err?}`
|
|
284
|
+
|
|
285
|
+
**Example:**
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
const result = await productEntity.list_entity(
|
|
289
|
+
{
|
|
290
|
+
attr_names: "name,price,category",
|
|
291
|
+
page: 1,
|
|
292
|
+
limit: 20,
|
|
293
|
+
sort_by: "price,created_at",
|
|
294
|
+
desc: "false,true" // price ascending, created_at descending
|
|
295
|
+
},
|
|
296
|
+
{ active: true }, // Additional filter
|
|
297
|
+
{ category: "Electronics", price: ">=500" }, // Search params
|
|
298
|
+
"*"
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (result.code === SUCCESS) {
|
|
302
|
+
console.log(`Total: ${result.total}`);
|
|
303
|
+
console.log(`Page data:`, result.data);
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Search Query Syntax:**
|
|
308
|
+
|
|
309
|
+
The `param_obj` supports advanced search operators:
|
|
310
|
+
|
|
311
|
+
```javascript
|
|
312
|
+
{
|
|
313
|
+
price: ">=100", // Greater than or equal
|
|
314
|
+
price: "<1000", // Less than
|
|
315
|
+
stock: ">0", // Greater than
|
|
316
|
+
name: "phone", // Regex search (case-insensitive)
|
|
317
|
+
category: "A,B,C", // Multiple values (OR)
|
|
318
|
+
tags: "sale,new" // Array contains all (for array fields)
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
### 2.6 Clone Entity
|
|
325
|
+
|
|
326
|
+
**Method:** `clone_entity(_id, param_obj, view)`
|
|
327
|
+
|
|
328
|
+
Creates a copy of an existing entity with modifications.
|
|
329
|
+
|
|
330
|
+
**Parameters:**
|
|
331
|
+
- `_id` (string): Source entity ObjectId
|
|
332
|
+
- `param_obj` (Object): New entity data (overrides)
|
|
333
|
+
- `view` (string): View filter
|
|
334
|
+
|
|
335
|
+
**Returns:** `{code, err?}`
|
|
336
|
+
|
|
337
|
+
**Example:**
|
|
338
|
+
|
|
339
|
+
```javascript
|
|
340
|
+
const result = await productEntity.clone_entity(
|
|
341
|
+
"507f1f77bcf86cd799439011",
|
|
342
|
+
{ name: "iPhone 15 Pro", price: 1199.99 },
|
|
343
|
+
"*"
|
|
344
|
+
);
|
|
345
|
+
// Creates a new product based on the original, with new name and price
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## 3. Direct Database Operations
|
|
351
|
+
|
|
352
|
+
For low-level operations without validation and hooks:
|
|
353
|
+
|
|
354
|
+
### 3.1 Basic CRUD
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
// Create (insert)
|
|
358
|
+
const doc = await entity.create({ name: "Test", value: 42 });
|
|
359
|
+
|
|
360
|
+
// Update
|
|
361
|
+
const result = await entity.update(
|
|
362
|
+
{ name: "Test" }, // query
|
|
363
|
+
{ $set: { value: 100 } } // update
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// Delete
|
|
367
|
+
const result = await entity.delete({ name: "Test" });
|
|
368
|
+
|
|
369
|
+
// Find multiple
|
|
370
|
+
const docs = await entity.find(
|
|
371
|
+
{ active: true }, // query
|
|
372
|
+
{ name: 1, value: 1 } // projection
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// Find one
|
|
376
|
+
const doc = await entity.find_one({ _id: oid }, { name: 1 });
|
|
377
|
+
|
|
378
|
+
// Count
|
|
379
|
+
const count = await entity.count({ active: true });
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### 3.2 Sorting and Pagination
|
|
383
|
+
|
|
384
|
+
```javascript
|
|
385
|
+
// Find with sort
|
|
386
|
+
const docs = await entity.find_sort(
|
|
387
|
+
{ active: true },
|
|
388
|
+
{ created_at: -1 }, // sort descending
|
|
389
|
+
{ name: 1, created_at: 1 }
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
// Paginated find
|
|
393
|
+
const docs = await entity.find_page(
|
|
394
|
+
{ active: true },
|
|
395
|
+
{ price: 1 }, // sort
|
|
396
|
+
2, // page (1-indexed)
|
|
397
|
+
20, // limit
|
|
398
|
+
{ name: 1, price: 1 }
|
|
399
|
+
);
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### 3.3 Aggregation
|
|
403
|
+
|
|
404
|
+
```javascript
|
|
405
|
+
// Sum field values
|
|
406
|
+
const total = await entity.sum({ category: "Electronics" }, "price");
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### 3.4 Array Operations
|
|
410
|
+
|
|
411
|
+
```javascript
|
|
412
|
+
// Remove from array field
|
|
413
|
+
await entity.pull({ _id: oid }, { tags: "deprecated" });
|
|
414
|
+
|
|
415
|
+
// Add to array field
|
|
416
|
+
await entity.push({ _id: oid }, { tags: "featured" });
|
|
417
|
+
|
|
418
|
+
// Add unique to array field
|
|
419
|
+
await entity.add_to_set({ _id: oid }, { tags: "sale" });
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### 3.5 Bulk Operations
|
|
423
|
+
|
|
424
|
+
```javascript
|
|
425
|
+
const items = [
|
|
426
|
+
{ sku: "A001", price: 99 },
|
|
427
|
+
{ sku: "A002", price: 149 }
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
await entity.bulk_update(items, ["sku"]);
|
|
431
|
+
// Updates documents matching sku, creates if not exists
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## 4. Reference Field Operations
|
|
437
|
+
|
|
438
|
+
### 4.1 Find by Reference
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
// Find by ObjectId or ref_label
|
|
442
|
+
const products = await entity.find_by_ref_value(
|
|
443
|
+
"Electronics", // Can be ObjectId or ref_label value
|
|
444
|
+
{ name: 1, price: 1 }, // projection
|
|
445
|
+
"order" // referring entity name (for ref_filter)
|
|
446
|
+
);
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### 4.2 Validate References
|
|
450
|
+
|
|
451
|
+
```javascript
|
|
452
|
+
const param_obj = {
|
|
453
|
+
name: "iPhone",
|
|
454
|
+
category: "Electronics" // Will be converted to ObjectId
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const result = await entity.validate_ref(param_obj);
|
|
458
|
+
if (result.code === SUCCESS) {
|
|
459
|
+
// param_obj.category now contains ObjectId
|
|
460
|
+
console.log(param_obj.category); // "507f1f77bcf86cd799439011"
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### 4.3 Convert References
|
|
465
|
+
|
|
466
|
+
```javascript
|
|
467
|
+
const elements = [
|
|
468
|
+
{ _id: "...", name: "Product 1", category: "507f..." },
|
|
469
|
+
{ _id: "...", name: "Product 2", category: "608a..." }
|
|
470
|
+
];
|
|
471
|
+
|
|
472
|
+
const ref_fields = [
|
|
473
|
+
{ name: "category", ref: "category" }
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
const converted = await entity.convert_ref_attrs(elements, ref_fields);
|
|
477
|
+
// converted[0].category is now "Electronics" instead of ObjectId
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### 4.4 Get Reference Labels
|
|
481
|
+
|
|
482
|
+
```javascript
|
|
483
|
+
// Get ref_label values for given IDs
|
|
484
|
+
const ids = ["507f...", "608a..."];
|
|
485
|
+
const labels = await entity.get_ref_labels(ids);
|
|
486
|
+
// Returns array of objects with _id and ref_label
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
490
|
+
|
|
491
|
+
## 5. GridFS File Storage
|
|
492
|
+
|
|
493
|
+
### 5.1 Overview
|
|
494
|
+
|
|
495
|
+
GridFS stores files in MongoDB as chunks, suitable for files larger than 16MB BSON size limit. The Hola framework provides a simplified API for file operations.
|
|
496
|
+
|
|
497
|
+
### 5.2 Import
|
|
498
|
+
|
|
499
|
+
```javascript
|
|
500
|
+
const {
|
|
501
|
+
save_file, read_file, pipe_file, delete_file,
|
|
502
|
+
save_file_fields_to_db, set_file_fields
|
|
503
|
+
} = require("hola-server/db/gridfs");
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### 5.3 Save File
|
|
507
|
+
|
|
508
|
+
**Method:** `save_file(collection, filename, filepath)`
|
|
509
|
+
|
|
510
|
+
Uploads a file to GridFS, replacing any existing file with the same name.
|
|
511
|
+
|
|
512
|
+
**Parameters:**
|
|
513
|
+
- `collection` (string): Bucket name (typically entity collection name)
|
|
514
|
+
- `filename` (string): File identifier
|
|
515
|
+
- `filepath` (string): Source file path or readable stream
|
|
516
|
+
|
|
517
|
+
```javascript
|
|
518
|
+
await save_file("product", "iphone_image_01", "/tmp/upload_xyz.jpg");
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 5.4 Read File
|
|
522
|
+
|
|
523
|
+
**Method:** `read_file(collection, filename, response)`
|
|
524
|
+
|
|
525
|
+
Streams a file directly to HTTP response.
|
|
526
|
+
|
|
527
|
+
**Parameters:**
|
|
528
|
+
- `collection` (string): Bucket name
|
|
529
|
+
- `filename` (string): File identifier
|
|
530
|
+
- `response` (Response): Express response object
|
|
531
|
+
|
|
532
|
+
```javascript
|
|
533
|
+
// Express route
|
|
534
|
+
app.get('/files/:collection/:filename', async (req, res) => {
|
|
535
|
+
await read_file(
|
|
536
|
+
req.params.collection,
|
|
537
|
+
req.params.filename,
|
|
538
|
+
res
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 5.5 Pipe File
|
|
544
|
+
|
|
545
|
+
**Method:** `pipe_file(collection, filename, dest_path)`
|
|
546
|
+
|
|
547
|
+
Downloads a file from GridFS to local disk.
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
await pipe_file(
|
|
551
|
+
"product",
|
|
552
|
+
"iphone_image_01",
|
|
553
|
+
"./downloads/product_image.jpg"
|
|
554
|
+
);
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
### 5.6 Delete File
|
|
558
|
+
|
|
559
|
+
**Method:** `delete_file(collection, filename)`
|
|
560
|
+
|
|
561
|
+
Removes a file from GridFS.
|
|
562
|
+
|
|
563
|
+
```javascript
|
|
564
|
+
await delete_file("product", "iphone_image_01");
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## 6. File Field Integration
|
|
570
|
+
|
|
571
|
+
### 6.1 Entity with File Fields
|
|
572
|
+
|
|
573
|
+
Define file fields in meta:
|
|
574
|
+
|
|
575
|
+
```javascript
|
|
576
|
+
fields: [
|
|
577
|
+
{ name: "sku", type: "string", required: true },
|
|
578
|
+
{ name: "name", type: "string", required: true },
|
|
579
|
+
{ name: "image", type: "file" },
|
|
580
|
+
{ name: "manual", type: "file" }
|
|
581
|
+
]
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### 6.2 Handling File Uploads
|
|
585
|
+
|
|
586
|
+
In router create/update handlers:
|
|
587
|
+
|
|
588
|
+
```javascript
|
|
589
|
+
const { set_file_fields, save_file_fields_to_db } = require("hola-server/db/gridfs");
|
|
590
|
+
|
|
591
|
+
// In create handler
|
|
592
|
+
router.post("/", upload.any(), async (req, res) => {
|
|
593
|
+
set_file_fields(meta, req, req.body);
|
|
594
|
+
|
|
595
|
+
const result = await entity.create_entity(req.body, "*");
|
|
596
|
+
if (result.code === SUCCESS) {
|
|
597
|
+
await save_file_fields_to_db(collection, meta.file_fields, req, req.body);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return res.json(result);
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**What happens:**
|
|
605
|
+
1. `set_file_fields()` sets field values based on uploaded files (e.g., `"sku_image"`)
|
|
606
|
+
2. `save_file_fields_to_db()` moves temp files to GridFS using the field values as filenames
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## 7. Best Practices
|
|
611
|
+
|
|
612
|
+
### 7.1 Use High-Level CRUD Methods
|
|
613
|
+
|
|
614
|
+
Prefer `create_entity()`, `update_entity()`, etc. over direct database operations for:
|
|
615
|
+
- Automatic validation
|
|
616
|
+
- Type conversion
|
|
617
|
+
- Reference resolution
|
|
618
|
+
- Lifecycle hooks
|
|
619
|
+
- Consistent error handling
|
|
620
|
+
|
|
621
|
+
### 7.2 Handle Error Codes
|
|
622
|
+
|
|
623
|
+
Always check the `code` field in results:
|
|
624
|
+
|
|
625
|
+
```javascript
|
|
626
|
+
const result = await entity.create_entity(data, "*");
|
|
627
|
+
return res.json(result);
|
|
628
|
+
// Entity methods return { code: SUCCESS } or { code: ERROR_CODE, err: [...] }
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### 7.3 Use Views for Field Filtering
|
|
632
|
+
|
|
633
|
+
Define different views for different contexts:
|
|
634
|
+
|
|
635
|
+
```javascript
|
|
636
|
+
// Admin view - all fields
|
|
637
|
+
await entity.read_entity(id, "name,price,cost,margin", "admin");
|
|
638
|
+
|
|
639
|
+
// Public view - limited fields
|
|
640
|
+
await entity.read_entity(id, "name,price", "public");
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 7.4 Optimize Queries
|
|
644
|
+
|
|
645
|
+
Use projections to limit returned fields:
|
|
646
|
+
|
|
647
|
+
```javascript
|
|
648
|
+
// Good - only fetch needed fields
|
|
649
|
+
const users = await entity.find({ active: true }, { name: 1, email: 1 });
|
|
650
|
+
|
|
651
|
+
// Avoid - fetches all fields
|
|
652
|
+
const users = await entity.find({ active: true }, {});
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### 7.5 File Storage Patterns
|
|
656
|
+
|
|
657
|
+
- Use entity primary key as base for file naming
|
|
658
|
+
- Clean up files when deleting entities
|
|
659
|
+
- Consider file versioning for updates
|
|
660
|
+
|
|
661
|
+
```javascript
|
|
662
|
+
// Good file naming pattern
|
|
663
|
+
const filename = `${entity.primary_key}_${field_name}`;
|
|
664
|
+
await save_file(collection, filename, filepath);
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## 8. Common Patterns
|
|
670
|
+
|
|
671
|
+
### 8.1 Create with File Upload
|
|
672
|
+
|
|
673
|
+
```javascript
|
|
674
|
+
router.post("/", upload.any(), async (req, res) => {
|
|
675
|
+
set_file_fields(meta, req, req.body);
|
|
676
|
+
|
|
677
|
+
const result = await entity.create_entity(req.body, "*");
|
|
678
|
+
|
|
679
|
+
if (result.code === SUCCESS) {
|
|
680
|
+
await save_file_fields_to_db(collection, meta.file_fields, req, req.body);
|
|
681
|
+
return res.json({ code: SUCCESS });
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return res.json(result);
|
|
685
|
+
});
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### 8.2 Update with File Upload
|
|
689
|
+
|
|
690
|
+
```javascript
|
|
691
|
+
router.put("/:id", upload.any(), async (req, res) => {
|
|
692
|
+
set_file_fields(meta, req, req.body);
|
|
693
|
+
|
|
694
|
+
const result = await entity.update_entity(req.params.id, req.body, "*");
|
|
695
|
+
|
|
696
|
+
if (result.code === SUCCESS) {
|
|
697
|
+
await save_file_fields_to_db(collection, meta.file_fields, req, req.body);
|
|
698
|
+
return res.json({ code: SUCCESS });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return res.json(result);
|
|
702
|
+
});
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### 8.3 Delete with Cascade
|
|
706
|
+
|
|
707
|
+
```javascript
|
|
708
|
+
router.delete("/", async (req, res) => {
|
|
709
|
+
const ids = req.body.ids; // Array of IDs
|
|
710
|
+
|
|
711
|
+
// Delete files first
|
|
712
|
+
for (const id of ids) {
|
|
713
|
+
const item = await entity.find_one(oid_query(id), {});
|
|
714
|
+
for (const field of meta.file_fields) {
|
|
715
|
+
if (item[field.name]) {
|
|
716
|
+
await delete_file(collection, item[field.name]);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Then delete entity (will cascade to related entities)
|
|
722
|
+
const result = await entity.delete_entity(ids);
|
|
723
|
+
return res.json(result);
|
|
724
|
+
});
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### 8.4 Search with References
|
|
728
|
+
|
|
729
|
+
```javascript
|
|
730
|
+
// Client sends category name, server resolves to ID
|
|
731
|
+
const result = await entity.list_entity(
|
|
732
|
+
{
|
|
733
|
+
attr_names: "name,price,category",
|
|
734
|
+
page: 1,
|
|
735
|
+
limit: 20,
|
|
736
|
+
sort_by: "price",
|
|
737
|
+
desc: "false"
|
|
738
|
+
},
|
|
739
|
+
{},
|
|
740
|
+
{ category: "Electronics" }, // Resolved to ObjectId automatically
|
|
741
|
+
"*"
|
|
742
|
+
);
|
|
743
|
+
```
|