@facetlayer/prism-framework 0.4.0 → 0.4.1
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 +176 -8
- package/dist/Errors.d.ts +38 -0
- package/dist/Errors.d.ts.map +1 -0
- package/dist/Metrics.d.ts +5 -0
- package/dist/Metrics.d.ts.map +1 -0
- package/dist/RequestContext.d.ts +17 -0
- package/dist/RequestContext.d.ts.map +1 -0
- package/dist/ServiceDefinition.d.ts +16 -0
- package/dist/ServiceDefinition.d.ts.map +1 -0
- package/dist/app/PrismApp.d.ts +31 -0
- package/dist/app/PrismApp.d.ts.map +1 -0
- package/dist/app/callEndpoint.d.ts +13 -0
- package/dist/app/callEndpoint.d.ts.map +1 -0
- package/dist/app/validateApp.d.ts +20 -0
- package/dist/app/validateApp.d.ts.map +1 -0
- package/dist/authorization/AuthSource.d.ts +8 -0
- package/dist/authorization/AuthSource.d.ts.map +1 -0
- package/dist/authorization/Authorization.d.ts +24 -0
- package/dist/authorization/Authorization.d.ts.map +1 -0
- package/dist/authorization/Resource.d.ts +5 -0
- package/dist/authorization/Resource.d.ts.map +1 -0
- package/dist/authorization/index.d.ts +5 -0
- package/dist/authorization/index.d.ts.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/databases/DatabaseInitializationOptions.d.ts +9 -0
- package/dist/databases/DatabaseInitializationOptions.d.ts.map +1 -0
- package/dist/databases/DatabaseSetup.d.ts +3 -0
- package/dist/databases/DatabaseSetup.d.ts.map +1 -0
- package/dist/endpoints/createEndpoint.d.ts +4 -0
- package/dist/endpoints/createEndpoint.d.ts.map +1 -0
- package/dist/endpoints/getEffectiveOperationId.d.ts +19 -0
- package/dist/endpoints/getEffectiveOperationId.d.ts.map +1 -0
- package/dist/env/Env.d.ts +2 -0
- package/dist/env/Env.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1364 -0
- package/dist/launch/launchConfig.d.ts +18 -0
- package/dist/launch/launchConfig.d.ts.map +1 -0
- package/dist/logging/index.d.ts +9 -0
- package/dist/logging/index.d.ts.map +1 -0
- package/dist/sse/ConnectionManager.d.ts +23 -0
- package/dist/sse/ConnectionManager.d.ts.map +1 -0
- package/dist/stdin/StdinServer.d.ts +38 -0
- package/dist/stdin/StdinServer.d.ts.map +1 -0
- package/dist/web/EndpointListing.d.ts +3 -0
- package/dist/web/EndpointListing.d.ts.map +1 -0
- package/dist/web/ExpressAppSetup.d.ts +18 -0
- package/dist/web/ExpressAppSetup.d.ts.map +1 -0
- package/dist/web/ExpressEndpointSetup.d.ts +31 -0
- package/dist/web/ExpressEndpointSetup.d.ts.map +1 -0
- package/dist/web/SseResponse.d.ts +15 -0
- package/dist/web/SseResponse.d.ts.map +1 -0
- package/dist/web/ViteIntegration.d.ts +19 -0
- package/dist/web/ViteIntegration.d.ts.map +1 -0
- package/dist/web/corsMiddleware.d.ts +14 -0
- package/dist/web/corsMiddleware.d.ts.map +1 -0
- package/dist/web/localhostOnlyMiddleware.d.ts +3 -0
- package/dist/web/localhostOnlyMiddleware.d.ts.map +1 -0
- package/dist/web/openapi/OpenAPI.d.ts +37 -0
- package/dist/web/openapi/OpenAPI.d.ts.map +1 -0
- package/dist/web/openapi/validateServicesForOpenapi.d.ts +32 -0
- package/dist/web/openapi/validateServicesForOpenapi.d.ts.map +1 -0
- package/dist/web/requestContextMiddleware.d.ts +3 -0
- package/dist/web/requestContextMiddleware.d.ts.map +1 -0
- package/docs/authorization.md +281 -0
- package/docs/cors-setup.md +172 -0
- package/docs/creating-services.md +220 -0
- package/docs/database-setup.md +134 -0
- package/docs/endpoint-tools.md +1 -11
- package/docs/env-files.md +12 -1
- package/docs/error-handling.md +70 -0
- package/docs/getting-started.md +22 -12
- package/docs/launch-configuration.md +223 -0
- package/docs/overview.md +62 -0
- package/docs/server-setup.md +144 -0
- package/docs/source-directory-organization.md +115 -0
- package/docs/stdin-protocol.md +176 -0
- package/package.json +42 -9
- package/src/Errors.ts +120 -0
- package/src/Metrics.ts +53 -0
- package/src/RequestContext.ts +36 -0
- package/src/ServiceDefinition.ts +35 -0
- package/src/__tests__/Authorization.test.ts +350 -0
- package/src/__tests__/Errors.test.ts +378 -0
- package/src/__tests__/ListEndpoints.test.ts +98 -0
- package/src/__tests__/PrismApp.test.ts +274 -0
- package/src/__tests__/RequestContext.test.ts +295 -0
- package/src/__tests__/SseResponse.test.ts +189 -0
- package/src/__tests__/StdinServer.test.ts +304 -0
- package/src/__tests__/corsMiddleware.test.ts +293 -0
- package/src/__tests__/createEndpoint.test.ts +412 -0
- package/src/__tests__/validateApp.test.ts +206 -0
- package/src/app/PrismApp.ts +117 -0
- package/src/app/callEndpoint.ts +55 -0
- package/src/app/validateApp.ts +78 -0
- package/src/authorization/AuthSource.ts +14 -0
- package/src/authorization/Authorization.ts +78 -0
- package/src/authorization/Resource.ts +8 -0
- package/src/authorization/index.ts +4 -0
- package/src/databases/DatabaseInitializationOptions.ts +9 -0
- package/src/databases/DatabaseSetup.ts +19 -0
- package/src/endpoints/createEndpoint.ts +39 -0
- package/src/endpoints/getEffectiveOperationId.ts +90 -0
- package/src/env/Env.ts +23 -0
- package/src/index.ts +78 -0
- package/src/launch/launchConfig.ts +59 -0
- package/src/list-endpoints-command.ts +1 -1
- package/src/logging/index.ts +25 -0
- package/src/sse/ConnectionManager.ts +79 -0
- package/src/stdin/StdinServer.ts +129 -0
- package/src/web/EndpointListing.ts +166 -0
- package/src/web/ExpressAppSetup.ts +125 -0
- package/src/web/ExpressEndpointSetup.ts +178 -0
- package/src/web/SseResponse.ts +78 -0
- package/src/web/ViteIntegration.ts +72 -0
- package/src/web/__tests__/OpenAPI.invalidZodSchemas.test.ts +250 -0
- package/src/web/corsMiddleware.ts +63 -0
- package/src/web/localhostOnlyMiddleware.ts +19 -0
- package/src/web/openapi/OpenAPI.ts +248 -0
- package/src/web/openapi/validateServicesForOpenapi.ts +76 -0
- package/src/web/requestContextMiddleware.ts +25 -0
- package/.claude/settings.local.json +0 -20
- package/CHANGELOG +0 -28
- package/CLAUDE.md +0 -44
- package/build.mts +0 -8
- package/test/call-command.test.ts +0 -96
- package/test/generate-api-clients.test.ts +0 -33
- package/test/generate-api-clients.test.ts.disabled +0 -75
- package/tsconfig.json +0 -21
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Stdin/Stdout Protocol
|
|
2
|
+
|
|
3
|
+
Prism Framework apps can run in **stdin protocol mode** instead of starting an HTTP server. This is useful for:
|
|
4
|
+
|
|
5
|
+
- Embedding a Prism app as a subprocess in a larger application
|
|
6
|
+
- Composing multiple Prism apps to serve parts of a web UI
|
|
7
|
+
- Running in environments where opening a port is not desirable
|
|
8
|
+
- Building process-based microservice architectures
|
|
9
|
+
|
|
10
|
+
## How It Works
|
|
11
|
+
|
|
12
|
+
When started with `--stdin`, the app communicates over **newline-delimited JSON (NDJSON)** on stdin and stdout:
|
|
13
|
+
|
|
14
|
+
- The parent process sends **request messages** as JSON lines to the app's stdin
|
|
15
|
+
- The app sends **response messages** as JSON lines to its stdout
|
|
16
|
+
- All logging is redirected to stderr so it doesn't interfere with the protocol
|
|
17
|
+
|
|
18
|
+
The same endpoints defined with `createEndpoint` work identically in both modes.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Setting Up Your App
|
|
23
|
+
|
|
24
|
+
Use `startStdinServer` as an alternative to `startServer`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import {
|
|
28
|
+
createEndpoint, App, startServer, startStdinServer,
|
|
29
|
+
type ServiceDefinition,
|
|
30
|
+
} from '@facetlayer/prism-framework';
|
|
31
|
+
import { z } from 'zod';
|
|
32
|
+
|
|
33
|
+
const myService: ServiceDefinition = {
|
|
34
|
+
name: 'items',
|
|
35
|
+
endpoints: [
|
|
36
|
+
createEndpoint({
|
|
37
|
+
method: 'GET',
|
|
38
|
+
path: '/items',
|
|
39
|
+
responseSchema: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
40
|
+
handler: async () => [{ id: '1', name: 'Item 1' }],
|
|
41
|
+
}),
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const app = new App({ services: [myService] });
|
|
46
|
+
|
|
47
|
+
if (process.argv.includes('--stdin')) {
|
|
48
|
+
startStdinServer({ app });
|
|
49
|
+
} else {
|
|
50
|
+
await startServer({ app, port: 3000 });
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Launching as a Subprocess
|
|
55
|
+
|
|
56
|
+
From the parent process, spawn the app and communicate over pipes:
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { spawn } from 'child_process';
|
|
60
|
+
|
|
61
|
+
const child = spawn('node', ['my-app.js', '--stdin'], {
|
|
62
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Send a request
|
|
66
|
+
child.stdin.write(JSON.stringify({
|
|
67
|
+
id: 'req-1',
|
|
68
|
+
method: 'GET',
|
|
69
|
+
path: '/items',
|
|
70
|
+
}) + '\n');
|
|
71
|
+
|
|
72
|
+
// Read responses line by line
|
|
73
|
+
let buffer = '';
|
|
74
|
+
child.stdout.on('data', (data) => {
|
|
75
|
+
buffer += data.toString();
|
|
76
|
+
const lines = buffer.split('\n');
|
|
77
|
+
buffer = lines.pop();
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
if (!line.trim()) continue;
|
|
80
|
+
const response = JSON.parse(line);
|
|
81
|
+
console.log('Response:', response);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Protocol Specification
|
|
87
|
+
|
|
88
|
+
### Request Message
|
|
89
|
+
|
|
90
|
+
Each request is a single JSON line written to the app's stdin:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"id": "unique-request-id",
|
|
95
|
+
"method": "GET",
|
|
96
|
+
"path": "/items/123",
|
|
97
|
+
"body": { "optional": "data" }
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
| Field | Type | Required | Description |
|
|
102
|
+
|----------|--------|----------|------------------------------------------------|
|
|
103
|
+
| `id` | string | Yes | Unique ID to correlate the response |
|
|
104
|
+
| `method` | string | Yes | HTTP method: GET, POST, PUT, DELETE, PATCH |
|
|
105
|
+
| `path` | string | Yes | Endpoint path, e.g. `/items` or `/items/123` |
|
|
106
|
+
| `body` | object | No | Request body / input data |
|
|
107
|
+
|
|
108
|
+
### Response Message
|
|
109
|
+
|
|
110
|
+
Each response is a single JSON line written to stdout:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"id": "unique-request-id",
|
|
115
|
+
"status": 200,
|
|
116
|
+
"body": { "id": "123", "name": "Item" }
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| Field | Type | Description |
|
|
121
|
+
|----------|--------|------------------------------------------------|
|
|
122
|
+
| `id` | string | Matches the request ID |
|
|
123
|
+
| `status` | number | HTTP-style status code (200, 400, 404, 500, etc.) |
|
|
124
|
+
| `body` | any | Response data or error details |
|
|
125
|
+
|
|
126
|
+
### Ready Signal
|
|
127
|
+
|
|
128
|
+
When the app starts, it sends a ready message:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{ "id": "_ready", "status": 200, "body": { "message": "stdin server ready" } }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Wait for this message before sending requests.
|
|
135
|
+
|
|
136
|
+
### Error Responses
|
|
137
|
+
|
|
138
|
+
Errors from handlers (using `NotFoundError`, `BadRequestError`, etc.) are returned with the appropriate status code:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{ "id": "req-1", "status": 404, "body": { "message": "Item not found" } }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Invalid JSON or missing fields return status 400:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "id": "unknown", "status": 400, "body": { "message": "Invalid JSON" } }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Process Lifecycle
|
|
151
|
+
|
|
152
|
+
- The app exits when stdin is closed (EOF)
|
|
153
|
+
- The parent should close stdin to signal the app to shut down
|
|
154
|
+
- All logging output goes to stderr, keeping stdout clean for the protocol
|
|
155
|
+
|
|
156
|
+
## Composing Multiple Subprocess UIs
|
|
157
|
+
|
|
158
|
+
A key use case is running multiple Prism apps as subprocesses that each render a portion of a larger web UI. The parent process acts as a coordinator:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
┌──────────────────────────────────────────┐
|
|
162
|
+
│ Parent Web Server │
|
|
163
|
+
│ │
|
|
164
|
+
│ ┌─────────────┐ ┌─────────────┐ │
|
|
165
|
+
│ │ App A │ │ App B │ │
|
|
166
|
+
│ │ (--stdin) │ │ (--stdin) │ │
|
|
167
|
+
│ │ /dashboard/* │ │ /settings/* │ │
|
|
168
|
+
│ └──────┬───────┘ └──────┬──────┘ │
|
|
169
|
+
│ │stdin/stdout │stdin/stdout │
|
|
170
|
+
│ └─────────┬────────┘ │
|
|
171
|
+
│ │ │
|
|
172
|
+
│ Request Router │
|
|
173
|
+
└──────────────────────────────────────────┘
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
The parent process routes incoming HTTP requests to the appropriate subprocess based on path prefix, translates them to stdin protocol messages, and sends the subprocess responses back to the browser.
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@facetlayer/prism-framework",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Base library and CLI tools for the Prism app framework",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Base library, server framework, and CLI tools for the Prism app framework",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/
|
|
7
|
-
"types": "dist/
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
8
|
"bin": {
|
|
9
9
|
"prism": "dist/cli.js"
|
|
10
10
|
},
|
|
@@ -17,26 +17,59 @@
|
|
|
17
17
|
},
|
|
18
18
|
"keywords": [
|
|
19
19
|
"framework",
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"typescript"
|
|
20
|
+
"express",
|
|
21
|
+
"electron",
|
|
22
|
+
"typescript",
|
|
23
|
+
"saas",
|
|
24
|
+
"tools"
|
|
23
25
|
],
|
|
24
26
|
"author": "",
|
|
25
27
|
"license": "MIT",
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"dist",
|
|
31
|
+
"docs",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
26
34
|
"packageManager": "pnpm@10.15.1",
|
|
27
35
|
"dependencies": {
|
|
36
|
+
"@asteasolutions/zod-to-openapi": "^8.1.0",
|
|
28
37
|
"@facetlayer/doc-files-helper": "0.1.2",
|
|
29
|
-
"@facetlayer/prism-framework-api": "0.2.0",
|
|
30
38
|
"@facetlayer/qc": "^0.1.0",
|
|
39
|
+
"@facetlayer/sqlite-wrapper": "^1.2.0",
|
|
40
|
+
"@facetlayer/streams": "^1.0.0",
|
|
41
|
+
"cookie-parser": "^1.4.7",
|
|
31
42
|
"dotenv": "^16.4.7",
|
|
43
|
+
"express": "^4.21.1",
|
|
44
|
+
"openapi3-ts": "^4.5.0",
|
|
45
|
+
"prom-client": "^15.1.3",
|
|
46
|
+
"swagger-ui-express": "^5.0.1",
|
|
32
47
|
"uuid": "^11.1.0",
|
|
33
|
-
"yargs": "18.0.0"
|
|
48
|
+
"yargs": "18.0.0",
|
|
49
|
+
"zod": "^4.1.12"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"vite": "^6.0.0",
|
|
53
|
+
"zod": "^4.0.5"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"vite": {
|
|
57
|
+
"optional": true
|
|
58
|
+
},
|
|
59
|
+
"zod": {
|
|
60
|
+
"optional": false
|
|
61
|
+
}
|
|
34
62
|
},
|
|
35
63
|
"devDependencies": {
|
|
36
64
|
"@facetlayer/build-config-nodejs": "^0.3.0",
|
|
65
|
+
"@facetlayer/subprocess": "^1.1.1",
|
|
66
|
+
"@types/cookie-parser": "^1.4.7",
|
|
67
|
+
"@types/express": "^5.0.0",
|
|
37
68
|
"@types/node": "^22.10.2",
|
|
69
|
+
"@types/swagger-ui-express": "^4.1.8",
|
|
38
70
|
"@types/uuid": "^10.0.0",
|
|
39
71
|
"@types/yargs": "17.0.34",
|
|
72
|
+
"esbuild": "^0.25.12",
|
|
40
73
|
"typescript": "^5.7.2",
|
|
41
74
|
"vitest": "^3.0.0"
|
|
42
75
|
}
|
package/src/Errors.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Errors
|
|
3
|
+
|
|
4
|
+
Helper classes for various HTTP error codes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class HttpError extends Error {
|
|
8
|
+
public statusCode: number;
|
|
9
|
+
public details?: any;
|
|
10
|
+
|
|
11
|
+
constructor(statusCode: number, message: string, details?: any) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.statusCode = statusCode;
|
|
14
|
+
this.details = details;
|
|
15
|
+
this.name = 'HttpError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class BadRequestError extends HttpError {
|
|
20
|
+
constructor(message: string = 'Bad Request', details?: any) {
|
|
21
|
+
super(400, message, details);
|
|
22
|
+
this.name = 'BadRequestError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SchemaValidationError extends HttpError {
|
|
27
|
+
constructor(message: string = 'Schema Validation Error', details?: any) {
|
|
28
|
+
super(422, message, details);
|
|
29
|
+
this.name = 'SchemaValidationError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class ResponseSchemaValidationError extends HttpError {
|
|
34
|
+
constructor(message: string = 'Response Schema Validation Error', details?: any) {
|
|
35
|
+
super(500, message, details);
|
|
36
|
+
this.name = 'ResponseSchemaValidationError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class UnauthorizedError extends HttpError {
|
|
41
|
+
constructor(message: string = 'Unauthorized', details?: any) {
|
|
42
|
+
super(401, message, details);
|
|
43
|
+
this.name = 'UnauthorizedError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ForbiddenError extends HttpError {
|
|
48
|
+
constructor(message: string = 'Forbidden', details?: any) {
|
|
49
|
+
super(403, message, details);
|
|
50
|
+
this.name = 'ForbiddenError';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class NotFoundError extends HttpError {
|
|
55
|
+
constructor(message: string = 'Not Found', details?: any) {
|
|
56
|
+
super(404, message, details);
|
|
57
|
+
this.name = 'NotFoundError';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class ConflictError extends HttpError {
|
|
62
|
+
constructor(message: string = 'Conflict', details?: any) {
|
|
63
|
+
super(409, message, details);
|
|
64
|
+
this.name = 'ConflictError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ValidationError extends HttpError {
|
|
69
|
+
constructor(message: string = 'Validation Error', details?: any) {
|
|
70
|
+
super(422, message, details);
|
|
71
|
+
this.name = 'ValidationError';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class NotImplementedError extends HttpError {
|
|
76
|
+
constructor(message: string = 'Not Implemented', details?: any) {
|
|
77
|
+
super(501, message, details);
|
|
78
|
+
this.name = 'NotImplementedError';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export class ServiceUnavailableError extends HttpError {
|
|
83
|
+
constructor(message: string = 'Service Unavailable', details?: any) {
|
|
84
|
+
super(503, message, details);
|
|
85
|
+
this.name = 'ServiceUnavailableError';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function createErrorFromStatus(
|
|
90
|
+
statusCode: number,
|
|
91
|
+
message?: string,
|
|
92
|
+
details?: any
|
|
93
|
+
): HttpError {
|
|
94
|
+
switch (statusCode) {
|
|
95
|
+
case 400:
|
|
96
|
+
return new BadRequestError(message, details);
|
|
97
|
+
case 401:
|
|
98
|
+
return new UnauthorizedError(message, details);
|
|
99
|
+
case 403:
|
|
100
|
+
return new ForbiddenError(message, details);
|
|
101
|
+
case 404:
|
|
102
|
+
return new NotFoundError(message, details);
|
|
103
|
+
case 409:
|
|
104
|
+
return new ConflictError(message, details);
|
|
105
|
+
case 422:
|
|
106
|
+
return new ValidationError(message, details);
|
|
107
|
+
case 500:
|
|
108
|
+
return new HttpError(500, message || 'Internal Server Error', details);
|
|
109
|
+
case 501:
|
|
110
|
+
return new NotImplementedError(message, details);
|
|
111
|
+
case 503:
|
|
112
|
+
return new ServiceUnavailableError(message, details);
|
|
113
|
+
default:
|
|
114
|
+
return new HttpError(statusCode, message || 'Unknown Error', details);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function isHttpError(error: any): error is HttpError {
|
|
119
|
+
return error instanceof HttpError;
|
|
120
|
+
}
|
package/src/Metrics.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import PromClient from 'prom-client';
|
|
2
|
+
|
|
3
|
+
let _hasSetupMetrics = false;
|
|
4
|
+
let httpRequests: PromClient.Counter;
|
|
5
|
+
let httpResponses: PromClient.Counter;
|
|
6
|
+
|
|
7
|
+
export function setupMetrics(): void {
|
|
8
|
+
_hasSetupMetrics = true;
|
|
9
|
+
|
|
10
|
+
PromClient.collectDefaultMetrics({
|
|
11
|
+
// prefix: ...
|
|
12
|
+
// labels: ...
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
httpRequests = new PromClient.Counter({
|
|
16
|
+
name: 'http_request_counter',
|
|
17
|
+
help: 'HTTP requests',
|
|
18
|
+
labelNames: ['method', 'endpoint'],
|
|
19
|
+
});
|
|
20
|
+
httpResponses = new PromClient.Counter({
|
|
21
|
+
name: 'http_response_counter',
|
|
22
|
+
help: 'HTTP responses',
|
|
23
|
+
labelNames: ['method', 'endpoint', 'status_code', 'duration'],
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Function to record an HTTP request
|
|
28
|
+
export function recordHttpRequest(method: string, endpoint: string): void {
|
|
29
|
+
if (!_hasSetupMetrics) {
|
|
30
|
+
setupMetrics();
|
|
31
|
+
}
|
|
32
|
+
httpRequests.inc({ method, endpoint });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function recordHttpResponse(
|
|
36
|
+
method: string,
|
|
37
|
+
endpoint: string,
|
|
38
|
+
statusCode: number,
|
|
39
|
+
duration: number
|
|
40
|
+
): void {
|
|
41
|
+
if (!_hasSetupMetrics) {
|
|
42
|
+
setupMetrics();
|
|
43
|
+
}
|
|
44
|
+
httpResponses.inc({ method, endpoint, status_code: statusCode.toString(), duration });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Function to get metrics in Prometheus format
|
|
48
|
+
export function getMetrics(): Promise<string> {
|
|
49
|
+
if (!_hasSetupMetrics) {
|
|
50
|
+
setupMetrics();
|
|
51
|
+
}
|
|
52
|
+
return PromClient.register.metrics();
|
|
53
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
|
+
import type { Request, Response } from 'express';
|
|
3
|
+
import type { Authorization } from './authorization/Authorization.ts';
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* RequestContext
|
|
7
|
+
*
|
|
8
|
+
* Helper object stored on every incoming request using AsyncLocalStorage.
|
|
9
|
+
*
|
|
10
|
+
* Includes
|
|
11
|
+
* - The request and response objects
|
|
12
|
+
* - Authorization data
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface RequestContext {
|
|
16
|
+
requestId: string;
|
|
17
|
+
startTime: number;
|
|
18
|
+
req?: Request;
|
|
19
|
+
res?: Response;
|
|
20
|
+
auth: Authorization;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const requestContextStorage = new AsyncLocalStorage<RequestContext>();
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export function withRequestContext<T>(context: RequestContext, fn: () => T): T {
|
|
27
|
+
return requestContextStorage.run(context, fn);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getCurrentRequestContext(): RequestContext | undefined {
|
|
31
|
+
return requestContextStorage.getStore();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const RequestContext = {
|
|
35
|
+
getCurrentRequestContext,
|
|
36
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import type { EndpointDefinition } from './web/ExpressEndpointSetup.ts';
|
|
3
|
+
|
|
4
|
+
export interface MiddlewareDefinition {
|
|
5
|
+
path: string;
|
|
6
|
+
handler: (req: Request, res: Response, next: NextFunction) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* ServiceDefinition
|
|
11
|
+
*
|
|
12
|
+
* A service is a self-contained module that can contain:
|
|
13
|
+
* - API endpoints
|
|
14
|
+
* - Middleware
|
|
15
|
+
* - Database schemas
|
|
16
|
+
* - Background jobs
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface ServiceDefinition {
|
|
20
|
+
name: string;
|
|
21
|
+
|
|
22
|
+
// API endpoints
|
|
23
|
+
endpoints?: EndpointDefinition[];
|
|
24
|
+
|
|
25
|
+
// Middleware
|
|
26
|
+
middleware?: MiddlewareDefinition[];
|
|
27
|
+
|
|
28
|
+
// Database schemas
|
|
29
|
+
databases?: Record<string, {
|
|
30
|
+
statements: string[];
|
|
31
|
+
}>;
|
|
32
|
+
|
|
33
|
+
// Callback used to launch background jobs
|
|
34
|
+
startJobs?: () => Promise<void>;
|
|
35
|
+
}
|