hola-server 1.0.11 → 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/db/gridfs.js
CHANGED
|
@@ -1,221 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview GridFS file storage utilities.
|
|
3
|
+
* @module db/gridfs
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const fs = require('fs');
|
|
2
7
|
const { GridFSBucket, MongoClient } = require('mongodb');
|
|
3
8
|
const { get_settings } = require('../setting');
|
|
4
9
|
|
|
10
|
+
const CHUNK_SIZE = 1024 * 1024; // 1MB
|
|
11
|
+
|
|
5
12
|
let gridfs_instance;
|
|
6
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Get or create GridFS singleton instance.
|
|
16
|
+
* @returns {Promise<GridFS>} GridFS instance
|
|
17
|
+
*/
|
|
7
18
|
const get_gridfs_instance = async () => {
|
|
8
|
-
if (gridfs_instance)
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
if (gridfs_instance) return gridfs_instance;
|
|
20
|
+
|
|
21
|
+
const { url } = get_settings().mongo;
|
|
22
|
+
const client = await MongoClient.connect(url, { useUnifiedTopology: true, useNewUrlParser: true });
|
|
23
|
+
|
|
24
|
+
gridfs_instance = new GridFS(client.db());
|
|
25
|
+
return gridfs_instance;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Wrap stream in promise.
|
|
30
|
+
* @param {Stream} stream - Stream to wrap
|
|
31
|
+
* @returns {Promise} Resolves on finish, rejects on error
|
|
32
|
+
*/
|
|
33
|
+
const stream_to_promise = (stream) => new Promise((resolve, reject) => {
|
|
34
|
+
stream.on('error', reject).on('finish', resolve);
|
|
35
|
+
});
|
|
18
36
|
|
|
19
37
|
class GridFS {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* @param {url of mongo} url
|
|
23
|
-
* @returns
|
|
24
|
-
*/
|
|
25
|
-
connect(url) {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
MongoClient.connect(url, { useUnifiedTopology: true, useNewUrlParser: true }, (err, client) => {
|
|
28
|
-
if (err) return reject(err);
|
|
29
|
-
resolve(client);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
38
|
+
constructor(db) {
|
|
39
|
+
this.db = db;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
|
-
*
|
|
36
|
-
* @param {
|
|
37
|
-
* @
|
|
38
|
-
* @param {the file path} filepath
|
|
39
|
-
* @returns
|
|
43
|
+
* Get GridFS bucket.
|
|
44
|
+
* @param {string} bucket_name - Bucket name
|
|
45
|
+
* @returns {GridFSBucket} Bucket instance
|
|
40
46
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const files = await bucket.find({ filename: filename }).toArray();
|
|
44
|
-
if (files && files.length > 0) {
|
|
45
|
-
await delete_file(bucket, files[0]._id);
|
|
46
|
-
}
|
|
47
|
-
return new Promise((resolve, reject) => {
|
|
48
|
-
if (typeof filepath === 'string') {
|
|
49
|
-
fs.createReadStream(filepath).pipe(bucket.openUploadStream(filename))
|
|
50
|
-
.on('error', err => reject(err))
|
|
51
|
-
.on('finish', item => resolve(item));
|
|
52
|
-
} else {
|
|
53
|
-
filepath.pipe(bucket.openUploadStream(filename))
|
|
54
|
-
.on('error', err => reject(err))
|
|
55
|
-
.on('finish', item => resolve(item));
|
|
56
|
-
}
|
|
57
|
-
});
|
|
47
|
+
bucket(bucket_name) {
|
|
48
|
+
return new GridFSBucket(this.db, { chunkSizeBytes: CHUNK_SIZE, bucketName: bucket_name });
|
|
58
49
|
}
|
|
59
50
|
|
|
60
51
|
/**
|
|
61
|
-
*
|
|
62
|
-
* @param {
|
|
63
|
-
* @param {
|
|
64
|
-
* @param {
|
|
52
|
+
* Save file to GridFS (replaces existing if found).
|
|
53
|
+
* @param {string} bucket_name - Bucket name
|
|
54
|
+
* @param {string} filename - File name
|
|
55
|
+
* @param {string|Stream} source - File path or readable stream
|
|
56
|
+
* @returns {Promise} Upload result
|
|
65
57
|
*/
|
|
66
|
-
|
|
67
|
-
const bucket =
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
stream
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
async save_file(bucket_name, filename, source) {
|
|
59
|
+
const bucket = this.bucket(bucket_name);
|
|
60
|
+
|
|
61
|
+
// Delete existing file if present
|
|
62
|
+
const existing = await bucket.find({ filename }).toArray();
|
|
63
|
+
if (existing.length > 0) {
|
|
64
|
+
await bucket.delete(existing[0]._id);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Create read stream from path if string
|
|
68
|
+
const read_stream = typeof source === 'string' ? fs.createReadStream(source) : source;
|
|
69
|
+
return stream_to_promise(read_stream.pipe(bucket.openUploadStream(filename)));
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
/**
|
|
81
|
-
*
|
|
82
|
-
* @param {
|
|
83
|
-
* @param {
|
|
84
|
-
* @param {
|
|
73
|
+
* Stream file to HTTP response.
|
|
74
|
+
* @param {string} bucket_name - Bucket name
|
|
75
|
+
* @param {string} filename - File name
|
|
76
|
+
* @param {Response} response - HTTP response object
|
|
85
77
|
*/
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
stream.pipe(write_stream)
|
|
93
|
-
.on('error', err => reject(err))
|
|
94
|
-
.on('finish', item => resolve(item));
|
|
95
|
-
});
|
|
78
|
+
read_file(bucket_name, filename, response) {
|
|
79
|
+
const stream = this.bucket(bucket_name).openDownloadStreamByName(filename);
|
|
80
|
+
stream.on('data', chunk => response.write(chunk));
|
|
81
|
+
stream.on('error', () => response.sendStatus(404));
|
|
82
|
+
stream.on('end', () => response.end());
|
|
96
83
|
}
|
|
97
84
|
|
|
98
85
|
/**
|
|
99
|
-
*
|
|
100
|
-
* @param {
|
|
101
|
-
* @param {
|
|
86
|
+
* Pipe file from GridFS to disk.
|
|
87
|
+
* @param {string} bucket_name - Bucket name
|
|
88
|
+
* @param {string} filename - Source file name
|
|
89
|
+
* @param {string} dest_path - Destination file path
|
|
90
|
+
* @returns {Promise} Pipe result
|
|
102
91
|
*/
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
if (files && files.length > 0) {
|
|
107
|
-
await delete_file(bucket, files[0]._id);
|
|
108
|
-
}
|
|
92
|
+
pipe_file(bucket_name, filename, dest_path) {
|
|
93
|
+
const stream = this.bucket(bucket_name).openDownloadStreamByName(filename);
|
|
94
|
+
return stream_to_promise(stream.pipe(fs.createWriteStream(dest_path)));
|
|
109
95
|
}
|
|
110
96
|
|
|
111
97
|
/**
|
|
112
|
-
* Delete
|
|
113
|
-
* @param {
|
|
114
|
-
* @param {
|
|
115
|
-
* @returns
|
|
98
|
+
* Delete file by name.
|
|
99
|
+
* @param {string} bucket_name - Bucket name
|
|
100
|
+
* @param {string} filename - File name
|
|
116
101
|
*/
|
|
117
|
-
delete_file(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
resolve(true);
|
|
124
|
-
}
|
|
125
|
-
}));
|
|
126
|
-
});
|
|
102
|
+
async delete_file(bucket_name, filename) {
|
|
103
|
+
const bucket = this.bucket(bucket_name);
|
|
104
|
+
const files = await bucket.find({ filename }).toArray();
|
|
105
|
+
if (files.length > 0) {
|
|
106
|
+
await bucket.delete(files[0]._id);
|
|
107
|
+
}
|
|
127
108
|
}
|
|
128
109
|
}
|
|
129
110
|
|
|
130
111
|
/**
|
|
131
|
-
*
|
|
132
|
-
* @param {meta
|
|
133
|
-
* @param {
|
|
134
|
-
* @param {
|
|
112
|
+
* Set file field values on entity object based on uploaded files.
|
|
113
|
+
* @param {Object} meta - Entity meta info
|
|
114
|
+
* @param {Request} req - HTTP request
|
|
115
|
+
* @param {Object} obj - Entity object
|
|
135
116
|
*/
|
|
136
|
-
const set_file_fields =
|
|
137
|
-
const file_fields = meta
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
delete obj[field.name];
|
|
152
|
-
}
|
|
153
|
-
});
|
|
117
|
+
const set_file_fields = (meta, req, obj) => {
|
|
118
|
+
const { file_fields, primary_keys } = meta;
|
|
119
|
+
if (!file_fields?.length || !req.files) return;
|
|
120
|
+
|
|
121
|
+
const primary_key = primary_keys.map(key => obj[key]).join('_');
|
|
122
|
+
|
|
123
|
+
for (const field of file_fields) {
|
|
124
|
+
const files = req.files[field.name];
|
|
125
|
+
if (files?.length > 0) {
|
|
126
|
+
obj[field.name] = file_fields.length === 1 ? primary_key : `${primary_key}_${field.name}`;
|
|
127
|
+
} else {
|
|
128
|
+
delete obj[field.name];
|
|
129
|
+
}
|
|
154
130
|
}
|
|
155
|
-
}
|
|
131
|
+
};
|
|
156
132
|
|
|
157
133
|
/**
|
|
158
|
-
*
|
|
159
|
-
* @param {
|
|
160
|
-
* @param {
|
|
161
|
-
* @param {
|
|
162
|
-
* @param {
|
|
134
|
+
* Save uploaded file fields to GridFS.
|
|
135
|
+
* @param {string} collection - MongoDB collection name
|
|
136
|
+
* @param {Object[]} file_fields - File field definitions
|
|
137
|
+
* @param {Request} req - HTTP request
|
|
138
|
+
* @param {Object} obj - Entity object
|
|
163
139
|
*/
|
|
164
|
-
const save_file_fields_to_db = async
|
|
165
|
-
if (file_fields
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
140
|
+
const save_file_fields_to_db = async (collection, file_fields, req, obj) => {
|
|
141
|
+
if (!file_fields?.length || !req.files) return;
|
|
142
|
+
|
|
143
|
+
const instance = await get_gridfs_instance();
|
|
144
|
+
|
|
145
|
+
for (const field of file_fields) {
|
|
146
|
+
if (!obj[field.name]) continue;
|
|
147
|
+
|
|
148
|
+
const [file] = req.files[field.name];
|
|
149
|
+
await instance.save_file(collection, obj[field.name], file.path);
|
|
150
|
+
fs.unlinkSync(file.path);
|
|
175
151
|
}
|
|
176
|
-
}
|
|
152
|
+
};
|
|
177
153
|
|
|
178
|
-
|
|
179
|
-
* Save file to gridfs file
|
|
180
|
-
* @param {collection name} collection
|
|
181
|
-
* @param {file name} filename
|
|
182
|
-
* @param {file full path} filepath
|
|
183
|
-
*/
|
|
154
|
+
// Wrapper functions for external API
|
|
184
155
|
const save_file = async (collection, filename, filepath) => {
|
|
185
156
|
const instance = await get_gridfs_instance();
|
|
186
157
|
await instance.save_file(collection, filename, filepath);
|
|
187
|
-
}
|
|
158
|
+
};
|
|
188
159
|
|
|
189
|
-
/**
|
|
190
|
-
* read file from gridfs
|
|
191
|
-
* @param {collection name} collection
|
|
192
|
-
* @param {file name} filename
|
|
193
|
-
* @param {http response} response
|
|
194
|
-
*/
|
|
195
160
|
const read_file = async (collection, filename, response) => {
|
|
196
161
|
const instance = await get_gridfs_instance();
|
|
197
|
-
|
|
198
|
-
}
|
|
162
|
+
instance.read_file(collection, filename, response);
|
|
163
|
+
};
|
|
199
164
|
|
|
200
|
-
/**
|
|
201
|
-
* pipe file from gridfs
|
|
202
|
-
* @param {collection name} collection
|
|
203
|
-
* @param {file name} filename
|
|
204
|
-
* @param {dest file name} dest_filename
|
|
205
|
-
*/
|
|
206
165
|
const pipe_file = async (collection, filename, dest_filename) => {
|
|
207
166
|
const instance = await get_gridfs_instance();
|
|
208
167
|
await instance.pipe_file(collection, filename, dest_filename);
|
|
209
|
-
}
|
|
168
|
+
};
|
|
210
169
|
|
|
211
|
-
/**
|
|
212
|
-
* delete file from gridfs
|
|
213
|
-
* @param {collection name} collection
|
|
214
|
-
* @param {file name} filename
|
|
215
|
-
*/
|
|
216
170
|
const delete_file = async (collection, filename) => {
|
|
217
171
|
const instance = await get_gridfs_instance();
|
|
218
|
-
await instance.
|
|
219
|
-
}
|
|
172
|
+
await instance.delete_file(collection, filename);
|
|
173
|
+
};
|
|
220
174
|
|
|
221
175
|
module.exports = { set_file_fields, save_file_fields_to_db, save_file, read_file, pipe_file, delete_file };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Default Field Attribute
|
|
2
|
+
|
|
3
|
+
Added `default` attribute support to meta field definitions for automatic value population during create operations.
|
|
4
|
+
|
|
5
|
+
## Changes
|
|
6
|
+
|
|
7
|
+
### Server: `core/meta.js`
|
|
8
|
+
|
|
9
|
+
1. Added `default` to `FIELD_ATTRS` array
|
|
10
|
+
2. Added validation in `validate_field`:
|
|
11
|
+
- Validates default value against field type using `type.convert()`
|
|
12
|
+
- Throws error if default value is invalid for the field type
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// Validate default value against field type
|
|
16
|
+
if (field.default !== undefined && field.type) {
|
|
17
|
+
const type = get_type(field.type);
|
|
18
|
+
const result = type.convert(field.default);
|
|
19
|
+
if (result.err) throw meta_error(meta.collection, `invalid default value...`);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Client: `components/BasicForm.vue`
|
|
24
|
+
|
|
25
|
+
1. Added `apply_defaults(form)` method that applies default values to empty form fields
|
|
26
|
+
2. Called during initialization and form prop changes
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
apply_defaults(form) {
|
|
30
|
+
const result = { ...form };
|
|
31
|
+
for (const field of this.fields) {
|
|
32
|
+
if (field.default !== undefined &&
|
|
33
|
+
(result[field.name] === undefined || result[field.name] === null || result[field.name] === "")) {
|
|
34
|
+
result[field.name] = field.default;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
{
|
|
45
|
+
collection: "products",
|
|
46
|
+
primary_keys: ["name"],
|
|
47
|
+
fields: [
|
|
48
|
+
{ name: "name", type: "string", required: true },
|
|
49
|
+
{ name: "quantity", type: "int", default: 0 },
|
|
50
|
+
{ name: "active", type: "boolean", default: true },
|
|
51
|
+
{ name: "price", type: "float", default: 9.99 }
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
When creating a new product, form fields automatically populate with their defined default values.
|
package/http/context.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Request context management using AsyncLocalStorage.
|
|
3
|
+
* @module http/context
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
2
7
|
const asyncLocalStorage = new AsyncLocalStorage();
|
|
3
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Set value in current request context.
|
|
11
|
+
* @param {string} key - Context key
|
|
12
|
+
* @param {*} obj - Value to store
|
|
13
|
+
*/
|
|
4
14
|
const set_context_value = (key, obj) => {
|
|
5
15
|
const store = asyncLocalStorage.getStore();
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
if (store) {
|
|
17
|
+
store[key] = obj;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
8
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Get value from current request context.
|
|
23
|
+
* @param {string} key - Context key
|
|
24
|
+
* @returns {*} Stored value or null
|
|
25
|
+
*/
|
|
9
26
|
const get_context_value = (key) => {
|
|
10
27
|
const store = asyncLocalStorage.getStore();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
return store[key];
|
|
15
|
-
}
|
|
28
|
+
return store ? store[key] : null;
|
|
29
|
+
};
|
|
16
30
|
|
|
17
|
-
module.exports = { asyncLocalStorage, set_context_value, get_context_value }
|
|
31
|
+
module.exports = { asyncLocalStorage, set_context_value, get_context_value };
|
package/http/cors.js
CHANGED
|
@@ -1,15 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CORS middleware initialization.
|
|
3
|
+
* @module http/cors
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const cors = require('cors');
|
|
2
7
|
const { get_settings } = require('../setting');
|
|
3
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Initialize CORS middleware with client URL whitelist.
|
|
11
|
+
* @param {Object} app - Express application instance
|
|
12
|
+
* @throws {Error} If server settings are missing
|
|
13
|
+
*/
|
|
4
14
|
const init_cors = (app) => {
|
|
5
|
-
const
|
|
6
|
-
if (server
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
const settings = get_settings();
|
|
16
|
+
if (!settings?.server) {
|
|
17
|
+
throw new Error('Server settings required for CORS initialization');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const client_urls = settings.server.client_web_url;
|
|
21
|
+
if (!Array.isArray(client_urls)) {
|
|
22
|
+
return;
|
|
12
23
|
}
|
|
13
|
-
|
|
24
|
+
|
|
25
|
+
app.use(cors({
|
|
26
|
+
origin: client_urls,
|
|
27
|
+
methods: ['GET', 'POST'],
|
|
28
|
+
credentials: true
|
|
29
|
+
}));
|
|
30
|
+
};
|
|
14
31
|
|
|
15
32
|
module.exports = { init_cors };
|
package/http/error.js
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Error handling middleware and utilities.
|
|
3
|
+
* @module http/error
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const { ERROR } = require('./code');
|
|
2
7
|
const { LOG_SYSTEM, is_log_error, log_error } = require('../db/db');
|
|
3
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Wrap async route handler to catch errors.
|
|
11
|
+
* @param {Function} fn - Async route handler
|
|
12
|
+
* @returns {Function} Wrapped handler
|
|
13
|
+
*/
|
|
4
14
|
const wrap_http = fn => (...args) => fn(...args).catch(args[2]);
|
|
5
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Global error handler middleware.
|
|
18
|
+
* Logs errors when enabled and returns standardized error response.
|
|
19
|
+
* @param {Object} app - Express application instance
|
|
20
|
+
*/
|
|
6
21
|
const handle_exception = app => {
|
|
7
22
|
app.use(function (error, req, res, next) {
|
|
8
|
-
if (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
if (error) {
|
|
24
|
+
const error_msg = error.stack || error.message || JSON.stringify(error);
|
|
25
|
+
|
|
26
|
+
if (is_log_error()) {
|
|
27
|
+
log_error(LOG_SYSTEM, error_msg, { path: req.originalUrl, method: req.method });
|
|
28
|
+
} else {
|
|
29
|
+
console.error('[Error]', error_msg);
|
|
12
30
|
}
|
|
13
|
-
} else {
|
|
14
|
-
throw new Error(error);
|
|
15
31
|
}
|
|
16
32
|
|
|
17
|
-
res.
|
|
33
|
+
if (!res.headersSent) {
|
|
34
|
+
res.json({ code: ERROR, err: "server error" });
|
|
35
|
+
}
|
|
18
36
|
});
|
|
19
|
-
}
|
|
37
|
+
};
|
|
20
38
|
|
|
21
|
-
module.exports = { wrap_http, handle_exception }
|
|
39
|
+
module.exports = { wrap_http, handle_exception };
|
package/http/express.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Express server initialization and configuration.
|
|
3
|
+
* @module http/express
|
|
4
|
+
*/
|
|
5
|
+
|
|
1
6
|
const express = require('express');
|
|
2
7
|
|
|
3
8
|
const { init_cors } = require('./cors');
|
|
4
9
|
const { NO_SESSION } = require('./code');
|
|
5
|
-
const { init_session,
|
|
10
|
+
const { init_session, get_session_user_id } = require('./session');
|
|
6
11
|
const { init_router_dirs } = require('./router');
|
|
12
|
+
const { ensure_entity_indexes } = require('../db/db');
|
|
7
13
|
const { handle_exception } = require('./error');
|
|
8
14
|
const { get_settings } = require('../setting');
|
|
9
15
|
const { asyncLocalStorage, set_context_value } = require('./context');
|
|
@@ -11,65 +17,88 @@ const { asyncLocalStorage, set_context_value } = require('./context');
|
|
|
11
17
|
const app = express();
|
|
12
18
|
let server_initialized = false;
|
|
13
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Check if URL is in the excluded list.
|
|
22
|
+
* @param {Object} server - Server settings
|
|
23
|
+
* @param {Object} req - Express request
|
|
24
|
+
* @returns {boolean} True if URL should be excluded from auth
|
|
25
|
+
*/
|
|
14
26
|
const is_excluded_url = (server, req) => {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
27
|
+
const patterns = server.exclude_urls;
|
|
28
|
+
if (!Array.isArray(patterns)) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return patterns.some(pattern => new RegExp(pattern, "i").test(req.originalUrl));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Configure body parser middleware with optional size limit.
|
|
36
|
+
* @param {Object} app - Express application
|
|
37
|
+
* @param {string|undefined} body_limit - Optional body size limit
|
|
38
|
+
*/
|
|
39
|
+
const configure_body_parser = (app, body_limit) => {
|
|
40
|
+
const options = body_limit ? { limit: body_limit, extended: true } : { extended: true };
|
|
41
|
+
app.use(express.json(options));
|
|
42
|
+
app.use(express.urlencoded(options));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create authentication middleware.
|
|
47
|
+
* @param {Object} server - Server settings
|
|
48
|
+
* @returns {Function} Express middleware
|
|
49
|
+
*/
|
|
50
|
+
const create_auth_middleware = (server) => (req, res, next) => {
|
|
51
|
+
if (server.check_user && !is_excluded_url(server, req)) {
|
|
52
|
+
if (!get_session_user_id(req)) {
|
|
53
|
+
res.json({ code: NO_SESSION, err: "authentication required" });
|
|
54
|
+
return;
|
|
20
55
|
}
|
|
21
56
|
}
|
|
22
57
|
|
|
23
|
-
|
|
24
|
-
|
|
58
|
+
asyncLocalStorage.run({}, () => {
|
|
59
|
+
set_context_value("req", req);
|
|
60
|
+
next();
|
|
61
|
+
});
|
|
62
|
+
};
|
|
25
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Initialize Express server with middleware and routes.
|
|
66
|
+
* @param {string} base_dir - Base directory for router files
|
|
67
|
+
* @param {string} port_attr - Port attribute name in settings
|
|
68
|
+
* @param {Function} [callback] - Optional callback after server starts
|
|
69
|
+
* @returns {Object} Express app instance
|
|
70
|
+
* @throws {Error} If server settings are invalid
|
|
71
|
+
*/
|
|
26
72
|
const init_express_server = (base_dir, port_attr, callback) => {
|
|
27
|
-
if (server_initialized
|
|
73
|
+
if (server_initialized) {
|
|
28
74
|
return app;
|
|
29
75
|
}
|
|
30
76
|
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
77
|
+
const settings = get_settings();
|
|
78
|
+
if (!settings?.server) {
|
|
79
|
+
throw new Error('Server settings required for initialization');
|
|
80
|
+
}
|
|
35
81
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
82
|
+
const { server } = settings;
|
|
83
|
+
const port = server[port_attr];
|
|
84
|
+
if (!port) {
|
|
85
|
+
throw new Error(`Port attribute '${port_attr}' not found in server settings`);
|
|
39
86
|
}
|
|
40
87
|
|
|
88
|
+
init_cors(app);
|
|
89
|
+
configure_body_parser(app, server.threshold?.body_limit);
|
|
41
90
|
init_session(app);
|
|
42
|
-
|
|
43
|
-
app.use((req, res, next) => {
|
|
44
|
-
if (server.check_user && !is_excluded_url(server, req)) {
|
|
45
|
-
const user_id = get_session_userid(req);
|
|
46
|
-
if (user_id == null) {
|
|
47
|
-
res.json({ code: NO_SESSION, err: "no session found" });
|
|
48
|
-
} else {
|
|
49
|
-
asyncLocalStorage.run({}, () => {
|
|
50
|
-
set_context_value("req", req);
|
|
51
|
-
next();
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
asyncLocalStorage.run({}, () => {
|
|
56
|
-
set_context_value("req", req);
|
|
57
|
-
next();
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
});
|
|
61
|
-
|
|
91
|
+
app.use(create_auth_middleware(server));
|
|
62
92
|
init_router_dirs(app, base_dir);
|
|
93
|
+
ensure_entity_indexes();
|
|
63
94
|
handle_exception(app);
|
|
64
95
|
|
|
65
|
-
app.listen(
|
|
66
|
-
if (callback)
|
|
67
|
-
await callback();
|
|
68
|
-
}
|
|
96
|
+
app.listen(port, async () => {
|
|
97
|
+
if (callback) await callback();
|
|
69
98
|
});
|
|
70
99
|
|
|
71
100
|
server_initialized = true;
|
|
72
101
|
return app;
|
|
73
|
-
}
|
|
102
|
+
};
|
|
74
103
|
|
|
75
104
|
module.exports = { init_express_server };
|