@stonyx/rest-server 0.2.1-beta.13 → 0.2.1-beta.130
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 +95 -2
- package/config/environment.js +11 -2
- package/dist/main.d.ts +18 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +99 -0
- package/dist/main.js.map +1 -0
- package/dist/request.d.ts +16 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +102 -0
- package/dist/request.js.map +1 -0
- package/package.json +25 -7
- package/.claude/improvements.md +0 -44
- package/.claude/project-structure.md +0 -150
- package/.github/workflows/ci.yml +0 -16
- package/.github/workflows/publish.yml +0 -51
- package/src/main.js +0 -86
- package/src/request.js +0 -84
package/README.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
[](https://github.com/abofs/stonyx-rest-server/actions/workflows/ci.yml)
|
|
2
|
+
[](https://www.npmjs.com/package/@stonyx/rest-server)
|
|
3
|
+
[](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.
|
|
@@ -70,14 +74,103 @@ Configuration is read from `stonyx/config` under `restServer`:
|
|
|
70
74
|
| Option | Type | Default | Description |
|
|
71
75
|
| :---------------: | :-----------------: | :---------- | :--------------------------------------------------------- |
|
|
72
76
|
| `dir` | **String** | `'./requests'` | Directory containing request classes to mount as routes |
|
|
73
|
-
| `camelCaseRoutes` | **Boolean** | `
|
|
77
|
+
| `camelCaseRoutes` | **Boolean** | *(unset — `config/environment.js` sets no default)* | When explicitly `true`, converts hyphenated filenames to camelCase when generating route paths. Unset, so filenames are used **verbatim** |
|
|
74
78
|
| `port` | **Number** | `2666` | Port to listen on |
|
|
75
79
|
| `origin` | **String \| Array** | `'*'` | CORS origin(s) allowed |
|
|
76
80
|
| `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | CORS allowed methods |
|
|
77
81
|
| `enableHealthCheck` | **Boolean** | `true` | Register `GET /health` endpoint (disable via `REST_HEALTH_CHECK_DISABLE=true`) |
|
|
82
|
+
| `caseSensitiveRoutes` | **Boolean** | `true` | Match route paths case-sensitively. Opt out with `restServer.caseSensitiveRoutes: false` (or `REST_CASE_SENSITIVE_ROUTES=false`, devDependencies installs only) — read [Breaking changes](#breaking-changes) before you do |
|
|
78
83
|
| `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
84
|
| `statusMap` | **Object** | `{}` | Optional mapping of HTTP status codes to custom messages |
|
|
80
85
|
|
|
86
|
+
### Case-Sensitive Route Matching
|
|
87
|
+
|
|
88
|
+
Routes match **case-sensitively**. `GET /Users` does not reach a route mounted at `/users`; it returns 404.
|
|
89
|
+
|
|
90
|
+
This is deliberate and is a security property, not a style choice. Express matches case-insensitively by default, but `request.path`, `request.baseUrl` and `request.originalUrl` all preserve the caller's casing — so an `auth` hook written against the path is case-sensitive while the router that dispatched to it is not. A caller who changes the case of a URL then reaches a handler the canonical URL is denied.
|
|
91
|
+
|
|
92
|
+
Measured against this repo's own sample requests, **before this change**:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
GET /public/SUCCESS -> 200 the /success handler runs
|
|
96
|
+
GET /PRIVATE/failure -> 505 the auth hook fires on a path it was never written for
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**After this change:**
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
GET /public/SUCCESS -> 404
|
|
103
|
+
GET /PRIVATE/failure -> 404
|
|
104
|
+
GET /public/success -> 200 canonical paths are untouched
|
|
105
|
+
GET /private/failure -> 505 canonical paths are untouched
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
A router that matches more loosely than every downstream matcher is a fail-open by construction, so the default is the strict one.
|
|
109
|
+
|
|
110
|
+
> **Why not `GET /private/FAILURE` as the probe?** It returns `200` both before and after, because the sample `private.ts` also registers `/:id`, which absorbs the miss. What changes is *which handler ran* — before, the case-varied URL reached the `/failure` handler that `auth` denies with `505`; after, it can only reach the `/:id` handler. Status alone is not a reliable signal that the fix landed; use `GET /public/SUCCESS` for that.
|
|
111
|
+
|
|
112
|
+
### Breaking changes
|
|
113
|
+
|
|
114
|
+
Case-sensitive matching is a **behaviour change**. Requests that previously reached a route now return 404.
|
|
115
|
+
|
|
116
|
+
**Who is affected:**
|
|
117
|
+
|
|
118
|
+
* Any client sending a URL whose case does not exactly match the mounted path — hand-written links, bookmarked or cached URLs, third-party callers, anything that upper-cases path segments.
|
|
119
|
+
* Any app with a capitalised or hyphenated request filename. Mount paths come from filenames, and `camelCaseRoutes` never lower-cases anything: it only upper-cases the letter following a `-`. So `Users.ts` mounts at `/Users` under **both** `camelCaseRoutes` settings, including the default. `phone-number.ts` mounts at `/phone-number` by default — `config/environment.js` declares no `camelCaseRoutes` key, so filenames are used verbatim — and at `/phoneNumber` only if you have explicitly set `camelCaseRoutes: true`. Clients that hardcode `/users` or `/phonenumber` start 404ing.
|
|
120
|
+
* Anything matching the URL downstream of the router — reverse proxies, WAF path rules, analytics path grouping, `originalUrl`-based routing.
|
|
121
|
+
* `GET /HEALTH` no longer answers. Only `GET /health` does.
|
|
122
|
+
|
|
123
|
+
**The symptom.** Express's default 404, with **no log line and no stack** — `RestServer` registers no 404 handler, so nothing is emitted server-side:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
HTTP/1.1 404 Not Found
|
|
127
|
+
Content-Type: text/html; charset=utf-8
|
|
128
|
+
|
|
129
|
+
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>Error</title></head>
|
|
130
|
+
<body><pre>Cannot GET /public/SUCCESS</pre></body></html>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If routes appear to have vanished after upgrading, `Cannot GET` in the response body is the only string to grep for. Silence in the logs is expected here and is not evidence of a dropped route or a bad build.
|
|
134
|
+
|
|
135
|
+
**Before you upgrade,** list your actual mount paths and compare them against the URLs your clients send:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
ls requests/ # every filename becomes a mount path
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Any filename that is not already all-lowercase produces a mixed-case mount that now requires exact casing from callers. Hyphenated filenames are **not** affected by default, because `camelCaseRoutes` is unset and filenames are used verbatim; they are only at risk if you have explicitly set `camelCaseRoutes: true`, which mounts `phone-number.ts` at `/phoneNumber`.
|
|
142
|
+
|
|
143
|
+
**Opting out (temporary).** Two forms, and they are **not** interchangeable:
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
// config/environment.js in YOUR app — works for every install shape
|
|
147
|
+
export default {
|
|
148
|
+
restServer: { caseSensitiveRoutes: false }
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# environment variable — only effective when @stonyx/rest-server is in your devDependencies
|
|
154
|
+
REST_CASE_SENSITIVE_ROUTES=false
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The environment variable is read by this module's own `config/environment.js`, and the Stonyx module loader merges that file only for `@stonyx/*` packages listed in your **`devDependencies`**. If you install `@stonyx/rest-server` into `dependencies`, the file is never loaded and the variable is inert. The config-object form wins in both install shapes, so prefer it.
|
|
158
|
+
|
|
159
|
+
Either form restores Express's default case-insensitive matching. It also re-opens the **case-variant fail-open** described above for any authorization that matches on a URL, so treat it as a temporary measure while you fix client casing, not as a setting to leave on. It has no effect on the other residuals below, which are open either way.
|
|
160
|
+
|
|
161
|
+
**Scope.** This makes *route matching* exact on the **case axis only**. It closes [#47](https://github.com/abofs/stonyx-rest-server/issues/47) and nothing else. The following are known-open members of the same loose-matching family. None is closed by this setting, and this list is the residual risk that is currently *tracked* — not a statement that URL matching is otherwise exact.
|
|
162
|
+
|
|
163
|
+
* **Path parameter values — [#69](https://github.com/abofs/stonyx-rest-server/issues/69).** Route matching is exact, but the router will deliver a `:param` value in any casing, and a hook comparing `request.params.id` is doing its own case-sensitive comparison against it. This is a **bypass**, not a normalisation nicety. Measured against this repo's sample `private.ts`, whose hook is `if (request.params?.id === 'restricted') return 403`:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
GET /private/restricted -> 403 Forbidden
|
|
167
|
+
GET /private/RESTRICTED -> 200 {"data":"param-route"} guarded handler runs
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
* **Trailing slashes — [#50](https://github.com/abofs/stonyx-rest-server/issues/50).** Express's `strict routing` is a separate setting and is still off, so `GET /private/failure/` still matches `/failure` and still bypasses a path-matching `auth` hook.
|
|
171
|
+
* **Mount-root trailing slash and absolute-form request targets — [#54](https://github.com/abofs/stonyx-rest-server/issues/54).** Both are seen by an `originalUrl`-matching hook as a different string from the canonical URL.
|
|
172
|
+
* **Percent-encoding — [#56](https://github.com/abofs/stonyx-rest-server/issues/56).** `GET /enc/%73ecret` reaches the handler that `GET /enc/secret` is denied, defeating hooks written against `req.path` and against `originalUrl` alike.
|
|
173
|
+
|
|
81
174
|
### Running Behind a Load Balancer
|
|
82
175
|
|
|
83
176
|
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`.
|
|
@@ -144,7 +237,7 @@ project-root/
|
|
|
144
237
|
* `public.js` — contains public-facing routes without authentication
|
|
145
238
|
* `private.js` — contains routes with authentication via the `auth` hook
|
|
146
239
|
|
|
147
|
-
The `RestServer` will automatically mount these routes using the filenames as paths (`/public` and `/private` by default, or camelCased if
|
|
240
|
+
The `RestServer` will automatically mount these routes using the filenames as paths (`/public` and `/private` by default, or camelCased if `camelCaseRoutes` is enabled).
|
|
148
241
|
|
|
149
242
|
### Example Requests
|
|
150
243
|
|
package/config/environment.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
const {
|
|
2
|
+
REST_CASE_SENSITIVE_ROUTES,
|
|
2
3
|
REST_CORS_ORIGIN,
|
|
3
4
|
REST_CORS_METHODS,
|
|
4
5
|
REST_HEALTH_CHECK_DISABLE,
|
|
5
6
|
REST_PORT,
|
|
6
7
|
REST_REQUEST_PATH,
|
|
7
8
|
REST_TRUST_PROXY
|
|
8
|
-
} = process;
|
|
9
|
+
} = process.env;
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
const config = {
|
|
12
|
+
// Secure by default. Opt out with REST_CASE_SENSITIVE_ROUTES=false, which
|
|
13
|
+
// restores express's default case-INSENSITIVE matching -- and, with it, the
|
|
14
|
+
// fail-open that abofs/stonyx-rest-server#47 closes: any authorization hook
|
|
15
|
+
// that matches on the request path is case-sensitive, so a case-varied URL
|
|
16
|
+
// reaches a handler the canonical URL is denied.
|
|
17
|
+
caseSensitiveRoutes: REST_CASE_SENSITIVE_ROUTES !== 'false',
|
|
11
18
|
enableHealthCheck: REST_HEALTH_CHECK_DISABLE !== 'true',
|
|
12
19
|
origin: REST_CORS_ORIGIN ?? '*',
|
|
13
20
|
methods: REST_CORS_METHODS ?? 'GET,POST,PATCH,PUT,DELETE',
|
|
@@ -17,3 +24,5 @@ export default {
|
|
|
17
24
|
logColor: 'yellow',
|
|
18
25
|
logMethod: 'api'
|
|
19
26
|
};
|
|
27
|
+
|
|
28
|
+
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;;IAwBhB,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,99 @@
|
|
|
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
|
+
// Only an explicit `false` opts out. A destructuring default fires on
|
|
31
|
+
// `undefined` alone, so a config carrying null/0/'' would skip it and set
|
|
32
|
+
// the flag falsy -- a silent fail-open into the exact hole this closes
|
|
33
|
+
// (measured: null, 0 and '' all restored case-INSENSITIVE matching).
|
|
34
|
+
// This mirrors config/environment.js's `REST_CASE_SENSITIVE_ROUTES !== 'false'`.
|
|
35
|
+
const caseSensitiveRoutes = config.restServer?.caseSensitiveRoutes !== false;
|
|
36
|
+
this.api = express();
|
|
37
|
+
// Mount case-sensitively (#47). Express defaults to case-INSENSITIVE
|
|
38
|
+
// matching, which dispatches on a looser match than any downstream
|
|
39
|
+
// authorization predicate uses -- a fail-open by construction. Set here in
|
|
40
|
+
// the constructor, before setupRouter() registers anything: express
|
|
41
|
+
// materialises the router lazily on first registration and reads this
|
|
42
|
+
// setting at that moment, so a later set is silently ineffective.
|
|
43
|
+
this.api.set('case sensitive routing', caseSensitiveRoutes);
|
|
44
|
+
}
|
|
45
|
+
static close() {
|
|
46
|
+
if (!RestServer.instance)
|
|
47
|
+
throw new Error('RestServer has not been initialized yet');
|
|
48
|
+
const { server } = RestServer.instance;
|
|
49
|
+
server.closeAllConnections();
|
|
50
|
+
server.close();
|
|
51
|
+
}
|
|
52
|
+
async init() {
|
|
53
|
+
// Self-register so log.api works even when @stonyx/rest-server is in the
|
|
54
|
+
// consumer's `dependencies` (stonyx loader only merges devDependencies).
|
|
55
|
+
const { logColor = 'yellow', logMethod = 'api' } = config.restServer;
|
|
56
|
+
log.defineType(logMethod, logColor);
|
|
57
|
+
await this.setupRouter();
|
|
58
|
+
const { port } = config.restServer;
|
|
59
|
+
// start REST server
|
|
60
|
+
this.server = this.api.listen(port);
|
|
61
|
+
log.title(`API Server is listening on port ${port}`);
|
|
62
|
+
}
|
|
63
|
+
async setupRouter() {
|
|
64
|
+
const { camelCaseRoutes, dir, enableHealthCheck } = config.restServer;
|
|
65
|
+
this.setupGlobalMiddleware();
|
|
66
|
+
try {
|
|
67
|
+
await forEachFileImport(dir, this.mountRoute.bind(this), { rawName: !camelCaseRoutes, ignoreAccessFailure: true });
|
|
68
|
+
if (enableHealthCheck)
|
|
69
|
+
this.api.get('/health', (_req, res) => res.sendStatus(200));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (config.debug)
|
|
73
|
+
console.log(error);
|
|
74
|
+
log.error(`Unable to dynamically configure routes from files in ${dir}`);
|
|
75
|
+
throw new Error(`Unable to dynamically configure routes from files in ${dir}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
setupGlobalMiddleware() {
|
|
79
|
+
const { origin, methods, trustProxy } = config.restServer;
|
|
80
|
+
if (trustProxy)
|
|
81
|
+
this.api.set('trust proxy', true);
|
|
82
|
+
this.api.use([
|
|
83
|
+
cors({ origin, methods }),
|
|
84
|
+
express.json()
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
mountRoute(routeClassUntyped, { name, options }) {
|
|
88
|
+
const routeClass = routeClassUntyped;
|
|
89
|
+
const { api } = this;
|
|
90
|
+
const classInstance = new routeClass(options);
|
|
91
|
+
const route = name === 'index' ? '/' : `/${name}`;
|
|
92
|
+
const { expressInstance } = classInstance;
|
|
93
|
+
classInstance.registerCalls();
|
|
94
|
+
expressInstance.mountpath = route;
|
|
95
|
+
// Mount handler to main api instance
|
|
96
|
+
api.use(route, expressInstance);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=main.js.map
|
package/dist/main.js.map
ADDED
|
@@ -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;;IAyB3B,aAAa,IAAI,IAAI;CAqDtB"}
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
// Only an explicit `false` opts out. A destructuring default fires on
|
|
29
|
+
// `undefined` alone, so a config carrying null/0/'' would skip it and set
|
|
30
|
+
// the flag falsy -- a silent fail-open into the exact hole this closes
|
|
31
|
+
// (measured: null, 0 and '' all restored case-INSENSITIVE matching).
|
|
32
|
+
// This mirrors config/environment.js's `REST_CASE_SENSITIVE_ROUTES !== 'false'`.
|
|
33
|
+
const caseSensitiveRoutes = config.restServer?.caseSensitiveRoutes !== false;
|
|
34
|
+
const api = express();
|
|
35
|
+
// Mount case-sensitively (#47). This site is required in addition to the
|
|
36
|
+
// one in src/main.ts and is the one a partial fix misses: the parent's
|
|
37
|
+
// setting does not reach this sub-app, because mountRoute() calls
|
|
38
|
+
// registerCalls() -- which materialises this router -- before api.use()
|
|
39
|
+
// mounts it, so the prototype-chained settings inheritance express applies
|
|
40
|
+
// on mount arrives too late. Parent-only leaves every sub-path open.
|
|
41
|
+
//
|
|
42
|
+
// Must also precede registerCalls() for the same lazy-router reason.
|
|
43
|
+
api.set('case sensitive routing', caseSensitiveRoutes);
|
|
44
|
+
api.disable('x-powered-by');
|
|
45
|
+
this.expressInstance = api;
|
|
46
|
+
}
|
|
47
|
+
registerCalls() {
|
|
48
|
+
const { expressInstance } = this;
|
|
49
|
+
const { getState, sendStatusResponse } = Request;
|
|
50
|
+
for (const [method, handlers] of Object.entries(this.handlers)) {
|
|
51
|
+
if (!METHODS.has(method)) {
|
|
52
|
+
console.warn(`Method "${method}" is not a valid HTTP method. Skipping...`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const [route, handler] of Object.entries(handlers)) {
|
|
56
|
+
expressInstance[method](route, async (req, res) => {
|
|
57
|
+
// Run auth after route matching so request.params is populated
|
|
58
|
+
if (this.auth) {
|
|
59
|
+
const status = this.auth(req, getState(req));
|
|
60
|
+
if (status)
|
|
61
|
+
return sendStatusResponse(res, status);
|
|
62
|
+
}
|
|
63
|
+
const callStack = [...makeArray(handler)];
|
|
64
|
+
const mainCall = callStack.pop();
|
|
65
|
+
let response;
|
|
66
|
+
// Run middleware
|
|
67
|
+
while (callStack.length) {
|
|
68
|
+
response = await callStack.shift().bind(this)(req, getState(req));
|
|
69
|
+
if (response !== undefined)
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (response === undefined)
|
|
73
|
+
response = await mainCall(req, getState(req));
|
|
74
|
+
if (Number.isInteger(response))
|
|
75
|
+
return sendStatusResponse(res, response);
|
|
76
|
+
// Handle redirect if set via call state object
|
|
77
|
+
const state = getState(req);
|
|
78
|
+
const { redirect } = state;
|
|
79
|
+
if (redirect)
|
|
80
|
+
return res.redirect(redirect);
|
|
81
|
+
// Handle pipe if set via call state object
|
|
82
|
+
const { pipe } = state;
|
|
83
|
+
if (pipe) {
|
|
84
|
+
const { headers, source } = pipe;
|
|
85
|
+
if (headers)
|
|
86
|
+
for (const [key, value] of Object.entries(headers))
|
|
87
|
+
res.set(key, value);
|
|
88
|
+
return source.pipe(res);
|
|
89
|
+
}
|
|
90
|
+
if (response === undefined) {
|
|
91
|
+
res.sendStatus(200);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (typeof response !== 'object')
|
|
95
|
+
return sendStatusResponse(res, 500);
|
|
96
|
+
res.send(response);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//# 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.
|
|
7
|
+
"version": "0.2.1-beta.130",
|
|
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",
|
|
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.3-beta.
|
|
39
|
+
"stonyx": "0.2.3-beta.95"
|
|
31
40
|
},
|
|
32
41
|
"devDependencies": {
|
|
33
|
-
"@stonyx/utils": "0.2.3-beta.
|
|
42
|
+
"@stonyx/utils": "0.2.3-beta.27",
|
|
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
|
-
"
|
|
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
|
}
|
package/.claude/improvements.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
# Improvement Opportunities
|
|
2
|
-
|
|
3
|
-
## 1. `new express()` in RestServer constructor
|
|
4
|
-
|
|
5
|
-
**File**: `src/main.js`, line 30
|
|
6
|
-
|
|
7
|
-
```js
|
|
8
|
-
this.api = new express();
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Express 5 (which this project targets via `"express": "^5.1.0"`) documents calling `express()` as a plain function, not as a constructor with `new`. While `new express()` works in practice because the function returns a new object regardless, it is unconventional and may break if Express ever enforces non-constructor semantics. Consider changing to:
|
|
12
|
-
|
|
13
|
-
```js
|
|
14
|
-
this.api = express();
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Note: The `Request` class in `src/request.js` line 26 already uses the correct pattern (`const api = express();` without `new`).
|
|
18
|
-
|
|
19
|
-
## 2. `setupGlobalMiddleware` is `async` but contains no awaits
|
|
20
|
-
|
|
21
|
-
**File**: `src/main.js`, lines 63-70
|
|
22
|
-
|
|
23
|
-
```js
|
|
24
|
-
async setupGlobalMiddleware() {
|
|
25
|
-
const { origin, methods } = config.restServer;
|
|
26
|
-
|
|
27
|
-
this.api.use([
|
|
28
|
-
cors({ origin, methods }),
|
|
29
|
-
express.json()
|
|
30
|
-
]);
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The `async` keyword is unnecessary here since the method body contains no `await` expressions and `app.use()` is synchronous. The `async` keyword causes the method to return a `Promise` wrapping `undefined`, but the caller (`setupRouter`) does not `await` it either — it calls `this.setupGlobalMiddleware()` without `await` on line 52. Removing `async` would make the intent clearer.
|
|
35
|
-
|
|
36
|
-
## 3. `logMethod` config option is not used in rest-server source
|
|
37
|
-
|
|
38
|
-
**File**: `config/environment.js`, line 16
|
|
39
|
-
|
|
40
|
-
```js
|
|
41
|
-
logMethod: 'api'
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
The `logMethod` property is defined in the default config but is never referenced anywhere in `src/main.js` or `src/request.js`. It is likely consumed by the Stonyx framework core (`stonyx/log`) for registering a named log method, but this should be verified. If it is indeed framework-level plumbing, consider documenting that it is a Stonyx convention rather than a rest-server feature. If it is unused, consider removing it to reduce config surface area.
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
# @stonyx/rest-server — Project Structure
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
REST server module for the Stonyx framework. Provides dynamic route registration from a file directory, built-in CORS/JSON middleware, per-route authorization hooks, and a structured `Request` base class for defining handlers.
|
|
6
|
-
|
|
7
|
-
- **Package**: `@stonyx/rest-server` (v0.2.1-beta.1)
|
|
8
|
-
- **License**: Apache-2.0
|
|
9
|
-
- **Entry point**: `src/main.js`
|
|
10
|
-
- **Module type**: ESM (`"type": "module"`)
|
|
11
|
-
- **Node version**: v24.13.0 (per `.nvmrc`)
|
|
12
|
-
- **Package manager**: pnpm
|
|
13
|
-
|
|
14
|
-
## Architecture
|
|
15
|
-
|
|
16
|
-
### RestServer (src/main.js)
|
|
17
|
-
|
|
18
|
-
Singleton class wrapping an Express 5 instance.
|
|
19
|
-
|
|
20
|
-
- **Constructor** — enforces singleton via `RestServer.instance`; creates the Express app with `new express()`
|
|
21
|
-
- **`init()`** — calls `setupRouter()`, then starts listening on the configured port
|
|
22
|
-
- **`setupRouter()`** — calls `setupGlobalMiddleware()`, then uses `forEachFileImport` (from `@stonyx/utils/file`) to dynamically import all files in the configured `dir` and mount each as a route via `mountRoute()`. Optionally registers a `/health` endpoint.
|
|
23
|
-
- **`setupGlobalMiddleware()`** — attaches `cors()` and `express.json()` middleware to the Express app
|
|
24
|
-
- **`mountRoute(routeClass, { name, options })`** — instantiates the imported Request subclass, wires up the `authorization` middleware if present, calls `registerCalls()`, and mounts the sub-app at `/<filename>`
|
|
25
|
-
- **`RestServer.close()`** — static method to close the server
|
|
26
|
-
|
|
27
|
-
### Request (src/request.js)
|
|
28
|
-
|
|
29
|
-
Base class for route definitions. Each file in the requests directory exports a class extending `Request`.
|
|
30
|
-
|
|
31
|
-
- **Constructor** — creates a child Express instance with `x-powered-by` disabled
|
|
32
|
-
- **`handlers`** — instance property: object mapping HTTP methods (`get`, `post`, `put`, `delete`, `patch`) to route-path/handler pairs
|
|
33
|
-
- **`auth(req, state)`** — optional hook. Return an integer status code to reject the request; return nothing to allow it through.
|
|
34
|
-
- **`authorization(req, res, next)`** — wrapper that calls `auth()` and short-circuits with a status response if it returns a code
|
|
35
|
-
- **`registerCalls()`** — iterates `handlers`, registers each route on the child Express instance. Supports:
|
|
36
|
-
- Single handler function or array (last element is the main handler, preceding elements are middleware)
|
|
37
|
-
- Middleware functions are bound to the class instance and executed in order
|
|
38
|
-
- Integer return = status code response (via `sendStatusResponse`)
|
|
39
|
-
- Object return = JSON response
|
|
40
|
-
- `undefined` return = 200 OK
|
|
41
|
-
- Pipe support via `state.pipe` (sets headers and pipes a stream)
|
|
42
|
-
- **`Request.getState(req)`** — attaches/retrieves a `__stonyxState` object on the Express request
|
|
43
|
-
- **`Request.sendStatusResponse(res, status)`** — sends status with optional custom message from `config.restServer.statusMap`
|
|
44
|
-
|
|
45
|
-
Valid HTTP methods (enforced): `get`, `post`, `put`, `delete`, `patch`
|
|
46
|
-
|
|
47
|
-
## Configuration Reference
|
|
48
|
-
|
|
49
|
-
From `config/environment.js`. All values are overridable via environment variables.
|
|
50
|
-
|
|
51
|
-
| Option | Type | Default | Env Var | Description |
|
|
52
|
-
|---------------------|-------------------|-------------------------------|----------------------------|-----------------------------------------------------------------|
|
|
53
|
-
| `enableHealthCheck` | **Boolean** | `true` | `REST_HEALTH_CHECK_DISABLE=true` to disable | Registers `GET /health` returning 200 |
|
|
54
|
-
| `trustProxy` | **Boolean** | `false` | `REST_TRUST_PROXY=true` to enable | Trust reverse proxy headers (`X-Forwarded-Proto`) for correct protocol detection behind load balancers |
|
|
55
|
-
| `origin` | **String** | `'*'` | `REST_CORS_ORIGIN` | CORS allowed origin(s) |
|
|
56
|
-
| `methods` | **String** | `'GET,POST,PATCH,PUT,DELETE'` | `REST_CORS_METHODS` | CORS allowed methods |
|
|
57
|
-
| `dir` | **String** | `'./requests'` | `REST_REQUEST_PATH` | Directory containing Request class files to mount as routes |
|
|
58
|
-
| `port` | **Number/String** | `2666` | `REST_PORT` | Port the REST server listens on |
|
|
59
|
-
| `logColor` | **String** | `'yellow'` | — | Console log color for this module (Stonyx logging integration) |
|
|
60
|
-
| `logMethod` | **String** | `'api'` | — | Log method name (Stonyx logging integration) |
|
|
61
|
-
|
|
62
|
-
Additional config used (not rest-server-specific):
|
|
63
|
-
- `config.debug` (top-level Stonyx config) — if truthy, logs errors during route setup
|
|
64
|
-
- `config.restServer.statusMap` (optional, no default in environment.js) — maps status codes to custom message strings
|
|
65
|
-
- `config.restServer.camelCaseRoutes` (optional, no default in environment.js) — when falsy, passes `rawName: true` to `forEachFileImport` so filenames are used as-is for route paths
|
|
66
|
-
|
|
67
|
-
## Test Structure
|
|
68
|
-
|
|
69
|
-
Tests use **QUnit** and run via `stonyx test` (the `npm test` script).
|
|
70
|
-
|
|
71
|
-
### test/config/environment.js
|
|
72
|
-
Overrides `restServer.dir` to `'./test/sample/requests'` so tests load sample request classes.
|
|
73
|
-
|
|
74
|
-
### test/unit/request-test.js
|
|
75
|
-
Unit tests for `Request` static methods:
|
|
76
|
-
- `getState` — creates/returns state object on request
|
|
77
|
-
- `sendStatusResponse` — sends status with optional `statusMap` message
|
|
78
|
-
|
|
79
|
-
### test/integration/rest-server-test.js
|
|
80
|
-
Integration tests that boot the full server and make HTTP requests:
|
|
81
|
-
- 404 for non-existent routes
|
|
82
|
-
- `/public` — JSON response, 200 OK default, URL params, middleware (success/failure), `this` binding for handlers and middleware
|
|
83
|
-
- `/private` — authenticated success, auth hook rejection (505)
|
|
84
|
-
- `/health` — health check endpoint returns 200
|
|
85
|
-
|
|
86
|
-
### test/sample/requests/
|
|
87
|
-
Sample Request subclasses used by integration tests:
|
|
88
|
-
- `public.js` — `PublicRequest` with various GET handlers demonstrating middleware, params, binding
|
|
89
|
-
- `private.js` — `PrivateRequest` with `auth()` hook that rejects `/failure` with 505
|
|
90
|
-
|
|
91
|
-
## CI/CD
|
|
92
|
-
|
|
93
|
-
### .github/workflows/ci.yml
|
|
94
|
-
Runs on pull requests to `dev` and `main`. Delegates to shared workflow at `abofs/stonyx-workflows`.
|
|
95
|
-
|
|
96
|
-
### .github/workflows/publish.yml
|
|
97
|
-
Publishes to NPM. Triggered by:
|
|
98
|
-
- `workflow_dispatch` with version-type selection (patch/minor/major) or custom version
|
|
99
|
-
- Pull requests to `main`/`dev`
|
|
100
|
-
- Pushes to `main`
|
|
101
|
-
|
|
102
|
-
Delegates to `abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main`.
|
|
103
|
-
|
|
104
|
-
## File Structure
|
|
105
|
-
|
|
106
|
-
```
|
|
107
|
-
stonyx-rest-server/
|
|
108
|
-
├── .claude/
|
|
109
|
-
│ ├── improvements.md
|
|
110
|
-
│ └── project-structure.md # this file
|
|
111
|
-
├── .github/
|
|
112
|
-
│ └── workflows/
|
|
113
|
-
│ ├── ci.yml # PR CI — delegates to shared workflow
|
|
114
|
-
│ └── publish.yml # NPM publish — delegates to shared workflow
|
|
115
|
-
├── config/
|
|
116
|
-
│ └── environment.js # Default config with env var overrides
|
|
117
|
-
├── src/
|
|
118
|
-
│ ├── main.js # RestServer class (singleton, Express wrapper)
|
|
119
|
-
│ └── request.js # Request base class (handler registration, auth hook)
|
|
120
|
-
├── test/
|
|
121
|
-
│ ├── config/
|
|
122
|
-
│ │ └── environment.js # Test config override (dir → test/sample/requests)
|
|
123
|
-
│ ├── integration/
|
|
124
|
-
│ │ └── rest-server-test.js # Integration tests (QUnit)
|
|
125
|
-
│ ├── sample/
|
|
126
|
-
│ │ └── requests/
|
|
127
|
-
│ │ ├── private.js # Sample private request with auth hook
|
|
128
|
-
│ │ └── public.js # Sample public request with middleware demos
|
|
129
|
-
│ └── unit/
|
|
130
|
-
│ └── request-test.js # Unit tests for Request statics (QUnit)
|
|
131
|
-
├── .gitignore
|
|
132
|
-
├── .npmignore
|
|
133
|
-
├── .nvmrc # Node v24.13.0
|
|
134
|
-
├── LICENSE.md # Apache 2.0
|
|
135
|
-
├── package.json
|
|
136
|
-
├── pnpm-lock.yaml
|
|
137
|
-
└── README.md
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Dependencies
|
|
141
|
-
|
|
142
|
-
### Runtime
|
|
143
|
-
- `cors` ^2.8.5 — CORS middleware
|
|
144
|
-
- `express` ^5.1.0 — HTTP framework
|
|
145
|
-
- `stonyx` (local link) — Framework core (config, logging)
|
|
146
|
-
|
|
147
|
-
### Dev
|
|
148
|
-
- `@stonyx/utils` (local link) — Utility functions (file import, object helpers)
|
|
149
|
-
- `qunit` ^2.24.1 — Test framework
|
|
150
|
-
- `sinon` ^21.0.0 — Test stubs/spies
|
package/.github/workflows/ci.yml
DELETED
|
@@ -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,51 +0,0 @@
|
|
|
1
|
-
name: Publish to NPM
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
repository_dispatch:
|
|
5
|
-
types: [cascade-publish]
|
|
6
|
-
workflow_dispatch:
|
|
7
|
-
inputs:
|
|
8
|
-
version-type:
|
|
9
|
-
description: 'Version type'
|
|
10
|
-
required: true
|
|
11
|
-
type: choice
|
|
12
|
-
options:
|
|
13
|
-
- patch
|
|
14
|
-
- minor
|
|
15
|
-
- major
|
|
16
|
-
custom-version:
|
|
17
|
-
description: 'Custom version (optional, overrides version-type)'
|
|
18
|
-
required: false
|
|
19
|
-
type: string
|
|
20
|
-
pull_request:
|
|
21
|
-
types: [opened, synchronize, reopened]
|
|
22
|
-
branches: [main]
|
|
23
|
-
push:
|
|
24
|
-
branches: [main]
|
|
25
|
-
|
|
26
|
-
concurrency:
|
|
27
|
-
group: ${{ github.event_name == 'repository_dispatch' && 'cascade-update' || format('publish-{0}', github.ref) }}
|
|
28
|
-
cancel-in-progress: false
|
|
29
|
-
|
|
30
|
-
permissions:
|
|
31
|
-
contents: write
|
|
32
|
-
id-token: write
|
|
33
|
-
pull-requests: write
|
|
34
|
-
|
|
35
|
-
jobs:
|
|
36
|
-
publish:
|
|
37
|
-
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
38
|
-
uses: abofs/stonyx-workflows/.github/workflows/npm-publish.yml@main
|
|
39
|
-
with:
|
|
40
|
-
version-type: ${{ github.event.inputs.version-type }}
|
|
41
|
-
custom-version: ${{ github.event.inputs.custom-version }}
|
|
42
|
-
cascade-source: ${{ github.event.client_payload.source_package || '' }}
|
|
43
|
-
secrets: inherit
|
|
44
|
-
|
|
45
|
-
cascade:
|
|
46
|
-
needs: publish
|
|
47
|
-
uses: abofs/stonyx-workflows/.github/workflows/cascade.yml@main
|
|
48
|
-
with:
|
|
49
|
-
package-name: ${{ needs.publish.outputs.package-name }}
|
|
50
|
-
published-version: ${{ needs.publish.outputs.published-version }}
|
|
51
|
-
secrets: inherit
|
package/src/main.js
DELETED
|
@@ -1,86 +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, trustProxy } = config.restServer;
|
|
65
|
-
|
|
66
|
-
if (trustProxy) this.api.set('trust proxy', true);
|
|
67
|
-
|
|
68
|
-
this.api.use([
|
|
69
|
-
cors({ origin, methods }),
|
|
70
|
-
express.json()
|
|
71
|
-
]);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async mountRoute(routeClass, { name, options }) {
|
|
75
|
-
const { api } = this;
|
|
76
|
-
const classInstance = new routeClass(options);
|
|
77
|
-
const route = name === 'index' ? '/' : `/${name}`;
|
|
78
|
-
const { expressInstance } = classInstance;
|
|
79
|
-
|
|
80
|
-
classInstance.registerCalls();
|
|
81
|
-
expressInstance.mountpath = route;
|
|
82
|
-
|
|
83
|
-
// Mount handler to main api instance
|
|
84
|
-
api.use(route, expressInstance);
|
|
85
|
-
}
|
|
86
|
-
}
|
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
|
-
}
|