hola-server 1.0.10 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -1
- package/core/array.js +79 -142
- package/core/bash.js +208 -259
- package/core/chart.js +26 -16
- package/core/cron.js +14 -3
- package/core/date.js +15 -44
- package/core/encrypt.js +19 -9
- package/core/file.js +42 -29
- package/core/lhs.js +32 -6
- package/core/meta.js +213 -289
- package/core/msg.js +20 -7
- package/core/number.js +105 -103
- package/core/obj.js +15 -12
- package/core/random.js +9 -6
- package/core/role.js +69 -77
- package/core/thread.js +12 -2
- package/core/type.js +300 -261
- package/core/url.js +20 -12
- package/core/validate.js +29 -26
- package/db/db.js +297 -227
- package/db/entity.js +631 -963
- package/db/gridfs.js +120 -166
- package/design/add_default_field_attr.md +56 -0
- package/http/context.js +22 -8
- package/http/cors.js +25 -8
- package/http/error.js +27 -9
- package/http/express.js +70 -41
- package/http/params.js +70 -42
- package/http/router.js +51 -40
- package/http/session.js +59 -36
- package/index.js +85 -9
- package/package.json +2 -2
- package/router/clone.js +28 -36
- package/router/create.js +21 -26
- package/router/delete.js +24 -28
- package/router/read.js +137 -123
- package/router/update.js +38 -56
- package/setting.js +22 -6
- package/skills/array.md +155 -0
- package/skills/bash.md +91 -0
- package/skills/chart.md +54 -0
- package/skills/code.md +422 -0
- package/skills/context.md +177 -0
- package/skills/date.md +58 -0
- package/skills/express.md +255 -0
- package/skills/file.md +60 -0
- package/skills/lhs.md +54 -0
- package/skills/meta.md +1023 -0
- package/skills/msg.md +30 -0
- package/skills/number.md +88 -0
- package/skills/obj.md +36 -0
- package/skills/params.md +206 -0
- package/skills/random.md +22 -0
- package/skills/role.md +59 -0
- package/skills/session.md +281 -0
- package/skills/storage.md +743 -0
- package/skills/thread.md +22 -0
- package/skills/type.md +547 -0
- package/skills/url.md +34 -0
- package/skills/validate.md +48 -0
- package/test/cleanup/close-db.js +5 -0
- package/test/core/array.js +226 -0
- package/test/core/chart.js +51 -0
- package/test/core/file.js +59 -0
- package/test/core/lhs.js +44 -0
- package/test/core/number.js +167 -12
- package/test/core/obj.js +47 -0
- package/test/core/random.js +24 -0
- package/test/core/thread.js +20 -0
- package/test/core/type.js +216 -0
- package/test/core/validate.js +67 -0
- package/test/db/db-ops.js +99 -0
- package/test/db/pipe_test.txt +0 -0
- package/test/db/test_case_design.md +528 -0
- package/test/db/test_db_class.js +613 -0
- package/test/db/test_entity_class.js +414 -0
- package/test/db/test_gridfs_class.js +234 -0
- package/test/entity/create.js +1 -1
- package/test/entity/delete-mixed.js +156 -0
- package/test/entity/ref-filter.js +63 -0
- package/tool/gen_i18n.js +55 -21
- package/test/crud/router.js +0 -99
- package/test/router/user.js +0 -17
package/skills/code.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# HTTP Response Codes Skill
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `hola-server/http/code.js` module defines standard response codes used throughout the Hola framework for consistent error handling and status reporting.
|
|
6
|
+
|
|
7
|
+
**Important:** Hola framework returns all responses as JSON objects with `code` and optional `err` fields. Do not map these codes to HTTP status codes - always return `200 OK` with the code in the JSON body.
|
|
8
|
+
|
|
9
|
+
## Importing
|
|
10
|
+
|
|
11
|
+
```javascript
|
|
12
|
+
const {
|
|
13
|
+
SUCCESS, ERROR,
|
|
14
|
+
NO_SESSION, NO_RIGHTS, NO_PARAMS, NOT_FOUND,
|
|
15
|
+
INVALID_PARAMS, REF_NOT_FOUND, REF_NOT_UNIQUE, HAS_REF,
|
|
16
|
+
DUPLICATE_KEY, NO_RESOURCE,
|
|
17
|
+
IMPORT_EMPTY_KEY, IMPORT_WRONG_FIELDS, IMPORT_DUPLICATE_KEY, IMPORT_NO_FOUND_REF
|
|
18
|
+
} = require("hola-server/http/code");
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Response Codes Reference
|
|
22
|
+
|
|
23
|
+
### Success & General Errors
|
|
24
|
+
|
|
25
|
+
| Code | Value | Description | Usage |
|
|
26
|
+
|------|-------|-------------|-------|
|
|
27
|
+
| `SUCCESS` | 1 | Operation succeeded | All successful operations |
|
|
28
|
+
| `ERROR` | 0 | General error | Unexpected failures, database errors |
|
|
29
|
+
|
|
30
|
+
### Authentication & Authorization (200-299)
|
|
31
|
+
|
|
32
|
+
| Code | Value | Description | Usage |
|
|
33
|
+
|------|-------|-------------|-------|
|
|
34
|
+
| `NO_SESSION` | 200 | No valid session | User not logged in |
|
|
35
|
+
| `NO_RIGHTS` | 201 | Insufficient permissions | User lacks required role/permissions |
|
|
36
|
+
|
|
37
|
+
### Validation & Parameters (202-207)
|
|
38
|
+
|
|
39
|
+
| Code | Value | Description | Usage |
|
|
40
|
+
|------|-------|-------------|-------|
|
|
41
|
+
| `NO_PARAMS` | 202 | Missing required parameters | Required fields not provided |
|
|
42
|
+
| `NOT_FOUND` | 203 | Entity not found | Query returned no results |
|
|
43
|
+
| `INVALID_PARAMS` | 204 | Invalid parameter values | Type conversion failed, validation error |
|
|
44
|
+
| `REF_NOT_FOUND` | 205 | Referenced entity not found | Foreign key constraint violation |
|
|
45
|
+
| `REF_NOT_UNIQUE` | 206 | Ambiguous reference | Multiple entities match ref_label |
|
|
46
|
+
| `HAS_REF` | 207 | Entity has references | Cannot delete due to foreign key constraints |
|
|
47
|
+
|
|
48
|
+
### Data Integrity (300-399)
|
|
49
|
+
|
|
50
|
+
| Code | Value | Description | Usage |
|
|
51
|
+
|------|-------|-------------|-------|
|
|
52
|
+
| `DUPLICATE_KEY` | 300 | Primary key already exists | Insert/update violates uniqueness |
|
|
53
|
+
|
|
54
|
+
### Resources (400-499)
|
|
55
|
+
|
|
56
|
+
| Code | Value | Description | Usage |
|
|
57
|
+
|------|-------|-------------|-------|
|
|
58
|
+
| `NO_RESOURCE` | 404 | Resource not found | Static file or route not found |
|
|
59
|
+
|
|
60
|
+
### Import Operations (100-199)
|
|
61
|
+
|
|
62
|
+
| Code | Value | Description | Usage |
|
|
63
|
+
|------|-------|-------------|-------|
|
|
64
|
+
| `IMPORT_EMPTY_KEY` | 100 | Empty primary key in import | CSV row missing key field |
|
|
65
|
+
| `IMPORT_WRONG_FIELDS` | 101 | Invalid fields in import | CSV columns don't match entity |
|
|
66
|
+
| `IMPORT_DUPLICATE_KEY` | 102 | Duplicate key in import | Multiple rows with same key |
|
|
67
|
+
| `IMPORT_NO_FOUND_REF` | 103 | Reference not found in import | Foreign key lookup failed |
|
|
68
|
+
|
|
69
|
+
## Usage Examples
|
|
70
|
+
|
|
71
|
+
### Basic Response Pattern
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const { SUCCESS, NO_PARAMS } = require("hola-server/http/code");
|
|
75
|
+
|
|
76
|
+
router.post("/create", async (req, res) => {
|
|
77
|
+
const { name, email } = req.body;
|
|
78
|
+
|
|
79
|
+
if (!name || !email) {
|
|
80
|
+
return res.json({ code: NO_PARAMS, err: ["name", "email"] });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const result = await entity.create_entity(req.body, "*");
|
|
84
|
+
return res.json(result); // Returns { code: SUCCESS } or { code: ERROR_CODE, err: [...] }
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Standard CRUD Operations
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
const { SUCCESS } = require("hola-server/http/code");
|
|
92
|
+
|
|
93
|
+
// Create
|
|
94
|
+
router.post("/", async (req, res) => {
|
|
95
|
+
const result = await entity.create_entity(req.body, "*");
|
|
96
|
+
return res.json(result);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Read
|
|
100
|
+
router.get("/:id", async (req, res) => {
|
|
101
|
+
const result = await entity.read_entity(req.params.id, "name,email,age", "*");
|
|
102
|
+
return res.json(result);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Update
|
|
106
|
+
router.put("/:id", async (req, res) => {
|
|
107
|
+
const result = await entity.update_entity(req.params.id, req.body, "*");
|
|
108
|
+
return res.json(result);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Delete
|
|
112
|
+
router.delete("/", async (req, res) => {
|
|
113
|
+
const result = await entity.delete_entity(req.body.ids);
|
|
114
|
+
return res.json(result);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// List
|
|
118
|
+
router.get("/", async (req, res) => {
|
|
119
|
+
const result = await entity.list_entity(
|
|
120
|
+
{ attr_names: "name,email", page: 1, limit: 20, sort_by: "created_at", desc: "true" },
|
|
121
|
+
{},
|
|
122
|
+
{},
|
|
123
|
+
"*"
|
|
124
|
+
);
|
|
125
|
+
return res.json(result);
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Client-Side Helpers
|
|
130
|
+
|
|
131
|
+
The `hola-web/src/core/axios.js` module provides convenience functions for interacting with the Hola API from Vue.js applications.
|
|
132
|
+
|
|
133
|
+
### Importing
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
import {
|
|
137
|
+
init_axios,
|
|
138
|
+
is_success_response,
|
|
139
|
+
save_entity,
|
|
140
|
+
read_entity,
|
|
141
|
+
list_entity,
|
|
142
|
+
delete_entity,
|
|
143
|
+
get_entity_meta,
|
|
144
|
+
get_ref_labels
|
|
145
|
+
} from "@/core/axios";
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Initialization
|
|
149
|
+
|
|
150
|
+
```javascript
|
|
151
|
+
// In main.js or app setup
|
|
152
|
+
import { init_axios } from "@/core/axios";
|
|
153
|
+
|
|
154
|
+
init_axios(
|
|
155
|
+
{ baseURL: "http://localhost:3000/api" },
|
|
156
|
+
{
|
|
157
|
+
handle_response: (code, data) => {
|
|
158
|
+
// Custom response handling
|
|
159
|
+
if (code === 200) { // NO_SESSION
|
|
160
|
+
router.push("/login");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Response Code Checking
|
|
168
|
+
|
|
169
|
+
```javascript
|
|
170
|
+
import { is_success_response, is_duplicated, is_been_referred } from "@/core/axios";
|
|
171
|
+
|
|
172
|
+
const result = await save_entity("product", form_data, false);
|
|
173
|
+
|
|
174
|
+
if (is_success_response(result.code)) {
|
|
175
|
+
this.$message.success(this.$t("msg.saved_successfully"));
|
|
176
|
+
} else if (is_duplicated(result.code)) {
|
|
177
|
+
this.$message.error(this.$t("msg.already_exists"));
|
|
178
|
+
} else if (is_been_referred(result.code)) {
|
|
179
|
+
this.$message.error(this.$t("msg.cannot_delete_referenced", { refs: result.err.join(", ") }));
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Available Checkers:**
|
|
184
|
+
- `is_success_response(code)` - Code is SUCCESS (1)
|
|
185
|
+
- `is_error_response(code)` - Code is ERROR (0)
|
|
186
|
+
- `is_duplicated(code)` - Code is DUPLICATE_KEY
|
|
187
|
+
- `is_been_referred(code)` - Code is HAS_REF
|
|
188
|
+
- `has_invalid_params(code)` - Code is INVALID_PARAMS
|
|
189
|
+
- `is_no_session(code)` - Code is NO_SESSION
|
|
190
|
+
|
|
191
|
+
### Entity Operations
|
|
192
|
+
|
|
193
|
+
#### Create/Update Entity
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
import { save_entity, is_success_response } from "@/core/axios";
|
|
197
|
+
|
|
198
|
+
// Create (edit_mode = false)
|
|
199
|
+
const result = await save_entity("product", {
|
|
200
|
+
name: "iPhone 15",
|
|
201
|
+
price: 999,
|
|
202
|
+
category: "Electronics"
|
|
203
|
+
}, false);
|
|
204
|
+
|
|
205
|
+
// Update (edit_mode = true)
|
|
206
|
+
const result = await save_entity("product", {
|
|
207
|
+
_id: "507f...",
|
|
208
|
+
price: 899
|
|
209
|
+
}, true);
|
|
210
|
+
|
|
211
|
+
// Clone (edit_mode = true, clone = true)
|
|
212
|
+
const result = await save_entity("product", {
|
|
213
|
+
_id: "507f...",
|
|
214
|
+
name: "iPhone 15 Pro"
|
|
215
|
+
}, true, true);
|
|
216
|
+
|
|
217
|
+
if (is_success_response(result.code)) {
|
|
218
|
+
this.$message.success(this.$t("msg.saved_successfully"));
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
#### Read Entity
|
|
223
|
+
|
|
224
|
+
```javascript
|
|
225
|
+
import { read_entity, read_property } from "@/core/axios";
|
|
226
|
+
|
|
227
|
+
// Read with references expanded
|
|
228
|
+
const product = await read_entity("product", "507f...", "name,price,category");
|
|
229
|
+
// product.category will be "Electronics" (ref_label)
|
|
230
|
+
|
|
231
|
+
// Read without reference expansion (faster)
|
|
232
|
+
const product = await read_property("product", "507f...", "name,price,category");
|
|
233
|
+
// product.category will be ObjectId
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### List Entities
|
|
237
|
+
|
|
238
|
+
```javascript
|
|
239
|
+
import { list_entity } from "@/core/axios";
|
|
240
|
+
|
|
241
|
+
const result = await list_entity(
|
|
242
|
+
"product",
|
|
243
|
+
{ category: "Electronics", min_price: 500 }, // Search params
|
|
244
|
+
{
|
|
245
|
+
attr_names: "name,price,category",
|
|
246
|
+
page: 1,
|
|
247
|
+
limit: 20,
|
|
248
|
+
sort_by: "price",
|
|
249
|
+
desc: "false"
|
|
250
|
+
}
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
if (is_success_response(result.code)) {
|
|
254
|
+
this.products = result.data;
|
|
255
|
+
this.total = result.total;
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Query Entities
|
|
260
|
+
|
|
261
|
+
```javascript
|
|
262
|
+
import { query_entity } from "@/core/axios";
|
|
263
|
+
|
|
264
|
+
// Get all active products
|
|
265
|
+
const result = await query_entity(
|
|
266
|
+
"product",
|
|
267
|
+
["name", "price"],
|
|
268
|
+
{ active: true }
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
if (is_success_response(result.code)) {
|
|
272
|
+
this.products = result.data;
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### Delete Entities
|
|
277
|
+
|
|
278
|
+
```javascript
|
|
279
|
+
import { delete_entity, is_success_response, is_been_referred } from "@/core/axios";
|
|
280
|
+
|
|
281
|
+
const result = await delete_entity("product", ["507f...", "608a..."]);
|
|
282
|
+
|
|
283
|
+
if (is_success_response(result.code)) {
|
|
284
|
+
this.$message.success("Deleted");
|
|
285
|
+
} else if (is_been_referred(result.code)) {
|
|
286
|
+
this.$message.error("Cannot delete: referenced by " + result.err.join(", "));
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Metadata Operations
|
|
291
|
+
|
|
292
|
+
#### Get Entity Meta
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
import { get_entity_meta } from "@/core/axios";
|
|
296
|
+
|
|
297
|
+
const meta = await get_entity_meta("product");
|
|
298
|
+
// Returns meta definition or null
|
|
299
|
+
// Result is cached automatically
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### Get Reference Labels
|
|
303
|
+
|
|
304
|
+
```javascript
|
|
305
|
+
import { get_ref_labels } from "@/core/axios";
|
|
306
|
+
|
|
307
|
+
// Get all categories for dropdown
|
|
308
|
+
const categories = await get_ref_labels("category", "product");
|
|
309
|
+
// Returns array of { _id, ref_label } objects
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### File Operations
|
|
313
|
+
|
|
314
|
+
#### Upload File
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
import { axios_upload } from "@/core/axios";
|
|
318
|
+
|
|
319
|
+
const file = this.$refs.fileInput.files[0];
|
|
320
|
+
const result = await axios_upload("/api/upload", file);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Download File
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
import { axios_download } from "@/core/axios";
|
|
327
|
+
|
|
328
|
+
axios_download("/api/export", "export.csv", { format: "csv" });
|
|
329
|
+
// Triggers browser download
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Complete Vue Component Example
|
|
333
|
+
|
|
334
|
+
```vue
|
|
335
|
+
<template>
|
|
336
|
+
<div>
|
|
337
|
+
<el-form :model="form">
|
|
338
|
+
<el-form-item label="Name">
|
|
339
|
+
<el-input v-model="form.name"></el-input>
|
|
340
|
+
</el-form-item>
|
|
341
|
+
<el-form-item label="Price">
|
|
342
|
+
<el-input-number v-model="form.price"></el-input-number>
|
|
343
|
+
</el-form-item>
|
|
344
|
+
<el-button @click="save">Save</el-button>
|
|
345
|
+
</el-form>
|
|
346
|
+
|
|
347
|
+
<el-table :data="list">
|
|
348
|
+
<el-table-column prop="name" label="Name"></el-table-column>
|
|
349
|
+
<el-table-column prop="price" label="Price"></el-table-column>
|
|
350
|
+
<el-table-column>
|
|
351
|
+
<template #default="{ row }">
|
|
352
|
+
<el-button @click="deleteItem(row._id)">Delete</el-button>
|
|
353
|
+
</template>
|
|
354
|
+
</el-table-column>
|
|
355
|
+
</el-table>
|
|
356
|
+
</div>
|
|
357
|
+
</template>
|
|
358
|
+
|
|
359
|
+
<script>
|
|
360
|
+
import {
|
|
361
|
+
save_entity,
|
|
362
|
+
list_entity,
|
|
363
|
+
delete_entity,
|
|
364
|
+
is_success_response
|
|
365
|
+
} from "@/core/axios";
|
|
366
|
+
|
|
367
|
+
export default {
|
|
368
|
+
data() {
|
|
369
|
+
return {
|
|
370
|
+
form: { name: "", price: 0 },
|
|
371
|
+
list: [],
|
|
372
|
+
total: 0
|
|
373
|
+
};
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
mounted() {
|
|
377
|
+
this.loadList();
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
methods: {
|
|
381
|
+
async save() {
|
|
382
|
+
const result = await save_entity("product", this.form, false);
|
|
383
|
+
|
|
384
|
+
if (is_success_response(result.code)) {
|
|
385
|
+
this.$message.success(this.$t("msg.saved_successfully"));
|
|
386
|
+
this.form = { name: "", price: 0 };
|
|
387
|
+
this.loadList();
|
|
388
|
+
} else {
|
|
389
|
+
this.$message.error(this.$t("msg.save_failed", { err: JSON.stringify(result.err) }));
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
async loadList() {
|
|
394
|
+
const result = await list_entity("product", {}, {
|
|
395
|
+
attr_names: "name,price",
|
|
396
|
+
page: 1,
|
|
397
|
+
limit: 20,
|
|
398
|
+
sort_by: "created_at",
|
|
399
|
+
desc: "true"
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
if (is_success_response(result.code)) {
|
|
403
|
+
this.list = result.data;
|
|
404
|
+
this.total = result.total;
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
|
|
408
|
+
async deleteItem(id) {
|
|
409
|
+
const result = await delete_entity("product", [id]);
|
|
410
|
+
|
|
411
|
+
if (is_success_response(result.code)) {
|
|
412
|
+
this.$message.success(this.$t("msg.deleted_successfully"));
|
|
413
|
+
this.loadList();
|
|
414
|
+
} else {
|
|
415
|
+
this.$message.error(this.$t("msg.delete_failed", { err: JSON.stringify(result.err) }));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
</script>
|
|
421
|
+
```
|
|
422
|
+
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# HTTP Context Utilities Skill
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `hola-server/http/context.js` module provides request-scoped context storage using Node.js `AsyncLocalStorage`. This allows you to store and retrieve values that are tied to the current HTTP request without explicitly passing them through function parameters.
|
|
6
|
+
|
|
7
|
+
## Importing
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const { set_context_value, get_context_value } = require("hola-server/http/context");
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API Reference
|
|
14
|
+
|
|
15
|
+
### `set_context_value(key, obj)`
|
|
16
|
+
Stores a value in the current request context.
|
|
17
|
+
|
|
18
|
+
**Parameters:**
|
|
19
|
+
- `key` (string): Context key
|
|
20
|
+
- `obj` (*): Value to store
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
set_context_value("user_id", "507f1f77bcf86cd799439011");
|
|
24
|
+
set_context_value("request_start", Date.now());
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### `get_context_value(key)`
|
|
28
|
+
Retrieves a value from the current request context.
|
|
29
|
+
|
|
30
|
+
**Parameters:**
|
|
31
|
+
- `key` (string): Context key
|
|
32
|
+
|
|
33
|
+
**Returns:** `*` - Stored value or `null` if not found
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
const user_id = get_context_value("user_id");
|
|
37
|
+
const start_time = get_context_value("request_start");
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## How It Works
|
|
41
|
+
|
|
42
|
+
The Hola framework automatically initializes AsyncLocalStorage context for each request in the authentication middleware (`http/express.js`). The request object is automatically stored with key `"req"`.
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// Happens automatically in express.js
|
|
46
|
+
asyncLocalStorage.run({}, () => {
|
|
47
|
+
set_context_value("req", req);
|
|
48
|
+
next();
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Usage Patterns
|
|
53
|
+
|
|
54
|
+
### Pattern 1: Accessing Request from Deep Functions
|
|
55
|
+
|
|
56
|
+
Instead of passing `req` through multiple function levels:
|
|
57
|
+
|
|
58
|
+
```javascript
|
|
59
|
+
// Without context (verbose):
|
|
60
|
+
async function process_order(req, order_data) {
|
|
61
|
+
const user_id = get_session_user_id(req);
|
|
62
|
+
await create_audit_log(req, "order_created", order_data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function create_audit_log(req, action, data) {
|
|
66
|
+
const user_id = get_session_user_id(req);
|
|
67
|
+
// ...
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// With context (clean):
|
|
71
|
+
const { get_context_value } = require("hola-server/http/context");
|
|
72
|
+
|
|
73
|
+
async function process_order(order_data) {
|
|
74
|
+
const req = get_context_value("req");
|
|
75
|
+
const user_id = get_session_user_id(req);
|
|
76
|
+
await create_audit_log("order_created", order_data);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function create_audit_log(action, data) {
|
|
80
|
+
const req = get_context_value("req");
|
|
81
|
+
const user_id = get_session_user_id(req);
|
|
82
|
+
// ...
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Pattern 2: Request Timing
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// Middleware to track request duration
|
|
90
|
+
app.use((req, res, next) => {
|
|
91
|
+
set_context_value("request_start", Date.now());
|
|
92
|
+
next();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Later in response
|
|
96
|
+
router.get("/api/data", async (req, res) => {
|
|
97
|
+
const data = await fetch_data();
|
|
98
|
+
|
|
99
|
+
const start = get_context_value("request_start");
|
|
100
|
+
const duration = Date.now() - start;
|
|
101
|
+
|
|
102
|
+
console.log(`Request took ${duration}ms`);
|
|
103
|
+
return res.json(data);
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Pattern 3: Storing Computed Values
|
|
108
|
+
|
|
109
|
+
```javascript
|
|
110
|
+
// Middleware that resolves user once
|
|
111
|
+
app.use(async (req, res, next) => {
|
|
112
|
+
const user_id = get_session_user_id(req);
|
|
113
|
+
if (user_id) {
|
|
114
|
+
const user = await user_entity.find_one({ _id: user_id }, {});
|
|
115
|
+
set_context_value("current_user", user);
|
|
116
|
+
}
|
|
117
|
+
next();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Access anywhere without re-fetching
|
|
121
|
+
router.post("/api/order", async (req, res) => {
|
|
122
|
+
const user = get_context_value("current_user");
|
|
123
|
+
const order = await create_order(req.body, user);
|
|
124
|
+
return res.json(order);
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Pattern 4: Custom Logging Context
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
// Set trace ID for request
|
|
132
|
+
app.use((req, res, next) => {
|
|
133
|
+
const trace_id = generate_trace_id();
|
|
134
|
+
set_context_value("trace_id", trace_id);
|
|
135
|
+
next();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Use in logging
|
|
139
|
+
function log_info(message, data) {
|
|
140
|
+
const trace_id = get_context_value("trace_id");
|
|
141
|
+
console.log(`[${trace_id}] ${message}`, data);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Anywhere in the request lifecycle
|
|
145
|
+
router.get("/api/process", async (req, res) => {
|
|
146
|
+
log_info("Processing started"); // Automatically includes trace_id
|
|
147
|
+
// ...
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Best Practices
|
|
152
|
+
|
|
153
|
+
1. **Use for cross-cutting concerns**: Context is ideal for request ID, user info, timing, etc.
|
|
154
|
+
|
|
155
|
+
2. **Don't overuse**: For values only needed in 1-2 places, pass them directly.
|
|
156
|
+
|
|
157
|
+
3. **Clear naming**: Use descriptive keys like `"current_user"` not `"u"`.
|
|
158
|
+
|
|
159
|
+
4. **Document context keys**: If your app uses many context values, document them.
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
// Good: centralized context keys
|
|
163
|
+
const CONTEXT_KEYS = {
|
|
164
|
+
REQUEST: "req",
|
|
165
|
+
USER: "current_user",
|
|
166
|
+
TRACE_ID: "trace_id",
|
|
167
|
+
REQUEST_START: "request_start"
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
set_context_value(CONTEXT_KEYS.TRACE_ID, trace_id);
|
|
171
|
+
const user = get_context_value(CONTEXT_KEYS.USER);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Limitations
|
|
175
|
+
|
|
176
|
+
- **Only works in request context**: Cannot use in background jobs, timers, or event handlers unless they originated from a request.
|
|
177
|
+
- **Async boundaries**: Works correctly with `async/await` and Promises, but not with callbacks or event emitters.
|
package/skills/date.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Date Utilities Skill
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The `hola-server/core/date.js` module provides date formatting and parsing utilities, wrapping the `dateformat` library for consistent application-wide formats.
|
|
6
|
+
|
|
7
|
+
## Importing
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
const {
|
|
11
|
+
simple_date, format_date, format_time,
|
|
12
|
+
format_date_time, parse_date
|
|
13
|
+
} = require("hola-server/core/date");
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## API Reference
|
|
17
|
+
|
|
18
|
+
### Formatting Functions
|
|
19
|
+
|
|
20
|
+
#### `simple_date(date)`
|
|
21
|
+
Formats date as `mm/dd`.
|
|
22
|
+
- **param**: `date` (Date)
|
|
23
|
+
- **returns**: `string`
|
|
24
|
+
|
|
25
|
+
#### `format_date(date)`
|
|
26
|
+
Formats date as `yyyymmdd`.
|
|
27
|
+
- **param**: `date` (Date)
|
|
28
|
+
- **returns**: `string`
|
|
29
|
+
|
|
30
|
+
#### `format_time(date)`
|
|
31
|
+
Formats time as `HH:MM`.
|
|
32
|
+
- **param**: `date` (Date)
|
|
33
|
+
- **returns**: `string`
|
|
34
|
+
|
|
35
|
+
#### `format_date_time(date)`
|
|
36
|
+
Formats as `yyyymmdd HH:MM:ss`.
|
|
37
|
+
- **param**: `date` (Date)
|
|
38
|
+
- **returns**: `string`
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
const now = new Date("2023-12-25T14:30:00");
|
|
42
|
+
format_date(now); // "20231225"
|
|
43
|
+
format_time(now); // "14:30"
|
|
44
|
+
format_date_time(now); // "20231225 14:30:00"
|
|
45
|
+
simple_date(now); // "12/25"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Parsing Functions
|
|
49
|
+
|
|
50
|
+
#### `parse_date(date_str)`
|
|
51
|
+
Parses a string in `yyyymmdd` format into a Date object (time set to 00:00:00).
|
|
52
|
+
- **param**: `date_str` (string) - E.g., "20231225".
|
|
53
|
+
- **returns**: `Date`
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
const d = parse_date("20230101");
|
|
57
|
+
// d is Date object for Jan 1, 2023 00:00:00 local time
|
|
58
|
+
```
|