@vida-global/core 1.3.1 → 1.3.3
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/.github/workflows/npm-publish.yml +17 -7
- package/.github/workflows/npm-test.yml +5 -3
- package/index.js +8 -13
- package/lib/redis/index.js +1 -1
- package/lib/server/README.md +1 -1
- package/lib/server/apiDocsGenerator.js +44 -13
- package/lib/server/controllerImporter.js +11 -9
- package/lib/server/index.js +2 -0
- package/lib/server/server.js +76 -69
- package/lib/server/serverController.js +169 -57
- package/package.json +2 -2
- package/test/server/server.test.js +5 -3
- package/test/server/serverController.test.js +202 -66
|
@@ -16,13 +16,23 @@ jobs:
|
|
|
16
16
|
publish:
|
|
17
17
|
runs-on: ubuntu-latest
|
|
18
18
|
steps:
|
|
19
|
-
-
|
|
20
|
-
|
|
19
|
+
- name: Checkout code
|
|
20
|
+
uses: actions/checkout@v4
|
|
21
|
+
|
|
22
|
+
- name: Setup Node
|
|
23
|
+
uses: actions/setup-node@v4
|
|
21
24
|
with:
|
|
22
25
|
node-version: '24'
|
|
23
26
|
registry-url: 'https://registry.npmjs.org'
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
27
|
+
|
|
28
|
+
- name: Update npm
|
|
29
|
+
run: npm install -g npm@latest
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: npm ci
|
|
33
|
+
|
|
34
|
+
- name: Run tests
|
|
35
|
+
run: npm test
|
|
36
|
+
|
|
37
|
+
- name: Publish
|
|
38
|
+
run: npm publish
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
# This workflow will run tests using node
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
name: npm test
|
|
2
5
|
|
|
3
6
|
on:
|
|
@@ -6,7 +9,6 @@ on:
|
|
|
6
9
|
jobs:
|
|
7
10
|
test:
|
|
8
11
|
runs-on: ubuntu-latest
|
|
9
|
-
|
|
10
12
|
steps:
|
|
11
13
|
- name: Checkout code
|
|
12
14
|
uses: actions/checkout@v4
|
|
@@ -14,8 +16,8 @@ jobs:
|
|
|
14
16
|
- name: Setup Node
|
|
15
17
|
uses: actions/setup-node@v4
|
|
16
18
|
with:
|
|
17
|
-
node-version:
|
|
18
|
-
|
|
19
|
+
node-version: '24'
|
|
20
|
+
registry-url: 'https://registry.npmjs.org'
|
|
19
21
|
|
|
20
22
|
- name: Install dependencies
|
|
21
23
|
run: npm ci
|
package/index.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const ActiveRecord = require('./lib/activeRecord');
|
|
7
|
-
const { redisClientFactory } = require('./lib/redis');
|
|
1
|
+
const httpLibs = require('./lib/http/client');
|
|
2
|
+
const serverLibs = require('./lib/server');
|
|
3
|
+
const { logger } = require('./lib/logger');
|
|
4
|
+
const ActiveRecord = require('./lib/activeRecord');
|
|
5
|
+
const redisLibs = require('./lib/redis');
|
|
8
6
|
|
|
9
7
|
|
|
10
8
|
module.exports = {
|
|
11
9
|
ActiveRecord,
|
|
12
|
-
|
|
13
|
-
HttpClient,
|
|
14
|
-
HttpError,
|
|
10
|
+
...httpLibs,
|
|
15
11
|
logger,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
VidaServerController,
|
|
12
|
+
...redisLibs,
|
|
13
|
+
...serverLibs
|
|
19
14
|
};
|
package/lib/redis/index.js
CHANGED
package/lib/server/README.md
CHANGED
|
@@ -194,5 +194,5 @@ titleUniqueness(title) {
|
|
|
194
194
|
- `{isInteger: true}`, `{isInteger: {gte: 0, lte: 100}}`
|
|
195
195
|
- `{isDateTime}`
|
|
196
196
|
- `{isEnum: {enums: [a, b, c]}}`
|
|
197
|
-
- `{regex: /foo/}
|
|
197
|
+
- `{isString: true}`, `{isString: {length: {gte: 10, lte: 100}, regex: /foo/}}
|
|
198
198
|
- `{function: someFunction}` If the function returns a string, that is considered an error and returned as the validation message
|
|
@@ -3,7 +3,7 @@ const { ControllerImporter } = require('./controllerImporter');
|
|
|
3
3
|
|
|
4
4
|
class ApiDocsGenerator {
|
|
5
5
|
#controllerDirectories
|
|
6
|
-
#
|
|
6
|
+
#controllerClasses;
|
|
7
7
|
#docs = {};
|
|
8
8
|
|
|
9
9
|
constructor(server) {
|
|
@@ -13,29 +13,60 @@ class ApiDocsGenerator {
|
|
|
13
13
|
|
|
14
14
|
generateDocs() {
|
|
15
15
|
console.log("\n\n");
|
|
16
|
-
this.
|
|
17
|
-
|
|
18
|
-
this.generateDocsForAction(action);
|
|
16
|
+
this.controllerClasses.forEach(controllerClass => {
|
|
17
|
+
controllerClass.actions.forEach(action => {
|
|
18
|
+
this.generateDocsForAction(action, controllerClass);
|
|
19
19
|
});
|
|
20
20
|
});
|
|
21
|
-
console.log("\n
|
|
21
|
+
console.log("\n");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
generateDocsForAction(action) {
|
|
26
|
-
const
|
|
25
|
+
generateDocsForAction(action, controllerClass) {
|
|
26
|
+
const documentationDetails = controllerClass.documentationForAction(action.action);
|
|
27
|
+
if (!documentationDetails?.description) return;
|
|
28
|
+
|
|
29
|
+
const components = this.collectComponents(controllerClass, documentationDetails, action);
|
|
30
|
+
|
|
31
|
+
const endpoint = components.endpoint;
|
|
27
32
|
this.#docs[endpoint] = this.#docs[endpoint] || {};
|
|
28
|
-
const actionDocs = this.#docs[endpoint][action.method.toLowerCase()] = {
|
|
29
|
-
|
|
33
|
+
const actionDocs = this.#docs[endpoint][action.method.toLowerCase()] = {};
|
|
34
|
+
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
collectComponents(controllerClass, documentationDetails, action) {
|
|
39
|
+
const components = {...documentationDetails};
|
|
40
|
+
components.endpoint = this.formatEndpoint(action);
|
|
41
|
+
components.method = action.method;
|
|
42
|
+
|
|
43
|
+
const pathParamNames = components.endpoint.match(/{[^}]+}/g).map(p => {
|
|
44
|
+
return p.replace(/[{}]/g, '')
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const parameters = Object.entries(controllerClass.parametersForAction(action.action));
|
|
48
|
+
const pathParams = parameters.filter(([param, _]) => pathParamNames.includes(param));
|
|
49
|
+
const otherParams = parameters.filter(([param, _]) => !pathParamNames.includes(param));
|
|
50
|
+
console.log(components.endpoint);
|
|
51
|
+
console.log(pathParams);
|
|
52
|
+
console.log(components.method);
|
|
53
|
+
console.log(parameters);
|
|
54
|
+
console.log(components.description);
|
|
55
|
+
console.log(components.summary);
|
|
56
|
+
console.log("\n");
|
|
57
|
+
//const auth = controllerClass.authenticationDocumentation(action.action);
|
|
58
|
+
//const method = action.method;
|
|
59
|
+
|
|
60
|
+
return components;
|
|
30
61
|
}
|
|
31
62
|
|
|
32
63
|
|
|
33
|
-
get
|
|
34
|
-
if (!this.#
|
|
64
|
+
get controllerClasses() {
|
|
65
|
+
if (!this.#controllerClasses) {
|
|
35
66
|
const controllerImporter = new ControllerImporter(this.#controllerDirectories);
|
|
36
|
-
this.#
|
|
67
|
+
this.#controllerClasses = controllerImporter.controllerClasses;
|
|
37
68
|
}
|
|
38
|
-
return this.#
|
|
69
|
+
return [...this.#controllerClasses];
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
|
|
@@ -4,7 +4,7 @@ const { VidaServerController } = require('./serverController');
|
|
|
4
4
|
|
|
5
5
|
class ControllerImporter {
|
|
6
6
|
#controllerDirectories;
|
|
7
|
-
#
|
|
7
|
+
#controllerClasses;
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
constructor(controllerDirectories) {
|
|
@@ -15,16 +15,16 @@ class ControllerImporter {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
get
|
|
19
|
-
if (!this.#
|
|
20
|
-
this.#
|
|
18
|
+
get controllerClasses() {
|
|
19
|
+
if (!this.#controllerClasses) {
|
|
20
|
+
this.#importControllerClasses();
|
|
21
21
|
}
|
|
22
|
-
return this.#
|
|
22
|
+
return this.#controllerClasses;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
this.#
|
|
26
|
+
#importControllerClasses() {
|
|
27
|
+
this.#controllerClasses = [];
|
|
28
28
|
this.#controllerDirectories.forEach(dir => {
|
|
29
29
|
this.#importDirectory(dir, dir);
|
|
30
30
|
});
|
|
@@ -44,12 +44,14 @@ class ControllerImporter {
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
#importFromFile(filePath, dir, topLevelDirectory) {
|
|
47
|
-
const exports
|
|
47
|
+
const exports = Object.values(require(filePath));
|
|
48
48
|
exports.forEach(_export => {
|
|
49
49
|
if (_export.prototype instanceof VidaServerController) {
|
|
50
50
|
const directoryPrefix = dir.replace(topLevelDirectory, '');
|
|
51
51
|
_export.directoryPrefix = directoryPrefix;
|
|
52
|
-
|
|
52
|
+
_export.autoLoadHelpers();
|
|
53
|
+
_export.autoLoadDocumentation();
|
|
54
|
+
this.#controllerClasses.push(_export);
|
|
53
55
|
}
|
|
54
56
|
});
|
|
55
57
|
}
|
package/lib/server/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
const { VidaServer } = require('./server');
|
|
2
2
|
const { VidaServerController } = require('./serverController');
|
|
3
3
|
const ERRORS = require('./errors');
|
|
4
|
+
const { ApiDocsGenerator } = require('./apiDocsGenerator');
|
|
4
5
|
|
|
5
6
|
module.exports = {
|
|
7
|
+
ApiDocsGenerator,
|
|
6
8
|
...ERRORS,
|
|
7
9
|
VidaServer,
|
|
8
10
|
VidaServerController,
|
package/lib/server/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
const { ControllerImporter } = require('./controllerImporter');
|
|
1
2
|
const express = require('express');
|
|
2
|
-
const fs = require('fs');
|
|
3
3
|
const httpLogger = require('pino-http')
|
|
4
4
|
const { logger } = require('../logger');
|
|
5
5
|
const mustacheExpress = require('mustache-express');
|
|
@@ -8,11 +8,6 @@ const responseTime = require('response-time');
|
|
|
8
8
|
const IoServer = require("socket.io")
|
|
9
9
|
const { Server } = require('http');
|
|
10
10
|
const { SystemController } = require('./systemController');
|
|
11
|
-
const { AuthorizationError,
|
|
12
|
-
ForbiddenError,
|
|
13
|
-
NotFoundError,
|
|
14
|
-
ValidationError,
|
|
15
|
-
VidaServerController } = require('./serverController');
|
|
16
11
|
|
|
17
12
|
|
|
18
13
|
class VidaServer {
|
|
@@ -32,11 +27,12 @@ class VidaServer {
|
|
|
32
27
|
}
|
|
33
28
|
|
|
34
29
|
|
|
35
|
-
async listen() {
|
|
30
|
+
async listen(callback) {
|
|
36
31
|
await this.registerControllers();
|
|
37
32
|
|
|
38
33
|
this.#httpServer.listen(this.#port, this.#host, () => {
|
|
39
34
|
this.logger.info(`Server is running on port ${this.#port}`);
|
|
35
|
+
if (callback) callback();
|
|
40
36
|
});
|
|
41
37
|
}
|
|
42
38
|
|
|
@@ -75,6 +71,7 @@ class VidaServer {
|
|
|
75
71
|
***********************************************************************************************/
|
|
76
72
|
setupMiddleware() {
|
|
77
73
|
this.use(this.jsonParsingMiddleware);
|
|
74
|
+
this.use(this.octetStreamParsingMiddleware);
|
|
78
75
|
this.use(responseTime())
|
|
79
76
|
this.use(express.static('public'))
|
|
80
77
|
this.use(this.loggingMiddleware);
|
|
@@ -97,39 +94,26 @@ class VidaServer {
|
|
|
97
94
|
}
|
|
98
95
|
|
|
99
96
|
|
|
100
|
-
get
|
|
101
|
-
return
|
|
102
|
-
logger: this.middlewareLogger,
|
|
103
|
-
customLogLevel: (res, err) => {
|
|
104
|
-
if (res.statusCode >= 500) return 'error';
|
|
105
|
-
if (res.statusCode >= 400) return 'warn';
|
|
106
|
-
return 'info';
|
|
107
|
-
},
|
|
108
|
-
serializers: {
|
|
109
|
-
// Format the request log as a string
|
|
110
|
-
req: (req) => `Request: ${req.method} ${req.url}`,
|
|
111
|
-
// Format the response log as a string
|
|
112
|
-
res: (res) => `statusCode=${res.statusCode}, responseTime=${res.get('X-Response-Time')}`,
|
|
113
|
-
},
|
|
114
|
-
wrapSerializers: false,
|
|
115
|
-
})
|
|
97
|
+
get octetStreamParsingMiddleware() {
|
|
98
|
+
return express.raw({ type: 'application/octet-stream', limit: this.octetStreamLimit });
|
|
116
99
|
}
|
|
117
100
|
|
|
118
101
|
|
|
119
|
-
get
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
102
|
+
get octetStreamLimit() {
|
|
103
|
+
return '128mb';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
get loggingMiddleware() {
|
|
108
|
+
return httpLogger({
|
|
109
|
+
logger: this.middlewareLogger,
|
|
110
|
+
customLogLevel: this.requestLogLevel.bind(this),
|
|
111
|
+
customSuccessMessage: this.requestLogMessage.bind(this),
|
|
112
|
+
customErrorMessage: this.requestLogMessage.bind(this),
|
|
113
|
+
customErrorObject: this.requestLogDetails.bind(this),
|
|
114
|
+
customSuccessObject: this.requestLogDetails.bind(this),
|
|
115
|
+
wrapSerializers: false,
|
|
116
|
+
})
|
|
133
117
|
}
|
|
134
118
|
|
|
135
119
|
|
|
@@ -151,6 +135,53 @@ class VidaServer {
|
|
|
151
135
|
use() { this.#expressServer.use(...arguments); }
|
|
152
136
|
|
|
153
137
|
|
|
138
|
+
/***********************************************************************************************
|
|
139
|
+
* LOGGING
|
|
140
|
+
***********************************************************************************************/
|
|
141
|
+
requestLogDetails(req, res, err) {
|
|
142
|
+
const details = {
|
|
143
|
+
req: `${req.method} ${req.url}`,
|
|
144
|
+
res: `statusCode=${res.statusCode}, responseTime=${res.get('X-Response-Time')}`,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (res.statusCode >= 500 && res.error) {
|
|
148
|
+
details.error = this.requestLogErrorDetails(req, res, res.error);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return details;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
requestLogErrorDetails(req, res, err) {
|
|
156
|
+
if (typeof err == 'string') return {message: err};
|
|
157
|
+
return {
|
|
158
|
+
type: err.constructor.name,
|
|
159
|
+
message: err.message,
|
|
160
|
+
stack: err.stack
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
requestLogMessage(req, res) {
|
|
166
|
+
const prefix = '[VidaServer]';
|
|
167
|
+
if (req.controller) {
|
|
168
|
+
return `${prefix} ${req.controller.constructor.name}#${req.action}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return `${prefix} ${res.status >= 500 ? 'request errored' : 'request completed'}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
requestLogLevel(req, res) {
|
|
176
|
+
return 'debug';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
get middlewareLogger() {
|
|
181
|
+
return logger;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
154
185
|
/***********************************************************************************************
|
|
155
186
|
* SETTINGS
|
|
156
187
|
***********************************************************************************************/
|
|
@@ -193,7 +224,6 @@ class VidaServer {
|
|
|
193
224
|
|
|
194
225
|
|
|
195
226
|
#registerController(controllerCls) {
|
|
196
|
-
controllerCls.autoLoadHelpers();
|
|
197
227
|
controllerCls.actions.forEach((action => {
|
|
198
228
|
this.#registerAction(action, controllerCls)
|
|
199
229
|
}).bind(this));
|
|
@@ -215,12 +245,14 @@ class VidaServer {
|
|
|
215
245
|
_put() { this.#expressServer.put(...arguments); }
|
|
216
246
|
_delete() { this.#expressServer.delete(...arguments); }
|
|
217
247
|
_patch() { this.#expressServer.patch(...arguments); }
|
|
248
|
+
_head() { this.#expressServer.head(...arguments); }
|
|
218
249
|
|
|
219
250
|
|
|
220
251
|
requestHandler(action, controllerCls) {
|
|
221
252
|
return async function(request, response) {
|
|
222
|
-
if (process.env.NODE_ENV != 'test') this.logger.info(`${controllerCls.name}#${action}`);
|
|
223
253
|
const controllerInstance = this.buildController(controllerCls, request, response);
|
|
254
|
+
request.controller = controllerInstance;
|
|
255
|
+
request.action = action;
|
|
224
256
|
await controllerInstance.performRequest(action);
|
|
225
257
|
}.bind(this);
|
|
226
258
|
}
|
|
@@ -232,41 +264,15 @@ class VidaServer {
|
|
|
232
264
|
|
|
233
265
|
|
|
234
266
|
get controllerClasses() {
|
|
235
|
-
if (this.#controllerClasses)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
this.#importControllers(dir, dir);
|
|
240
|
-
});
|
|
267
|
+
if (!this.#controllerClasses) {
|
|
268
|
+
const controllerImporter = new ControllerImporter(this.controllerDirectories);
|
|
269
|
+
this.#controllerClasses = controllerImporter.controllerClasses;
|
|
270
|
+
}
|
|
241
271
|
|
|
242
272
|
return this.#controllerClasses;
|
|
243
273
|
}
|
|
244
274
|
|
|
245
275
|
|
|
246
|
-
#importControllers(dir, topLevelDirectory) {
|
|
247
|
-
fs.readdirSync(dir).forEach((f => {
|
|
248
|
-
const filePath = `${dir}/${f}`;
|
|
249
|
-
if (fs.lstatSync(filePath).isDirectory()) {
|
|
250
|
-
this.#importControllers(filePath, topLevelDirectory);
|
|
251
|
-
} else if (f.endsWith('.js')) {
|
|
252
|
-
this.#importControllersFromFile(filePath, dir, topLevelDirectory);
|
|
253
|
-
}
|
|
254
|
-
}).bind(this));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
#importControllersFromFile(filePath, dir, topLevelDirectory) {
|
|
259
|
-
const exports = Object.values(require(filePath));
|
|
260
|
-
exports.forEach(_export => {
|
|
261
|
-
if (_export.prototype instanceof VidaServerController) {
|
|
262
|
-
const directoryPrefix = dir.replace(topLevelDirectory, '');
|
|
263
|
-
_export.directoryPrefix = directoryPrefix;
|
|
264
|
-
this.#controllerClasses.push(_export);
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
|
|
270
276
|
get controllerDirectories() {
|
|
271
277
|
return [`${process.cwd()}/controllers`];
|
|
272
278
|
}
|
|
@@ -291,6 +297,7 @@ class VidaServer {
|
|
|
291
297
|
post() { this.handleDeprecatedRoutingCall('post', arguments); }
|
|
292
298
|
put() { this.handleDeprecatedRoutingCall('put', arguments); }
|
|
293
299
|
patch() { this.handleDeprecatedRoutingCall('patch', arguments); }
|
|
300
|
+
head() { this.handleDeprecatedRoutingCall('head', arguments); }
|
|
294
301
|
delete() { this.handleDeprecatedRoutingCall('delete', arguments); }
|
|
295
302
|
|
|
296
303
|
|