@stonyx/rest-server 0.2.1-beta.38 → 0.2.1-beta.39
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/dist/main.d.ts +17 -0
- package/dist/main.js +80 -0
- package/dist/request.d.ts +15 -0
- package/dist/request.js +85 -0
- package/package.json +18 -6
- package/src/main.js +0 -88
- package/src/request.js +0 -84
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Express } from 'express';
|
|
2
|
+
import type { Server } from 'http';
|
|
3
|
+
export { default as Request } from './request.js';
|
|
4
|
+
export default class RestServer {
|
|
5
|
+
static instance: RestServer;
|
|
6
|
+
api: Express;
|
|
7
|
+
server: Server;
|
|
8
|
+
constructor();
|
|
9
|
+
static close(): void;
|
|
10
|
+
init(): Promise<void>;
|
|
11
|
+
setupRouter(): Promise<void>;
|
|
12
|
+
setupGlobalMiddleware(): void;
|
|
13
|
+
mountRoute(routeClassUntyped: unknown, { name, options }: {
|
|
14
|
+
name: string;
|
|
15
|
+
options?: unknown;
|
|
16
|
+
}): void;
|
|
17
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Stone Costa
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import cors from 'cors';
|
|
17
|
+
import express from 'express';
|
|
18
|
+
import config from 'stonyx/config';
|
|
19
|
+
import log from 'stonyx/log';
|
|
20
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
21
|
+
export { default as Request } from './request.js';
|
|
22
|
+
export default class RestServer {
|
|
23
|
+
static instance;
|
|
24
|
+
api;
|
|
25
|
+
server;
|
|
26
|
+
constructor() {
|
|
27
|
+
if (RestServer.instance)
|
|
28
|
+
return RestServer.instance;
|
|
29
|
+
RestServer.instance = this;
|
|
30
|
+
this.api = express();
|
|
31
|
+
}
|
|
32
|
+
static close() {
|
|
33
|
+
if (!RestServer.instance)
|
|
34
|
+
throw new Error('RestServer has not been initialized yet');
|
|
35
|
+
const { server } = RestServer.instance;
|
|
36
|
+
server.closeAllConnections();
|
|
37
|
+
server.close();
|
|
38
|
+
}
|
|
39
|
+
async init() {
|
|
40
|
+
await this.setupRouter();
|
|
41
|
+
const { port } = config.restServer;
|
|
42
|
+
// start REST server
|
|
43
|
+
this.server = this.api.listen(port);
|
|
44
|
+
log.title(`API Server is listening on port ${port}`);
|
|
45
|
+
}
|
|
46
|
+
async setupRouter() {
|
|
47
|
+
const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
|
|
48
|
+
this.setupGlobalMiddleware();
|
|
49
|
+
try {
|
|
50
|
+
await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
|
|
51
|
+
if (enableHealthCheck)
|
|
52
|
+
this.api.get('/health', (_req, res) => res.sendStatus(200));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (config.debug)
|
|
56
|
+
console.log(error);
|
|
57
|
+
throw log.error(`Unable to dynamically configure routes from files in ${dir}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
setupGlobalMiddleware() {
|
|
61
|
+
const { origin, methods, trustProxy } = config.restServer;
|
|
62
|
+
if (trustProxy)
|
|
63
|
+
this.api.set('trust proxy', true);
|
|
64
|
+
this.api.use([
|
|
65
|
+
cors({ origin, methods }),
|
|
66
|
+
express.json()
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
mountRoute(routeClassUntyped, { name, options }) {
|
|
70
|
+
const routeClass = routeClassUntyped;
|
|
71
|
+
const { api } = this;
|
|
72
|
+
const classInstance = new routeClass(options);
|
|
73
|
+
const route = name === 'index' ? '/' : `/${name}`;
|
|
74
|
+
const { expressInstance } = classInstance;
|
|
75
|
+
classInstance.registerCalls();
|
|
76
|
+
expressInstance.mountpath = route;
|
|
77
|
+
// Mount handler to main api instance
|
|
78
|
+
api.use(route, expressInstance);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Request as ExpressRequest, type Response as ExpressResponse, type Express } from 'express';
|
|
2
|
+
export type RequestState = Record<string, unknown>;
|
|
3
|
+
export type RequestHandler = (req: ExpressRequest, state: RequestState) => unknown | Promise<unknown>;
|
|
4
|
+
export type AuthHandler = (req: ExpressRequest, state: RequestState) => number | undefined;
|
|
5
|
+
export type RouteHandlers = Record<string, Record<string, RequestHandler | RequestHandler[]>>;
|
|
6
|
+
export default class Request {
|
|
7
|
+
static stateProp: string;
|
|
8
|
+
static getState(req: Record<string, unknown>): RequestState;
|
|
9
|
+
static sendStatusResponse(res: ExpressResponse, status: number): void;
|
|
10
|
+
expressInstance: Express;
|
|
11
|
+
handlers: RouteHandlers;
|
|
12
|
+
auth?: AuthHandler;
|
|
13
|
+
constructor();
|
|
14
|
+
registerCalls(): void;
|
|
15
|
+
}
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import config from 'stonyx/config';
|
|
3
|
+
import { makeArray } from '@stonyx/utils/object';
|
|
4
|
+
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
|
|
5
|
+
export default class Request {
|
|
6
|
+
static stateProp = '__stonyxState';
|
|
7
|
+
static getState(req) {
|
|
8
|
+
const { stateProp } = Request;
|
|
9
|
+
if (req[stateProp] !== undefined)
|
|
10
|
+
return req[stateProp];
|
|
11
|
+
req[stateProp] = {};
|
|
12
|
+
return req[stateProp];
|
|
13
|
+
}
|
|
14
|
+
static sendStatusResponse(res, status) {
|
|
15
|
+
const statusMap = config.restServer?.statusMap || {};
|
|
16
|
+
const message = statusMap[status] || '';
|
|
17
|
+
if (message) {
|
|
18
|
+
res.status(status).send(message);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
res.sendStatus(status);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
expressInstance;
|
|
25
|
+
handlers;
|
|
26
|
+
constructor() {
|
|
27
|
+
const api = express();
|
|
28
|
+
api.disable('x-powered-by');
|
|
29
|
+
this.expressInstance = api;
|
|
30
|
+
}
|
|
31
|
+
registerCalls() {
|
|
32
|
+
const { expressInstance } = this;
|
|
33
|
+
const { getState, sendStatusResponse } = Request;
|
|
34
|
+
for (const [method, handlers] of Object.entries(this.handlers)) {
|
|
35
|
+
if (!METHODS.has(method)) {
|
|
36
|
+
console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const [route, handler] of Object.entries(handlers)) {
|
|
40
|
+
expressInstance[method](route, async (req, res) => {
|
|
41
|
+
// Run auth after route matching so request.params is populated
|
|
42
|
+
if (this.auth) {
|
|
43
|
+
const status = this.auth(req, getState(req));
|
|
44
|
+
if (status)
|
|
45
|
+
return sendStatusResponse(res, status);
|
|
46
|
+
}
|
|
47
|
+
const callStack = [...makeArray(handler)];
|
|
48
|
+
const mainCall = callStack.pop();
|
|
49
|
+
let response;
|
|
50
|
+
// Run middleware
|
|
51
|
+
while (callStack.length) {
|
|
52
|
+
response = await callStack.shift().bind(this)(req, getState(req));
|
|
53
|
+
if (response !== undefined)
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (response === undefined)
|
|
57
|
+
response = await mainCall(req, getState(req));
|
|
58
|
+
if (Number.isInteger(response))
|
|
59
|
+
return sendStatusResponse(res, response);
|
|
60
|
+
// Handle redirect if set via call state object
|
|
61
|
+
const state = getState(req);
|
|
62
|
+
const { redirect } = state;
|
|
63
|
+
if (redirect)
|
|
64
|
+
return res.redirect(redirect);
|
|
65
|
+
// Handle pipe if set via call state object
|
|
66
|
+
const { pipe } = state;
|
|
67
|
+
if (pipe) {
|
|
68
|
+
const { headers, source } = pipe;
|
|
69
|
+
if (headers)
|
|
70
|
+
for (const [key, value] of Object.entries(headers))
|
|
71
|
+
res.set(key, value);
|
|
72
|
+
return source.pipe(res);
|
|
73
|
+
}
|
|
74
|
+
if (response === undefined) {
|
|
75
|
+
res.sendStatus(200);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (typeof response !== 'object')
|
|
79
|
+
return sendStatusResponse(res, 500);
|
|
80
|
+
res.send(response);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
package/package.json
CHANGED
|
@@ -4,16 +4,20 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.2.1-beta.
|
|
7
|
+
"version": "0.2.1-beta.39",
|
|
8
8
|
"description": "Rest Server Module for Stonyx Framework",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "https://github.com/abofs/stonyx-rest-server"
|
|
12
12
|
},
|
|
13
|
-
"main": "
|
|
13
|
+
"main": "dist/main.js",
|
|
14
|
+
"types": "dist/main.d.ts",
|
|
14
15
|
"type": "module",
|
|
15
16
|
"exports": {
|
|
16
|
-
".":
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/main.d.ts",
|
|
19
|
+
"default": "./dist/main.js"
|
|
20
|
+
}
|
|
17
21
|
},
|
|
18
22
|
"author": "Stone Costa",
|
|
19
23
|
"license": "Apache-2.0",
|
|
@@ -21,7 +25,7 @@
|
|
|
21
25
|
"Stone Costa <stone.costa@synamicd.com>"
|
|
22
26
|
],
|
|
23
27
|
"files": [
|
|
24
|
-
"
|
|
28
|
+
"dist",
|
|
25
29
|
"config",
|
|
26
30
|
"README.md"
|
|
27
31
|
],
|
|
@@ -36,10 +40,18 @@
|
|
|
36
40
|
},
|
|
37
41
|
"devDependencies": {
|
|
38
42
|
"@stonyx/utils": "0.2.3-beta.7",
|
|
43
|
+
"@types/cors": "^2.8.17",
|
|
44
|
+
"@types/express": "^5.0.6",
|
|
45
|
+
"@types/node": "^25.5.2",
|
|
46
|
+
"@types/qunit": "^2.19.13",
|
|
47
|
+
"@types/sinon": "^21.0.1",
|
|
39
48
|
"qunit": "^2.24.1",
|
|
40
|
-
"sinon": "^21.0.0"
|
|
49
|
+
"sinon": "^21.0.0",
|
|
50
|
+
"typescript": "^5.8.3"
|
|
41
51
|
},
|
|
42
52
|
"scripts": {
|
|
43
|
-
"
|
|
53
|
+
"build": "tsc",
|
|
54
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
55
|
+
"test": "pnpm build && pnpm build:test && stonyx test 'dist-test/test/**/*-test.js'"
|
|
44
56
|
}
|
|
45
57
|
}
|
package/src/main.js
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright 2025 Stone Costa
|
|
3
|
-
*
|
|
4
|
-
* Licensed under the Apache License, Version 2.0 (the 'License');
|
|
5
|
-
* you may not use this file except in compliance with the License.
|
|
6
|
-
* You may obtain a copy of the License at
|
|
7
|
-
*
|
|
8
|
-
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
*
|
|
10
|
-
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
* See the License for the specific language governing permissions and
|
|
14
|
-
* limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import cors from 'cors';
|
|
18
|
-
import express from 'express';
|
|
19
|
-
import config from 'stonyx/config';
|
|
20
|
-
import log from 'stonyx/log';
|
|
21
|
-
import { forEachFileImport } from '@stonyx/utils/file';
|
|
22
|
-
|
|
23
|
-
export { default as Request } from './request.js';
|
|
24
|
-
|
|
25
|
-
export default class RestServer {
|
|
26
|
-
constructor() {
|
|
27
|
-
if (RestServer.instance) return RestServer.instance;
|
|
28
|
-
RestServer.instance = this;
|
|
29
|
-
|
|
30
|
-
this.api = new express();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static close() {
|
|
34
|
-
if (!RestServer.instance) throw new Error('RestServer has not been initialized yet');
|
|
35
|
-
|
|
36
|
-
const { server } = RestServer.instance;
|
|
37
|
-
server.closeAllConnections();
|
|
38
|
-
server.close();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async init() {
|
|
42
|
-
await this.setupRouter();
|
|
43
|
-
|
|
44
|
-
const { port } = config.restServer;
|
|
45
|
-
|
|
46
|
-
// start REST server
|
|
47
|
-
this.server = this.api.listen(port);
|
|
48
|
-
log.title(`API Server is listening on port ${port}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async setupRouter() {
|
|
52
|
-
const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
|
|
53
|
-
this.setupGlobalMiddleware();
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
|
|
57
|
-
|
|
58
|
-
if (enableHealthCheck) this.api.get('/health', (_req, res) => res.sendStatus(200));
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (config.debug) console.log(error);
|
|
61
|
-
throw log.error(`Unable to dynamically configure routes from files in ${dir}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async setupGlobalMiddleware() {
|
|
66
|
-
const { origin, methods, trustProxy } = config.restServer;
|
|
67
|
-
|
|
68
|
-
if (trustProxy) this.api.set('trust proxy', true);
|
|
69
|
-
|
|
70
|
-
this.api.use([
|
|
71
|
-
cors({ origin, methods }),
|
|
72
|
-
express.json()
|
|
73
|
-
]);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async mountRoute(routeClass, { name, options }) {
|
|
77
|
-
const { api } = this;
|
|
78
|
-
const classInstance = new routeClass(options);
|
|
79
|
-
const route = name === 'index' ? '/' : `/${name}`;
|
|
80
|
-
const { expressInstance } = classInstance;
|
|
81
|
-
|
|
82
|
-
classInstance.registerCalls();
|
|
83
|
-
expressInstance.mountpath = route;
|
|
84
|
-
|
|
85
|
-
// Mount handler to main api instance
|
|
86
|
-
api.use(route, expressInstance);
|
|
87
|
-
}
|
|
88
|
-
}
|
package/src/request.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import config from 'stonyx/config';
|
|
3
|
-
import { makeArray } from '@stonyx/utils/object';
|
|
4
|
-
|
|
5
|
-
const METHODS = new Set(['get', 'post', 'put', 'delete', 'patch']);
|
|
6
|
-
|
|
7
|
-
export default class Request {
|
|
8
|
-
static stateProp = '__stonyxState';
|
|
9
|
-
|
|
10
|
-
static getState(req) {
|
|
11
|
-
const { stateProp } = Request;
|
|
12
|
-
if (req[stateProp] !== undefined) return req[stateProp];
|
|
13
|
-
|
|
14
|
-
req[stateProp] = {};
|
|
15
|
-
return req[stateProp];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
static sendStatusResponse(res, status) {
|
|
19
|
-
const statusMap = config.restServer?.statusMap || {};
|
|
20
|
-
const message = statusMap[status] || '';
|
|
21
|
-
|
|
22
|
-
return message ? res.status(status).send(message) : res.sendStatus(status);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
constructor() {
|
|
26
|
-
const api = express();
|
|
27
|
-
api.disable('x-powered-by');
|
|
28
|
-
|
|
29
|
-
this.expressInstance = api;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
registerCalls() {
|
|
33
|
-
const { expressInstance } = this;
|
|
34
|
-
const { getState, sendStatusResponse } = Request;
|
|
35
|
-
|
|
36
|
-
for (const [method, handlers] of Object.entries(this.handlers)) {
|
|
37
|
-
if (!METHODS.has(method)) {
|
|
38
|
-
console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
for (const [route, handler] of Object.entries(handlers)) {
|
|
43
|
-
expressInstance[method](route, async (req, res) => {
|
|
44
|
-
// Run auth after route matching so request.params is populated
|
|
45
|
-
if (this.auth) {
|
|
46
|
-
const status = this.auth(req, getState(req));
|
|
47
|
-
if (status) return sendStatusResponse(res, status);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const callStack = [...makeArray(handler)];
|
|
51
|
-
const mainCall = callStack.pop();
|
|
52
|
-
let response;
|
|
53
|
-
|
|
54
|
-
// Run middleware
|
|
55
|
-
while(callStack.length) {
|
|
56
|
-
response = await callStack.shift().bind(this)(req, getState(req));
|
|
57
|
-
if (response !== undefined) break;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (response === undefined) response = await mainCall(req, getState(req));
|
|
61
|
-
if (Number.isInteger(response)) return sendStatusResponse(res, response);
|
|
62
|
-
|
|
63
|
-
// Handle redirect if set via call state object
|
|
64
|
-
const { redirect } = getState(req);
|
|
65
|
-
if (redirect) return res.redirect(redirect);
|
|
66
|
-
|
|
67
|
-
// Handle pipe if set via call state object
|
|
68
|
-
const { pipe } = getState(req);
|
|
69
|
-
if (pipe) {
|
|
70
|
-
const { headers, source } = pipe;
|
|
71
|
-
|
|
72
|
-
if (headers) for (const [key, value] of Object.entries(headers)) res.set(key, value);
|
|
73
|
-
return source.pipe(res);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (response === undefined) return res.sendStatus(200);
|
|
77
|
-
if (typeof response !== 'object') return sendStatusResponse(res, 500);
|
|
78
|
-
|
|
79
|
-
res.send(response);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|