@stonyx/rest-server 0.2.1-beta.1 → 0.2.1-beta.100

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 CHANGED
@@ -1,3 +1,7 @@
1
+ [![CI](https://github.com/abofs/stonyx-rest-server/actions/workflows/ci.yml/badge.svg)](https://github.com/abofs/stonyx-rest-server/actions/workflows/ci.yml)
2
+ [![npm version](https://img.shields.io/npm/v/@stonyx/rest-server.svg)](https://www.npmjs.com/package/@stonyx/rest-server)
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+
1
5
  # @stonyx/rest-server
2
6
 
3
7
  REST server module for the [Stonyx framework](https://github.com/abofs/stonyx), providing dynamic route registration and built-in request handling with optional authentication hooks.
@@ -41,16 +45,13 @@ export default {
41
45
  };
42
46
  ```
43
47
 
44
- Then initialize the Stonyx framework, which auto-initializes all of its modules, including `@stonyx/rest-server`:
45
-
46
- ```js
47
- import Stonyx from 'stonyx';
48
- import config from './config/environment.js';
48
+ Then run the application via the Stonyx CLI, which auto-initializes all modules including the REST server:
49
49
 
50
- new Stonyx(config);
50
+ ```bash
51
+ stonyx serve
51
52
  ```
52
53
 
53
- For further framework initialization instructions, see the [Stonyx repository](https://github.com/abofs/stonyx).
54
+ For further framework instructions, see the [Stonyx repository](https://github.com/abofs/stonyx).
54
55
 
55
56
  ### Optional Direct Usage
56
57
 
@@ -72,12 +73,26 @@ Configuration is read from `stonyx/config` under `restServer`:
72
73
 
73
74
  | Option | Type | Default | Description |
74
75
  | :---------------: | :-----------------: | :---------- | :--------------------------------------------------------- |
75
- | `dir` | **String** | `undefined` | Directory containing request classes to mount as routes |
76
+ | `dir` | **String** | `'./requests'` | Directory containing request classes to mount as routes |
76
77
  | `camelCaseRoutes` | **Boolean** | `true` | Convert filenames to camelCase when generating route paths |
77
- | `port` | **Number** | `3000` | Port to listen on |
78
+ | `port` | **Number** | `2666` | Port to listen on |
78
79
  | `origin` | **String \| Array** | `'*'` | CORS origin(s) allowed |
80
+ | `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | CORS allowed methods |
81
+ | `enableHealthCheck` | **Boolean** | `true` | Register `GET /health` endpoint (disable via `REST_HEALTH_CHECK_DISABLE=true`) |
82
+ | `trustProxy` | **Boolean** | `false` | Trust reverse proxy headers (e.g. `X-Forwarded-Proto`). Enable via `REST_TRUST_PROXY=true` when running behind a load balancer such as AWS ALB/ELB to ensure correct protocol detection. |
79
83
  | `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
80
- | `debug` | **Boolean** | `false` | Enable debug logging during route setup |
84
+
85
+ ### Running Behind a Load Balancer
86
+
87
+ When your application runs behind a reverse proxy or load balancer (e.g. AWS ALB/ELB), the load balancer terminates SSL and forwards requests to your server over HTTP internally. This means Express sees `http` as the protocol even though the original client request used `https`.
88
+
89
+ To fix this, enable the `trustProxy` option:
90
+
91
+ ```bash
92
+ REST_TRUST_PROXY=true
93
+ ```
94
+
95
+ This tells Express to trust the `X-Forwarded-Proto` header set by the load balancer, so `request.protocol` correctly returns `https`. This is important for any functionality that generates URLs based on the incoming request protocol, such as JSON:API relationship links.
81
96
 
82
97
  ## Request Class
83
98
 
@@ -135,10 +150,6 @@ project-root/
135
150
 
136
151
  The `RestServer` will automatically mount these routes using the filenames as paths (`/public` and `/private` by default, or camelCased if configured).
137
152
 
138
- Perfect! Here’s a self-contained **“Example Requests”** section with sample `curl` calls:
139
-
140
- ---
141
-
142
153
  ### Example Requests
143
154
 
144
155
  Assuming you have `public.js` and `private.js` routes mounted, you can test them like this:
@@ -3,15 +3,19 @@ const {
3
3
  REST_CORS_METHODS,
4
4
  REST_HEALTH_CHECK_DISABLE,
5
5
  REST_PORT,
6
- REST_REQUEST_PATH
7
- } = process;
6
+ REST_REQUEST_PATH,
7
+ REST_TRUST_PROXY
8
+ } = process.env;
8
9
 
9
- export default {
10
+ const config = {
10
11
  enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
11
12
  origin: REST_CORS_ORIGIN ?? '*',
12
13
  methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
13
14
  dir: REST_REQUEST_PATH ?? './requests',
14
15
  port: REST_PORT ?? 2666,
16
+ trustProxy: REST_TRUST_PROXY === 'true',
15
17
  logColor: 'yellow',
16
18
  logMethod: 'api'
17
19
  };
20
+
21
+ export default config;
package/dist/main.d.ts ADDED
@@ -0,0 +1,18 @@
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
+ }
18
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAiBA,OAAgB,EAAE,KAAK,OAAO,EAAoE,MAAM,SAAS,CAAC;AAIlH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,cAAc,CAAC;AAElD,MAAM,CAAC,OAAO,OAAO,UAAU;IAC7B,MAAM,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE5B,GAAG,EAAG,OAAO,CAAC;IACd,MAAM,EAAG,MAAM,CAAC;;IAShB,MAAM,CAAC,KAAK,IAAI,IAAI;IAQd,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAelC,qBAAqB,IAAI,IAAI;IAW7B,UAAU,CAAC,iBAAiB,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI;CAarG"}
package/dist/main.js ADDED
@@ -0,0 +1,86 @@
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
+ // Self-register so log.api works even when @stonyx/rest-server is in the
41
+ // consumer's `dependencies` (stonyx loader only merges devDependencies).
42
+ const { logColor = 'yellow', logMethod = 'api' } = config.restServer;
43
+ log.defineType(logMethod, logColor);
44
+ await this.setupRouter();
45
+ const { port } = config.restServer;
46
+ // start REST server
47
+ this.server = this.api.listen(port);
48
+ log.title(`API Server is listening on port ${port}`);
49
+ }
50
+ async setupRouter() {
51
+ const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
52
+ this.setupGlobalMiddleware();
53
+ try {
54
+ await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
55
+ if (enableHealthCheck)
56
+ this.api.get('/health', (_req, res) => res.sendStatus(200));
57
+ }
58
+ catch (error) {
59
+ if (config.debug)
60
+ console.log(error);
61
+ log.error(`Unable to dynamically configure routes from files in ${dir}`);
62
+ throw new Error(`Unable to dynamically configure routes from files in ${dir}`);
63
+ }
64
+ }
65
+ setupGlobalMiddleware() {
66
+ const { origin, methods, trustProxy } = config.restServer;
67
+ if (trustProxy)
68
+ this.api.set('trust proxy', true);
69
+ this.api.use([
70
+ cors({ origin, methods }),
71
+ express.json()
72
+ ]);
73
+ }
74
+ mountRoute(routeClassUntyped, { name, options }) {
75
+ const routeClass = routeClassUntyped;
76
+ const { api } = this;
77
+ const classInstance = new routeClass(options);
78
+ const route = name === 'index' ? '/' : `/${name}`;
79
+ const { expressInstance } = classInstance;
80
+ classInstance.registerCalls();
81
+ expressInstance.mountpath = route;
82
+ // Mount handler to main api instance
83
+ api.use(route, expressInstance);
84
+ }
85
+ }
86
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
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: ExpressRequest): RequestState;
9
+ static sendStatusResponse(res: ExpressResponse, status: number): void;
10
+ expressInstance: Express;
11
+ handlers: RouteHandlers;
12
+ auth?: AuthHandler;
13
+ constructor();
14
+ registerCalls(): void;
15
+ }
16
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.d.ts","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAE,KAAK,OAAO,IAAI,cAAc,EAAE,KAAK,QAAQ,IAAI,eAAe,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAMlH,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACnD,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AACtG,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,MAAM,GAAG,SAAS,CAAC;AAC3F,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,cAAc,EAAE,CAAC,CAAC,CAAC;AAE9F,MAAM,CAAC,OAAO,OAAO,OAAO;IAC1B,MAAM,CAAC,SAAS,SAAmB;IAEnC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY;IASlD,MAAM,CAAC,kBAAkB,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAWrE,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAG,aAAa,CAAC;IACjB,IAAI,CAAC,EAAE,WAAW,CAAC;;IAS3B,aAAa,IAAI,IAAI;CAqDtB"}
@@ -0,0 +1,87 @@
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
+ const record = req;
10
+ if (record[stateProp] !== undefined)
11
+ return record[stateProp];
12
+ record[stateProp] = {};
13
+ return record[stateProp];
14
+ }
15
+ static sendStatusResponse(res, status) {
16
+ const statusMap = config.restServer?.statusMap ?? {};
17
+ const message = statusMap[status] || '';
18
+ if (message) {
19
+ res.status(status).send(message);
20
+ }
21
+ else {
22
+ res.sendStatus(status);
23
+ }
24
+ }
25
+ expressInstance;
26
+ handlers;
27
+ constructor() {
28
+ const api = express();
29
+ api.disable('x-powered-by');
30
+ this.expressInstance = api;
31
+ }
32
+ registerCalls() {
33
+ const { expressInstance } = this;
34
+ const { getState, sendStatusResponse } = Request;
35
+ for (const [method, handlers] of Object.entries(this.handlers)) {
36
+ if (!METHODS.has(method)) {
37
+ console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
38
+ continue;
39
+ }
40
+ for (const [route, handler] of Object.entries(handlers)) {
41
+ expressInstance[method](route, async (req, res) => {
42
+ // Run auth after route matching so request.params is populated
43
+ if (this.auth) {
44
+ const status = this.auth(req, getState(req));
45
+ if (status)
46
+ return sendStatusResponse(res, status);
47
+ }
48
+ const callStack = [...makeArray(handler)];
49
+ const mainCall = callStack.pop();
50
+ let response;
51
+ // Run middleware
52
+ while (callStack.length) {
53
+ response = await callStack.shift().bind(this)(req, getState(req));
54
+ if (response !== undefined)
55
+ break;
56
+ }
57
+ if (response === undefined)
58
+ response = await mainCall(req, getState(req));
59
+ if (Number.isInteger(response))
60
+ return sendStatusResponse(res, response);
61
+ // Handle redirect if set via call state object
62
+ const state = getState(req);
63
+ const { redirect } = state;
64
+ if (redirect)
65
+ return res.redirect(redirect);
66
+ // Handle pipe if set via call state object
67
+ const { pipe } = state;
68
+ if (pipe) {
69
+ const { headers, source } = pipe;
70
+ if (headers)
71
+ for (const [key, value] of Object.entries(headers))
72
+ res.set(key, value);
73
+ return source.pipe(res);
74
+ }
75
+ if (response === undefined) {
76
+ res.sendStatus(200);
77
+ return;
78
+ }
79
+ if (typeof response !== 'object')
80
+ return sendStatusResponse(res, 500);
81
+ res.send(response);
82
+ });
83
+ }
84
+ }
85
+ }
86
+ }
87
+ //# sourceMappingURL=request.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request.js","sourceRoot":"","sources":["../src/request.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -4,22 +4,31 @@
4
4
  "stonyx-async",
5
5
  "stonyx-module"
6
6
  ],
7
- "version": "0.2.1-beta.1",
7
+ "version": "0.2.1-beta.100",
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",
20
24
  "contributors": [
21
25
  "Stone Costa <stone.costa@synamicd.com>"
22
26
  ],
27
+ "files": [
28
+ "dist",
29
+ "config",
30
+ "README.md"
31
+ ],
23
32
  "publishConfig": {
24
33
  "access": "public",
25
34
  "provenance": true
@@ -27,14 +36,23 @@
27
36
  "dependencies": {
28
37
  "cors": "^2.8.5",
29
38
  "express": "^5.1.0",
30
- "stonyx": "^0.2.2"
39
+ "stonyx": "0.2.3-beta.81"
31
40
  },
32
41
  "devDependencies": {
33
- "@stonyx/utils": "^0.2.2",
42
+ "@stonyx/utils": "0.2.3-beta.26",
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",
34
48
  "qunit": "^2.24.1",
35
- "sinon": "^21.0.0"
49
+ "sinon": "^21.0.0",
50
+ "tsx": "^4.21.0",
51
+ "typescript": "^5.8.3"
36
52
  },
37
53
  "scripts": {
38
- "test": "qunit --require ./stonyx-bootstrap.cjs"
54
+ "build": "tsc",
55
+ "typecheck": "tsc -p tsconfig.test.json",
56
+ "test": "pnpm build && pnpm typecheck && NODE_ENV=test node --import tsx/esm --import ./test/setup.ts node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'"
39
57
  }
40
58
  }
@@ -1,16 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- pull_request:
5
- branches: [dev, main]
6
-
7
- concurrency:
8
- group: ci-${{ github.head_ref || github.ref }}
9
- cancel-in-progress: true
10
-
11
- permissions:
12
- contents: read
13
-
14
- jobs:
15
- test:
16
- uses: abofs/stonyx-workflows/.github/workflows/ci.yml@main
@@ -1,35 +0,0 @@
1
- name: Publish to NPM
2
-
3
- on:
4
- workflow_dispatch:
5
- inputs:
6
- version-type:
7
- description: 'Version type'
8
- required: true
9
- type: choice
10
- options:
11
- - patch
12
- - minor
13
- - major
14
- custom-version:
15
- description: 'Custom version (optional, overrides version-type)'
16
- required: false
17
- type: string
18
- pull_request:
19
- types: [opened, synchronize, reopened]
20
- branches: [main, dev]
21
- push:
22
- branches: [main]
23
-
24
- permissions:
25
- contents: write
26
- id-token: write
27
- pull-requests: write
28
-
29
- jobs:
30
- publish:
31
- uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
32
- with:
33
- version-type: ${{ github.event.inputs.version-type }}
34
- custom-version: ${{ github.event.inputs.custom-version }}
35
- secrets: inherit
package/src/main.js DELETED
@@ -1,89 +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
- RestServer.instance.server.close();
37
- }
38
-
39
- async init() {
40
- await this.setupRouter();
41
-
42
- const { port } = config.restServer;
43
-
44
- // start REST server
45
- this.server = this.api.listen(port);
46
- log.title(`API Server is listening on port ${port}`);
47
- }
48
-
49
- async setupRouter() {
50
- const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
51
- this.setupGlobalMiddleware();
52
-
53
- try {
54
- await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
55
-
56
- if (enableHealthCheck) this.api.get('/health', (_req, res) => res.sendStatus(200));
57
- } catch (error) {
58
- if (config.debug) console.log(error);
59
- throw log.error(`Unable to dynamically configure routes from files in ${dir}`);
60
- }
61
- }
62
-
63
- async setupGlobalMiddleware() {
64
- const { origin, methods } = config.restServer;
65
-
66
- this.api.use([
67
- cors({ origin, methods }),
68
- express.json()
69
- ]);
70
- }
71
-
72
- async mountRoute(routeClass, { name, options }) {
73
- const { api } = this;
74
- const classInstance = new routeClass(options);
75
- const route = name === 'index' ? '/' : `/${name}`;
76
- const { expressInstance, authorization } = classInstance;
77
-
78
- const routeCalls = [ expressInstance ];
79
-
80
- // Assign auth callback if it exists
81
- if (authorization) routeCalls.unshift(authorization.bind(classInstance));
82
-
83
- classInstance.registerCalls();
84
- expressInstance.mountpath = route;
85
-
86
- // Mount handler to main api instance
87
- api.use(route, ...routeCalls);
88
- }
89
- }
package/src/request.js DELETED
@@ -1,85 +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
- // auth hook wrapper
33
- authorization(req, res, next) {
34
- if (!this.auth) return next();
35
-
36
- const status = this.auth(req, Request.getState(req));
37
- if (status) return Request.sendStatusResponse(res, status);
38
-
39
- next();
40
- }
41
-
42
- registerCalls() {
43
- const { expressInstance } = this;
44
- const { getState } = Request;
45
-
46
- for (const [method, handlers] of Object.entries(this.handlers)) {
47
- if (!METHODS.has(method)) {
48
- console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
49
- continue;
50
- }
51
-
52
- for (const [route, handler] of Object.entries(handlers)) {
53
- expressInstance[method](route, async (req, res) => {
54
- const callStack = [...makeArray(handler)];
55
- const mainCall = callStack.pop();
56
- const { sendStatusResponse } = Request;
57
- let response;
58
-
59
- // Run middleware
60
- while(callStack.length) {
61
- response = await callStack.shift().bind(this)(req, getState(req));
62
- if (response !== undefined) break;
63
- }
64
-
65
- if (response === undefined) response = await mainCall(req, getState(req));
66
- if (Number.isInteger(response)) return sendStatusResponse(res, response);
67
-
68
- // Handle pipe if set via call state object
69
- const { pipe } = getState(req);
70
- if (pipe) {
71
- const { headers, source } = pipe;
72
-
73
- if (headers) for (const [key, value] of Object.entries(headers)) res.set(key, value);
74
- return source.pipe(res);
75
- }
76
-
77
- if (response === undefined) return res.sendStatus(200);
78
- if (typeof response !== 'object') return sendStatusResponse(res, 500);
79
-
80
- res.send(response);
81
- });
82
- }
83
- }
84
- }
85
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * commonJS Bootstrap loading - Stonyx must be loaded first, prior to the rest of the application
3
- */
4
- const { default:Stonyx } = require('stonyx');
5
- const { default:config } = require('./config/environment.js');
6
-
7
- // Override dir for tests
8
- config.dir = './test/sample/requests';
9
-
10
- new Stonyx(config, __dirname);
11
-
12
- module.exports = Stonyx;