@vida-global/core 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +9 -0
  2. package/index.js +17 -0
  3. package/lib/active_record/README.md +205 -0
  4. package/lib/active_record/baseRecord.js +112 -0
  5. package/lib/active_record/db/connection.js +128 -0
  6. package/lib/active_record/db/connectionConfiguration.js +114 -0
  7. package/lib/active_record/db/importSchema.js +4 -0
  8. package/lib/active_record/db/migration.js +132 -0
  9. package/lib/active_record/db/migrationTemplate.js +8 -0
  10. package/lib/active_record/db/migrationVersion.js +68 -0
  11. package/lib/active_record/db/migrator.js +169 -0
  12. package/lib/active_record/db/queryInterface.js +47 -0
  13. package/lib/active_record/db/schema.js +113 -0
  14. package/lib/active_record/index.js +6 -0
  15. package/lib/active_record/utils.js +43 -0
  16. package/lib/http/README.md +32 -0
  17. package/lib/http/client.js +129 -0
  18. package/lib/http/error.js +34 -0
  19. package/lib/logger/README.md +2 -0
  20. package/lib/logger/index.js +16 -0
  21. package/lib/release/develop.js +27 -0
  22. package/lib/release/git.js +86 -0
  23. package/lib/release/increment.js +56 -0
  24. package/lib/release/index.js +10 -0
  25. package/lib/release/release.js +30 -0
  26. package/lib/release/utils.js +44 -0
  27. package/lib/server/README.md +37 -0
  28. package/lib/server/index.js +9 -0
  29. package/lib/server/server.js +359 -0
  30. package/lib/server/serverController.js +344 -0
  31. package/lib/server/systemController.js +23 -0
  32. package/package.json +37 -0
  33. package/scripts/active_record/migrate.js +30 -0
  34. package/scripts/release.js +62 -0
  35. package/test/active_record/baseRecord.test.js +179 -0
  36. package/test/active_record/db/connection.test.js +221 -0
  37. package/test/active_record/db/connectionConfiguration.test.js +184 -0
  38. package/test/active_record/db/migrator.test.js +266 -0
  39. package/test/active_record/db/queryInterface.test.js +66 -0
  40. package/test/http/client.test.js +271 -0
  41. package/test/http/error.test.js +71 -0
  42. package/test/release/develop.test.js +57 -0
  43. package/test/release/git.test.js +189 -0
  44. package/test/release/increment.test.js +145 -0
  45. package/test/release/release.test.js +72 -0
  46. package/test/release/utils.test.js +148 -0
  47. package/test/server/helpers/controllers/barController.js +9 -0
  48. package/test/server/helpers/controllers/fooController.js +48 -0
  49. package/test/server/helpers/controllers/sub/bazController.js +10 -0
  50. package/test/server/helpers/server.js +14 -0
  51. package/test/server/server.test.js +188 -0
  52. package/test/server/serverController.test.js +251 -0
@@ -0,0 +1,359 @@
1
+ const express = require('express');
2
+ const fs = require('fs');
3
+ const httpLogger = require('pino-http')
4
+ const { logger } = require('../logger');
5
+ const mustacheExpress = require('mustache-express');
6
+ const { pino } = require('pino');
7
+ const responseTime = require('response-time');
8
+ const IoServer = require("socket.io")
9
+ const { Server } = require('http');
10
+ const { SystemController } = require('./systemController');
11
+ const { AuthorizationError,
12
+ ValidationError,
13
+ VidaServerController } = require('./serverController');
14
+
15
+
16
+ class VidaServer {
17
+ #controllerClasses;
18
+ #_expressServer;
19
+ #host;
20
+ #_httpServer;
21
+ #_ioServer;
22
+ #port;
23
+
24
+
25
+ constructor({ port, host }={}) {
26
+ this.#port = port || process.env.VIDA_PORT || 3000;
27
+ this.#host = host || process.env.VIDA_HOST || 'localhost';
28
+ this.setupMiddleware();
29
+ this.configureSettings();
30
+ }
31
+
32
+
33
+ async listen() {
34
+ await this.registerControllers();
35
+
36
+ this.#httpServer.listen(this.#port, this.#host, () => {
37
+ this.logger.info(`Server is running on port ${this.#port}`);
38
+ });
39
+ }
40
+
41
+
42
+ get host() { return this.#host; }
43
+ get port() { return this.#port; }
44
+ get logger() { return logger; }
45
+
46
+
47
+ /***********************************************************************************************
48
+ * WEB SOCKETS
49
+ ***********************************************************************************************/
50
+ get #ioServer() {
51
+ if (!this.#_ioServer) {
52
+ this.#_ioServer = IoServer(this.#httpServer, {
53
+ cors: { origin: this.corsOrigins, credentials: true }
54
+ });
55
+ }
56
+
57
+ return this.#_ioServer;
58
+ }
59
+
60
+
61
+ emitWebSocketEvent(channel, eventName, eventData) {
62
+ this.#ioServer.to(channel).emit(eventName, eventData);
63
+ }
64
+
65
+
66
+ addWebSocketEventHandler(eventName, eventHandler) {
67
+ this.#ioServer.on(eventName, eventHandler);
68
+ }
69
+
70
+
71
+ /***********************************************************************************************
72
+ * MIDDLEWARE
73
+ ***********************************************************************************************/
74
+ setupMiddleware() {
75
+ this.use(this.jsonParsingMiddleware);
76
+ this.use(responseTime())
77
+ this.use(express.static('public'))
78
+ this.use(this.loggingMiddleware);
79
+ this.use('/static', express.static(this.staticFilesDirectory));
80
+ this.use(this.connectionAbortedMiddleware);
81
+
82
+ this.#expressServer.engine('html', mustacheExpress());
83
+ }
84
+
85
+
86
+ get jsonParsingMiddleware() {
87
+ return express.json({
88
+ limit: '50Mb', // Increase JSON limit
89
+ parameterLimit: 50000, // Increase parameter limit
90
+ type: [
91
+ 'application/json',
92
+ 'text/plain', // AWS sends this content-type for its messages/notifications
93
+ ]
94
+ })
95
+ }
96
+
97
+
98
+ get loggingMiddleware() {
99
+ return httpLogger({
100
+ logger: this.middleWareLogger,
101
+ customLogLevel: (res, err) => {
102
+ if (res.statusCode >= 500) return 'error';
103
+ if (res.statusCode >= 400) return 'warn';
104
+ return 'info';
105
+ },
106
+ serializers: {
107
+ // Format the request log as a string
108
+ req: (req) => `Request: ${req.method} ${req.url}`,
109
+ // Format the response log as a string
110
+ res: (res) => `statusCode=${res.statusCode}, responseTime=${res.get('X-Response-Time')}`,
111
+ },
112
+ wrapSerializers: false,
113
+ })
114
+ }
115
+
116
+
117
+ get middlewareLogger() {
118
+ return pino({
119
+ transport: {
120
+ level: process.env.LOG_LEVEL ? 'debug' : 'info',
121
+ target: 'pino-pretty',
122
+ options: {
123
+ colorize: true,
124
+ ignore: 'pid,hostname',
125
+ translateTime: 'SYS:standard',
126
+ messageFormat: '{msg}',
127
+ }
128
+ },
129
+ });
130
+ }
131
+
132
+
133
+ get staticFilesDirectory() {
134
+ return `${process.cwd()}/static`;
135
+ }
136
+
137
+
138
+ get connectionAbortedMiddleware() {
139
+ return async function (err, req, res, next) {
140
+ if (err && err.code === 'ECONNABORTED') {
141
+ return res.status(400).end(); // Don't process this error any further to avoid its logging
142
+ }
143
+ next();
144
+ };
145
+ }
146
+
147
+
148
+ use() { this.#expressServer.use(...arguments); }
149
+
150
+
151
+ /***********************************************************************************************
152
+ * SETTINGS
153
+ ***********************************************************************************************/
154
+ configureSettings() {
155
+ this.set('view engine', 'mustache');
156
+ this.set('views', this.viewsDirectory);
157
+ this.set('json spaces', 2);
158
+ }
159
+
160
+
161
+ viewsDirectory() {
162
+ return `${process.cwd()}/views`;
163
+ }
164
+
165
+
166
+ get corsOrigins() {
167
+ return [];
168
+ }
169
+
170
+
171
+ set() { this.#expressServer.set(...arguments); }
172
+
173
+
174
+ /***********************************************************************************************
175
+ * ACTION SETUP
176
+ * The server searches through all paths defined in `controllerDirectories` for any subclasses
177
+ * of VidaServerController and registers their actions
178
+ ***********************************************************************************************/
179
+ registerControllers() {
180
+ this.#registerDefaultControllers();
181
+ this.controllerClasses.forEach((controllerCls => {
182
+ this.#registerController(controllerCls);
183
+ }).bind(this));
184
+ }
185
+
186
+
187
+ #registerDefaultControllers() {
188
+ this.#registerController(SystemController);
189
+ }
190
+
191
+
192
+ #registerController(controllerCls) {
193
+ controllerCls.actions.forEach((action => {
194
+ this.#registerAction(action, controllerCls)
195
+ }).bind(this));
196
+ }
197
+
198
+
199
+ #registerAction(action, controllerCls) {
200
+ const method = action.method.toLowerCase();
201
+ const requestHandler = this.requestHandler(action.action, controllerCls)
202
+ this['_'+method](action.path, requestHandler);
203
+ }
204
+
205
+
206
+ _get() { this.#expressServer.get(...arguments); }
207
+ _post() { this.#expressServer.post(...arguments); }
208
+ _put() { this.#expressServer.put(...arguments); }
209
+ _delete() { this.#expressServer.delete(...arguments); }
210
+ _patch() { this.#expressServer.patch(...arguments); }
211
+
212
+
213
+ requestHandler(action, controllerCls) {
214
+ return async function(request, response) {
215
+ if (process.env.NODE_ENV != 'test') this.logger.info(`${controllerCls.name}#${action}`);
216
+ const controllerInstance = this.buildController(controllerCls, request, response);
217
+ await controllerInstance.setupRequestState();
218
+
219
+ if (controllerInstance.rendered) {
220
+ response.end();
221
+ return;
222
+ }
223
+
224
+ controllerInstance.setupCallbacks();
225
+
226
+ const responseBody = await this.performRequest(controllerInstance, action);
227
+
228
+ if (!controllerInstance.rendered) {
229
+ await controllerInstance.render(responseBody || {});
230
+ }
231
+
232
+ response.end();
233
+ }.bind(this);
234
+ }
235
+
236
+
237
+ async performRequest(controllerInstance, action) {
238
+ try {
239
+ return await this._performRequest(controllerInstance, action);
240
+ } catch(err) {
241
+ await this.handleError(err, controllerInstance);
242
+ }
243
+ }
244
+
245
+
246
+ async handleError(err, controllerInstance) {
247
+ if (err.constructor == AuthorizationError) {
248
+ await controllerInstance.renderUnauthorizedResponse();
249
+ return;
250
+
251
+ } else if (err.constructor == ValidationError) {
252
+ await controllerInstance.renderErrors(err.errors);
253
+ return;
254
+
255
+ } else {
256
+ controllerInstance.statusCode = 500;
257
+ await controllerInstance.render({error: err.message});
258
+ }
259
+
260
+ if (process.env.NODE_ENV == 'development') console.log(err);
261
+ }
262
+
263
+
264
+ async _performRequest(controllerInstance, action) {
265
+ if (await controllerInstance.runBeforeCallbacks(action) === false) return false;
266
+ const responseBody = await controllerInstance[action]();
267
+ if (await controllerInstance.runAfterCallbacks(action) === false) return false;
268
+
269
+ return responseBody;
270
+ }
271
+
272
+
273
+ buildController(controllerCls, request, response) {
274
+ return new controllerCls(request, response);
275
+ }
276
+
277
+
278
+ get controllerClasses() {
279
+ if (this.#controllerClasses) return this.#controllerClasses;
280
+ this.#controllerClasses = [];
281
+
282
+ this.controllerDirectories.forEach(dir => {
283
+ this.#importControllers(dir, dir);
284
+ });
285
+
286
+ return this.#controllerClasses;
287
+ }
288
+
289
+
290
+ #importControllers(dir, topLevelDirectory) {
291
+ fs.readdirSync(dir).forEach((f => {
292
+ const filePath = `${dir}/${f}`;
293
+ if (fs.lstatSync(filePath).isDirectory()) {
294
+ this.#importControllers(filePath, topLevelDirectory);
295
+ } else if (f.endsWith('.js')) {
296
+ this.#importControllersFromFile(filePath, dir, topLevelDirectory);
297
+ }
298
+ }).bind(this));
299
+ }
300
+
301
+
302
+ #importControllersFromFile(filePath, dir, topLevelDirectory) {
303
+ const exports = Object.values(require(filePath));
304
+ exports.forEach(_export => {
305
+ if (_export.prototype instanceof VidaServerController) {
306
+ const directoryPrefix = dir.replace(topLevelDirectory, '');
307
+ _export.directoryPrefix = directoryPrefix;
308
+ this.#controllerClasses.push(_export);
309
+ }
310
+ });
311
+ }
312
+
313
+
314
+ get controllerDirectories() {
315
+ return [`${process.cwd()}/controllers`];
316
+ }
317
+
318
+
319
+ get #expressServer() {
320
+ if (!this.#_expressServer) this.#_expressServer = express();
321
+ return this.#_expressServer;
322
+ }
323
+
324
+
325
+ get #httpServer() { // Needed for io socket to work
326
+ if (!this.#_httpServer) this.#_httpServer = Server(this.#expressServer);
327
+ return this.#_httpServer;
328
+ }
329
+
330
+
331
+ /***********************************************************************************************
332
+ * DEPRECATED PASS THROUGH METHODS
333
+ ***********************************************************************************************/
334
+ get() { this.handleDeprecatedRoutingCall('get', arguments); }
335
+ post() { this.handleDeprecatedRoutingCall('post', arguments); }
336
+ put() { this.handleDeprecatedRoutingCall('put', arguments); }
337
+ patch() { this.handleDeprecatedRoutingCall('patch', arguments); }
338
+ delete() { this.handleDeprecatedRoutingCall('delete', arguments); }
339
+
340
+
341
+ handleDeprecatedRoutingCall(methodName, args) {
342
+ this.emitRoutingDeprecationWarning(methodName);
343
+ this.#expressServer[methodName](...args);
344
+ }
345
+
346
+
347
+ emitRoutingDeprecationWarning(methodName) {
348
+ if (!this.routingDeprecationWarnings) this.routingDeprecationWarnings = {};
349
+ if (this.routingDeprecationWarnings[methodName]) return;
350
+ this.routingDeprecationWarnings[methodName] = true;
351
+
352
+ process.emitWarning(`\`${methodName}\` is deprecated for creating routes. Use \`VidaServerController\` instead.`, 'DeprecationWarning');
353
+ }
354
+ }
355
+
356
+
357
+ module.exports = {
358
+ VidaServer
359
+ }
@@ -0,0 +1,344 @@
1
+ const { logger } = require('../logger');
2
+ const { camelize } = require('inflection');
3
+
4
+
5
+ class VidaServerController {
6
+ #afterCallbacks = [];
7
+ #beforeCallbacks = [];
8
+ #params;
9
+ #rendered = false;
10
+ #request;
11
+ #requestBody;
12
+ #requestQuery;
13
+ #response;
14
+
15
+
16
+ constructor(request, response) {
17
+ if (this.constructor == VidaServerController) {
18
+ throw new Error("ServerControllers must be subclasses of VidaServerController");
19
+ }
20
+ this.#request = request;
21
+ this.#response = response;
22
+ }
23
+
24
+
25
+ get _request() { return this.#request };
26
+ get _response() { return this.#response };
27
+
28
+ get params() {
29
+ if (!this.#params) {
30
+ const body = this.#processRequestData('body');
31
+ const query = this.#processRequestData('query');
32
+ this.#params = {...this._request.params, ...body, ...query};
33
+ }
34
+ return structuredClone(this.#params);
35
+ }
36
+
37
+ get requestHeaders() { return structuredClone(this._request.headers || {}); }
38
+ get responseHeaders() { return this._response.headers; };
39
+ get contentType() { return this.requestHeaders['content-type']; }
40
+ get logger() { return logger; }
41
+ get rendered() { return this.#rendered }
42
+
43
+ get statusCode() { return this._response.statusCode; }
44
+ set statusCode(_status) { this._response.statusCode = _status; }
45
+
46
+
47
+ #processRequestData(key) {
48
+ const instanceVarKey = key.charAt(0).toUpperCase() + key.slice(1);
49
+ const instanceVarName = `#request${instanceVarKey}`;
50
+ const data = this._request[key] || {};
51
+ if (this[instanceVarName] === undefined) {
52
+ const processedData = {};
53
+ this[instanceVarName] = processedData;
54
+ for (let [paramName, paramValue] of Object.entries(data)) {
55
+ processedData[paramName] = this.#processedRequestDataValue(paramValue)
56
+ }
57
+ }
58
+
59
+ return structuredClone(this[instanceVarName]);
60
+ }
61
+
62
+
63
+ #processedRequestDataValue(value) {
64
+ try {
65
+ return JSON.parse(value);
66
+ } catch(err) {
67
+ return value;
68
+ }
69
+ }
70
+
71
+
72
+ async render(body, options={}) {
73
+ if (typeof body == 'string') {
74
+ this._response.send(body);
75
+ } else {
76
+ body = await this.formatJSONBody(body, options);
77
+ this._response.json(body);
78
+ }
79
+ this.#rendered = true;
80
+ }
81
+
82
+
83
+ async formatJSONBody(body, options) {
84
+ body = await this._formatJSONBody(body, options);
85
+ return {data: body, status: this.statusText};
86
+ }
87
+
88
+
89
+ async _formatJSONBody(body, options) {
90
+ if (body === undefined) return null;
91
+ if (!body || typeof body != 'object') return body;
92
+
93
+ if (body.toApiResponse) {
94
+ const formattedBody = await body.toApiResponse(options);
95
+ return await this._formatJSONBody(formattedBody, options);
96
+
97
+ } else if (Array.isArray(body)) {
98
+ for (const idx in body) {
99
+ body[idx] = await this._formatJSONBody(body[idx], options);
100
+ }
101
+
102
+ } else if (body.constructor == Date) {
103
+ return body.getTime();
104
+
105
+ } else {
106
+ for (let [key, val] of Object.entries(body)) {
107
+ body[key] = await this._formatJSONBody(val, options);
108
+ }
109
+ }
110
+
111
+ return body;
112
+ }
113
+
114
+
115
+ get statusText() {
116
+ switch(this.statusCode) {
117
+ case 200:
118
+ return 'ok';
119
+ case 400:
120
+ return 'bad request'
121
+ case 401:
122
+ return 'unauthorized';
123
+ case 404:
124
+ return 'not found';
125
+ case 500:
126
+ return 'server error';
127
+ }
128
+ }
129
+
130
+
131
+ async renderErrors(errors) {
132
+ this.statusCode = 400;
133
+ await this.render({ errors });
134
+ }
135
+
136
+
137
+ async renderNotFoundResponse() {
138
+ this.statusCode = 404;
139
+ await this.render();
140
+ }
141
+
142
+
143
+ async renderUnauthorizedResponse() {
144
+ this.statusCode = 401;
145
+ await this.render();
146
+ }
147
+
148
+
149
+ /***********************************************************************************************
150
+ * CALLBACKS
151
+ ***********************************************************************************************/
152
+ async setupRequestState() {}
153
+ setupCallbacks() {}
154
+
155
+
156
+ beforeCallback(callback, options={}) {
157
+ this.#addCallback(callback, options, this.#beforeCallbacks);
158
+ }
159
+
160
+
161
+ afterCallback(callback, options={}) {
162
+ this.#addCallback(callback, options, this.#afterCallbacks);
163
+ }
164
+
165
+
166
+ #addCallback(callback, options, callbacksList) {
167
+ callbacksList.push({ callback, options });
168
+ }
169
+
170
+
171
+ async runBeforeCallbacks(actionName) {
172
+ return await this.#runCallbacks(this.#beforeCallbacks, actionName);
173
+ }
174
+
175
+
176
+ async runAfterCallbacks(actionName) {
177
+ return await this.#runCallbacks(this.#afterCallbacks, actionName);
178
+ }
179
+
180
+
181
+ async #runCallbacks(callbacksList, actionName) {
182
+ for (const { callback, options } of callbacksList) {
183
+ if (!this.#shouldRunCallback(actionName, options)) continue;
184
+ if (await this[callback]() === false) return false;
185
+ }
186
+
187
+ return true;
188
+ }
189
+
190
+
191
+ #shouldRunCallback(actionName, options) {
192
+ if (options.only) {
193
+ if (Array.isArray(options.only)) return options.only.includes(actionName);
194
+ return options.only == actionName;
195
+ }
196
+
197
+ if (options.except) {
198
+ if (Array.isArray(options.except)) return !options.except.includes(actionName);
199
+ return options.except != actionName;
200
+ }
201
+
202
+ return true;
203
+ }
204
+
205
+
206
+ /***********************************************************************************************
207
+ * VALIDATIONS
208
+ ***********************************************************************************************/
209
+ async validateParameters(validations) {
210
+ const errors = {};
211
+ for (const [parameterName, parameterValidations] of Object.entries(validations)) {
212
+ const parameterErrors = this.validateParameter(this.params[parameterName], parameterValidations);
213
+ if (parameterErrors.length) errors[parameterName] = parameterErrors;
214
+ }
215
+
216
+ if (Object.keys(errors).length) throw new ValidationError(errors);
217
+ }
218
+
219
+
220
+ validateParameter(value, parameterValidations) {
221
+ const errors = [];
222
+ for (const [validationType, validationOptions] of Object.entries(parameterValidations)) {
223
+ const validationMethod = `validate${camelize(validationType)}`;
224
+ const error = this[validationMethod](value, validationOptions);
225
+ if (error) errors.push(error);
226
+ }
227
+
228
+ return errors;
229
+ }
230
+
231
+
232
+ validatePresence(value) {
233
+ if (!value) return 'required';
234
+ }
235
+
236
+
237
+ validateIsInteger(value, options) {
238
+ const num = parseFloat(value);
239
+ if (/\D/.test(value) || isNaN(num) || !Number.isInteger(num)) return 'must be an integer';
240
+ if (options && options.gte !== undefined) {
241
+ if (num < options.gte) return `must be greater than or equal to ${options.gte}`;
242
+ }
243
+ }
244
+
245
+
246
+ validateIsDateTime(value) {
247
+ if (new Date(value) == 'Invalid Date') return 'invalid date';
248
+ }
249
+
250
+
251
+ validateIsEnum(value, options) {
252
+ if (!options.enums.includes(value)) return options.error;
253
+ }
254
+
255
+
256
+ validateFunction(value, fnc) {
257
+ const error = fnc(value);
258
+ if (error) return error;
259
+ }
260
+
261
+
262
+ /***********************************************************************************************
263
+ * ACTION SETUP
264
+ * The controller looks for any methods that fit the pattern of `{httpMethod}ActionName`
265
+ * (e.g. getFooBar) and prepares server routes for them. By default, a method `getFooBar` on a
266
+ * `BazController` would have an endpoint of `GET /baz/fooBar`. The server will also prepend
267
+ * directory names, so if `bazController.js` is nested within the `ban` directory
268
+ * (e.g. `ban/bazController.js`), the endpoint will be `GET /ban/baz/fooBar`.
269
+ *
270
+ * Default routing logic can be overriden by defining custom routes in the `routes` getter. To
271
+ * provider a custom route for `getFooBar`, `routes` should return an object like:
272
+ * {getFooBar: '/ban/:accountIdd/foo/:userId/bar'}`
273
+ ***********************************************************************************************/
274
+ static get actions() {
275
+ const actionNames = this.actionNames;
276
+ return actionNames.map((actionName => {
277
+ return this._constructAction(actionName);
278
+ }).bind(this));
279
+ }
280
+
281
+
282
+ static _constructAction(actionName) {
283
+ const routePrefix = this.routePrefix
284
+ const action = {};
285
+ const match = actionName.match(this.actionRegExp);
286
+
287
+ action.method = match.groups.method.toUpperCase();
288
+ action.action = actionName;
289
+ action.path = this._routeForAction(action.method, match.groups.path, routePrefix, actionName);
290
+ return action;
291
+ }
292
+
293
+
294
+ static _routeForAction(method, path, routePrefix, actionName) {
295
+ if (this.routes[actionName]) return this.routes[actionName];
296
+ if (path == 'Index') return routePrefix;
297
+
298
+ path = path.charAt(0).toLowerCase() + path.slice(1);
299
+ return `${routePrefix}/${path}`;
300
+ }
301
+
302
+
303
+ static get routePrefix() {
304
+ const controllerName = this.name.replace(/Controller$/, '');
305
+ const controllerPrefix = controllerName.charAt(0).toLowerCase() + controllerName.slice(1);
306
+
307
+ return `${this.directoryPrefix || ''}/${controllerPrefix}`;
308
+ }
309
+
310
+
311
+ static get routes() {
312
+ return {};
313
+ }
314
+
315
+
316
+ static get actionNames() {
317
+ const methodNames = Object.getOwnPropertyNames(this.prototype);
318
+ const nameRegExp = this.actionRegExp;
319
+ return methodNames.filter(name => nameRegExp.test(name));
320
+ }
321
+
322
+
323
+ static get actionRegExp() {
324
+ return new RegExp(/^(?<method>get|post|put|delete|patch)(?<path>[A-Z][a-zA-Z]+)$/);
325
+ }
326
+ }
327
+
328
+
329
+ class AuthorizationError extends Error {}
330
+ class ValidationError extends Error {
331
+ #errors;
332
+ constructor(errors) {
333
+ super();
334
+ this.#errors = errors;
335
+ }
336
+ get errors() { return structuredClone(this.#errors) };
337
+ }
338
+
339
+
340
+ module.exports = {
341
+ AuthorizationError,
342
+ ValidationError,
343
+ VidaServerController
344
+ };