@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 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
+ }
@@ -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.38",
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": "src/main.js",
13
+ "main": "dist/main.js",
14
+ "types": "dist/main.d.ts",
14
15
  "type": "module",
15
16
  "exports": {
16
- ".": "./src/main.js"
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
- "src",
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
- "test": "stonyx test"
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
- }