@fishka/express 0.9.13 → 0.9.15
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 +108 -38
- package/dist/cjs/api.types.d.ts +8 -21
- package/dist/cjs/api.types.js +1 -21
- package/dist/cjs/api.types.js.map +1 -1
- package/dist/cjs/error-handling.d.ts +72 -3
- package/dist/cjs/error-handling.js +107 -7
- package/dist/cjs/error-handling.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -0
- package/dist/cjs/index.js +1 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/route-table.d.ts +15 -7
- package/dist/cjs/route-table.js +3 -0
- package/dist/cjs/route-table.js.map +1 -1
- package/dist/cjs/router.d.ts +25 -33
- package/dist/cjs/router.js +40 -60
- package/dist/cjs/router.js.map +1 -1
- package/dist/cjs/utils/type-validators.d.ts +58 -0
- package/dist/cjs/utils/type-validators.js +122 -0
- package/dist/cjs/utils/type-validators.js.map +1 -0
- package/dist/esm/api.types.d.ts +8 -21
- package/dist/esm/api.types.js +0 -18
- package/dist/esm/api.types.js.map +1 -1
- package/dist/esm/error-handling.d.ts +72 -3
- package/dist/esm/error-handling.js +106 -7
- package/dist/esm/error-handling.js.map +1 -1
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/route-table.d.ts +15 -7
- package/dist/esm/route-table.js +3 -0
- package/dist/esm/route-table.js.map +1 -1
- package/dist/esm/router.d.ts +25 -33
- package/dist/esm/router.js +42 -62
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/utils/type-validators.d.ts +58 -0
- package/dist/esm/utils/type-validators.js +102 -0
- package/dist/esm/utils/type-validators.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,56 +12,105 @@ npm install @fishka/express
|
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
14
|
import express from 'express';
|
|
15
|
-
import { createRouteTable } from '@fishka/express';
|
|
15
|
+
import { createRouteTable, param, toInt } from '@fishka/express';
|
|
16
|
+
import { assertString } from '@fishka/assertions';
|
|
16
17
|
|
|
17
18
|
const app = express();
|
|
18
19
|
app.use(express.json());
|
|
19
20
|
|
|
20
21
|
const routes = createRouteTable(app);
|
|
21
22
|
|
|
22
|
-
// GET /users/:id -
|
|
23
|
-
routes.get
|
|
24
|
-
id:
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
// GET /users/:id - with typed path params
|
|
24
|
+
routes.get('users/:id', {
|
|
25
|
+
$path: { id: param(toInt()) },
|
|
26
|
+
run: async ctx => ({
|
|
27
|
+
id: ctx.path.id, // number - typed from $path
|
|
28
|
+
name: 'John',
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
27
31
|
|
|
28
|
-
// GET /users -
|
|
29
|
-
routes.get
|
|
30
|
-
{ id:
|
|
31
|
-
{ id:
|
|
32
|
+
// GET /users - list all users
|
|
33
|
+
routes.get('users', async () => [
|
|
34
|
+
{ id: 1, name: 'John' },
|
|
35
|
+
{ id: 2, name: 'Jane' },
|
|
32
36
|
]);
|
|
33
37
|
|
|
34
|
-
// POST /users
|
|
35
|
-
routes.post<{ name: string }, { id:
|
|
38
|
+
// POST /users - with body validation
|
|
39
|
+
routes.post<{ name: string }, { id: number }>('users', {
|
|
36
40
|
$body: { name: v => assertString(v, 'name required') },
|
|
37
|
-
run: async ctx => ({ id:
|
|
41
|
+
run: async ctx => ({ id: 1 }),
|
|
38
42
|
});
|
|
39
43
|
|
|
40
|
-
// DELETE /users/:id
|
|
41
|
-
routes.delete('users/:id', async () => {
|
|
42
|
-
// Delete user logic
|
|
43
|
-
});
|
|
44
|
+
// DELETE /users/:id
|
|
45
|
+
routes.delete('users/:id', async () => {});
|
|
44
46
|
|
|
45
47
|
app.listen(3000);
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
## URL
|
|
50
|
+
## URL Parameter Validation
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
Use `param()` to validate and transform path/query parameters. All operators are composable:
|
|
51
53
|
|
|
52
54
|
```typescript
|
|
53
|
-
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
import { param, toInt, minLength, matches, min, range, oneOf } from '@fishka/express';
|
|
56
|
+
|
|
57
|
+
routes.get('users/:id', {
|
|
58
|
+
$path: {
|
|
59
|
+
id: param(toInt()), // string → number
|
|
60
|
+
},
|
|
61
|
+
$query: {
|
|
62
|
+
page: param(toInt(), min(1)), // number >= 1
|
|
63
|
+
limit: param(toInt(), range(1, 100)), // number 1-100
|
|
64
|
+
sort: param(oneOf('asc', 'desc')), // enum
|
|
65
|
+
search: param(minLength(3)), // string min 3 chars
|
|
61
66
|
},
|
|
67
|
+
run: async ctx => ({
|
|
68
|
+
id: ctx.path.id, // number
|
|
69
|
+
page: ctx.query.page, // number
|
|
70
|
+
sort: ctx.query.sort, // 'asc' | 'desc'
|
|
71
|
+
}),
|
|
62
72
|
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Available Operators
|
|
76
|
+
|
|
77
|
+
**Transformations (string → T):**
|
|
78
|
+
- `toInt()` - parse to integer
|
|
79
|
+
- `toNumber()` - parse to number
|
|
80
|
+
- `toBool()` - parse 'true'/'false' to boolean
|
|
81
|
+
- `oneOf('a', 'b')` - enum values
|
|
63
82
|
|
|
64
|
-
|
|
83
|
+
**String validators:**
|
|
84
|
+
- `minLength(n)` - minimum length
|
|
85
|
+
- `maxLength(n)` - maximum length
|
|
86
|
+
- `matches(/regex/)` - regex match
|
|
87
|
+
- `trim` - trim whitespace
|
|
88
|
+
- `lowercase` / `uppercase` - case transform
|
|
89
|
+
|
|
90
|
+
**Number validators:**
|
|
91
|
+
- `min(n)` - minimum value
|
|
92
|
+
- `max(n)` - maximum value
|
|
93
|
+
- `range(min, max)` - value range
|
|
94
|
+
|
|
95
|
+
**Generic:**
|
|
96
|
+
- `check(fn, msg)` - custom validation
|
|
97
|
+
- `map(fn)` - transform value
|
|
98
|
+
|
|
99
|
+
### Optional Parameters
|
|
100
|
+
|
|
101
|
+
Use `optional()` to make parameters optional:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { optional, param, toInt } from '@fishka/express';
|
|
105
|
+
|
|
106
|
+
routes.get('users', {
|
|
107
|
+
$query: {
|
|
108
|
+
page: optional(param(toInt())), // number | undefined
|
|
109
|
+
},
|
|
110
|
+
run: async ctx => {
|
|
111
|
+
const page = ctx.query.page ?? 1;
|
|
112
|
+
},
|
|
113
|
+
});
|
|
65
114
|
```
|
|
66
115
|
|
|
67
116
|
## Authentication
|
|
@@ -110,12 +159,11 @@ assertHttp(user.isAdmin, HTTP_FORBIDDEN, 'Admin access required');
|
|
|
110
159
|
|
|
111
160
|
## Complete Example
|
|
112
161
|
|
|
113
|
-
|
|
162
|
+
Full initialization with TLS context, validation, and error handling:
|
|
114
163
|
|
|
115
164
|
```typescript
|
|
116
165
|
import express from 'express';
|
|
117
|
-
import { createRouteTable, createTlsMiddleware, catchAllMiddleware,
|
|
118
|
-
import { assertString, assertTruthy } from '@fishka/assertions';
|
|
166
|
+
import { createRouteTable, createTlsMiddleware, catchAllMiddleware, param, toInt } from '@fishka/express';
|
|
119
167
|
|
|
120
168
|
const app = express();
|
|
121
169
|
|
|
@@ -125,21 +173,43 @@ app.use(express.json());
|
|
|
125
173
|
// 2. Initialize TLS context (Request IDs, etc.)
|
|
126
174
|
app.use(createTlsMiddleware());
|
|
127
175
|
|
|
128
|
-
// 3.
|
|
129
|
-
registerUrlParameter('id', {
|
|
130
|
-
validator: val => assertString(val),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// 4. Define routes
|
|
176
|
+
// 3. Define routes with typed parameters
|
|
134
177
|
const routes = createRouteTable(app);
|
|
178
|
+
|
|
135
179
|
routes.get('health', async () => ({ status: 'UP' }));
|
|
136
180
|
|
|
137
|
-
|
|
181
|
+
routes.get('users/:id', {
|
|
182
|
+
$path: { id: param(toInt()) },
|
|
183
|
+
run: async ctx => ({ id: ctx.path.id }),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// 4. Error handler - catches middleware/parsing errors
|
|
138
187
|
app.use(catchAllMiddleware);
|
|
139
188
|
|
|
140
189
|
app.listen(3000);
|
|
141
190
|
```
|
|
142
191
|
|
|
192
|
+
## Process Handlers
|
|
193
|
+
|
|
194
|
+
Handle uncaught errors and graceful shutdown in one place:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { installProcessHandlers } from '@fishka/express';
|
|
198
|
+
|
|
199
|
+
installProcessHandlers({
|
|
200
|
+
// Error handlers
|
|
201
|
+
onUncaughtException: err => sendToMonitoring(err),
|
|
202
|
+
onUnhandledRejection: reason => sendToMonitoring(reason),
|
|
203
|
+
|
|
204
|
+
// Graceful shutdown
|
|
205
|
+
onShutdown: async () => {
|
|
206
|
+
await database.close();
|
|
207
|
+
await server.close();
|
|
208
|
+
},
|
|
209
|
+
shutdownTimeout: 15000, // Force exit after 15s (default: 10s)
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
143
213
|
## License
|
|
144
214
|
|
|
145
215
|
MIT
|
package/dist/cjs/api.types.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
export type
|
|
1
|
+
/** Validator function that validates and returns typed value */
|
|
2
|
+
export type TypeValidator<T> = (value: unknown) => T;
|
|
3
|
+
/** Map of param name to type validator */
|
|
4
|
+
export type TypedValidatorMap = Record<string, TypeValidator<unknown>>;
|
|
5
|
+
/** Infer validated types from validator map */
|
|
6
|
+
export type InferValidated<T extends TypedValidatorMap | undefined> = T extends TypedValidatorMap ? {
|
|
7
|
+
[K in keyof T]: ReturnType<T[K]>;
|
|
8
|
+
} : Record<string, never>;
|
|
3
9
|
export declare class HttpError extends Error {
|
|
4
10
|
readonly status: number;
|
|
5
11
|
readonly details?: Record<string, unknown> | undefined;
|
|
@@ -62,22 +68,3 @@ export interface ApiResponse<ResponseEntity = unknown> {
|
|
|
62
68
|
}
|
|
63
69
|
/** Converts an API response value into a standardized ApiResponse structure. */
|
|
64
70
|
export declare function response<T = unknown>(result: T): ApiResponse<T>;
|
|
65
|
-
/** Globally identified URL (path or query) parameter info. */
|
|
66
|
-
export interface UrlParameterInfo {
|
|
67
|
-
/** Optional global validator for this parameter. */
|
|
68
|
-
validator?: ValueAssertion<string>;
|
|
69
|
-
/** Description for documentation. */
|
|
70
|
-
description?: string;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Default documentation and validation for URL parameters.
|
|
74
|
-
* @Internal
|
|
75
|
-
*/
|
|
76
|
-
export declare const URL_PARAMETER_INFO: Record<string, UrlParameterInfo>;
|
|
77
|
-
/** Registers a new URL parameter. */
|
|
78
|
-
export declare function registerUrlParameter(name: string, info: UrlParameterInfo): void;
|
|
79
|
-
/**
|
|
80
|
-
* Asserts that the value is a registered URL parameter name.
|
|
81
|
-
* @Internal
|
|
82
|
-
*/
|
|
83
|
-
export declare function assertUrlParameter(name: unknown): asserts name is string;
|
package/dist/cjs/api.types.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.HttpError = void 0;
|
|
4
4
|
exports.assertHttp = assertHttp;
|
|
5
5
|
exports.response = response;
|
|
6
|
-
exports.registerUrlParameter = registerUrlParameter;
|
|
7
|
-
exports.assertUrlParameter = assertUrlParameter;
|
|
8
6
|
const assertions_1 = require("@fishka/assertions");
|
|
9
|
-
const http_status_codes_1 = require("./http-status-codes");
|
|
10
7
|
class HttpError extends Error {
|
|
11
8
|
constructor(status, message, details) {
|
|
12
9
|
super(message);
|
|
@@ -55,21 +52,4 @@ function assertHttp(value, status, message) {
|
|
|
55
52
|
function response(result) {
|
|
56
53
|
return { result };
|
|
57
54
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Default documentation and validation for URL parameters.
|
|
60
|
-
* @Internal
|
|
61
|
-
*/
|
|
62
|
-
exports.URL_PARAMETER_INFO = {};
|
|
63
|
-
/** Registers a new URL parameter. */
|
|
64
|
-
function registerUrlParameter(name, info) {
|
|
65
|
-
exports.URL_PARAMETER_INFO[name] = info;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Asserts that the value is a registered URL parameter name.
|
|
69
|
-
* @Internal
|
|
70
|
-
*/
|
|
71
|
-
function assertUrlParameter(name) {
|
|
72
|
-
assertHttp(typeof name === 'string', http_status_codes_1.HTTP_BAD_REQUEST, 'Url parameter name must be a string');
|
|
73
|
-
assertHttp(exports.URL_PARAMETER_INFO[name], http_status_codes_1.HTTP_BAD_REQUEST, `Invalid URL parameter: '${name}'. Please register it using 'registerUrlParameter('${name}', ...)'`);
|
|
74
|
-
}
|
|
75
55
|
//# sourceMappingURL=api.types.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.types.js","sourceRoot":"","sources":["../../src/api.types.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"api.types.js","sourceRoot":"","sources":["../../src/api.types.ts"],"names":[],"mappings":";;;AAyDA,gCAEC;AA2BD,4BAEC;AAxFD,mDAAkD;AAclD,MAAa,SAAU,SAAQ,KAAK;IAClC,YACkB,MAAc,EAC9B,OAAe,EACC,OAAiC;QAEjD,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,WAAM,GAAN,MAAM,CAAQ;QAEd,YAAO,GAAP,OAAO,CAA0B;QAGjD,qDAAqD;QACrD,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnD,CAAC;CACF;AAVD,8BAUC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,SAAgB,UAAU,CAAC,KAAc,EAAE,MAAc,EAAE,OAAe;IACxE,IAAA,yBAAY,EAAC,KAAK,EAAE,GAAG,EAAE,CAAC,IAAI,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAC5D,CAAC;AA0BD,gFAAgF;AAChF,SAAgB,QAAQ,CAAc,MAAS;IAC7C,OAAO,EAAE,MAAM,EAAE,CAAC;AACpB,CAAC"}
|
|
@@ -1,9 +1,78 @@
|
|
|
1
1
|
import { NextFunction } from 'express';
|
|
2
2
|
import { ExpressFunction, ExpressRequest, ExpressResponse } from './utils/express.utils';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* @Internal
|
|
5
|
+
* Wraps a route handler to catch and convert errors to API responses.
|
|
6
|
+
* Applied automatically to all routes registered via createRouteTable().
|
|
7
|
+
*
|
|
8
|
+
* Catches:
|
|
9
|
+
* - Errors thrown in validators ($path, $query, $body)
|
|
10
|
+
* - Errors thrown in the run() handler
|
|
11
|
+
* - Errors thrown in endpoint middlewares
|
|
12
|
+
*
|
|
13
|
+
* Logs errors to console (error level for 5xx).
|
|
14
|
+
*/
|
|
4
15
|
export declare function catchRouteErrors(fn: ExpressFunction): ExpressFunction;
|
|
5
16
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
17
|
+
* Express error-handling middleware (4 parameters) for catching errors.
|
|
18
|
+
* Can be mounted at any level - global, path-specific, or router-specific.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Global
|
|
22
|
+
* app.use(catchAllMiddleware);
|
|
23
|
+
*
|
|
24
|
+
* // Path-specific
|
|
25
|
+
* app.use('/api', catchAllMiddleware);
|
|
26
|
+
*
|
|
27
|
+
* // Router-specific
|
|
28
|
+
* const router = express.Router();
|
|
29
|
+
* router.use(catchAllMiddleware);
|
|
30
|
+
*
|
|
31
|
+
* Catches:
|
|
32
|
+
* - Errors from Express middleware (passed via next(error))
|
|
33
|
+
* - JSON parsing errors (SyntaxError) - returns 400
|
|
34
|
+
* - Any errors that escape catchRouteErrors
|
|
35
|
+
*
|
|
36
|
+
* Note: Individual routes are already wrapped with catchRouteErrors(),
|
|
37
|
+
* so this middleware primarily catches middleware and parsing errors.
|
|
8
38
|
*/
|
|
9
39
|
export declare function catchAllMiddleware(error: unknown, _: ExpressRequest, res: ExpressResponse, next: NextFunction): Promise<void>;
|
|
40
|
+
/** Options for installProcessHandlers(). */
|
|
41
|
+
export interface ProcessHandlersOptions {
|
|
42
|
+
/** Custom handler for uncaught exceptions. Called before default logging. */
|
|
43
|
+
onUncaughtException?: (error: Error) => void;
|
|
44
|
+
/** Custom handler for unhandled promise rejections. Called before default logging. */
|
|
45
|
+
onUnhandledRejection?: (reason: unknown) => void;
|
|
46
|
+
/** Async cleanup function called on shutdown signals. */
|
|
47
|
+
onShutdown?: () => Promise<void>;
|
|
48
|
+
/** Force exit timeout in ms if shutdown hangs. Default: 10000 */
|
|
49
|
+
shutdownTimeout?: number;
|
|
50
|
+
/** Signals to handle for graceful shutdown. Default: ['SIGTERM', 'SIGINT'] */
|
|
51
|
+
shutdownSignals?: NodeJS.Signals[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Installs process-level handlers for errors and graceful shutdown.
|
|
55
|
+
* Call once at application startup, before app.listen().
|
|
56
|
+
*
|
|
57
|
+
* Error handling:
|
|
58
|
+
* - Uncaught exceptions (sync throws outside Express middleware)
|
|
59
|
+
* - Unhandled promise rejections (forgotten await, missing .catch())
|
|
60
|
+
*
|
|
61
|
+
* Graceful shutdown:
|
|
62
|
+
* - SIGTERM (Docker/K8s/systemd stop)
|
|
63
|
+
* - SIGINT (Ctrl+C)
|
|
64
|
+
* - Timeout protection (force exit if shutdown hangs)
|
|
65
|
+
* - Double-shutdown prevention
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* installProcessHandlers({
|
|
69
|
+
* onUncaughtException: (error) => sendToMonitoring(error),
|
|
70
|
+
* onUnhandledRejection: (reason) => sendToMonitoring(reason),
|
|
71
|
+
* onShutdown: async () => {
|
|
72
|
+
* await database.close();
|
|
73
|
+
* await server.close();
|
|
74
|
+
* },
|
|
75
|
+
* shutdownTimeout: 15000,
|
|
76
|
+
* });
|
|
77
|
+
*/
|
|
78
|
+
export declare function installProcessHandlers(options?: ProcessHandlersOptions): void;
|
|
@@ -2,11 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.catchRouteErrors = catchRouteErrors;
|
|
4
4
|
exports.catchAllMiddleware = catchAllMiddleware;
|
|
5
|
+
exports.installProcessHandlers = installProcessHandlers;
|
|
5
6
|
const assertions_1 = require("@fishka/assertions");
|
|
6
7
|
const api_types_1 = require("./api.types");
|
|
7
8
|
const http_status_codes_1 = require("./http-status-codes");
|
|
8
9
|
const thread_local_storage_1 = require("./thread-local/thread-local-storage");
|
|
9
10
|
const conversion_utils_1 = require("./utils/conversion.utils");
|
|
11
|
+
/**
|
|
12
|
+
* Converts any error into a standardized API response format.
|
|
13
|
+
* - HttpError: Uses the error's status code and message
|
|
14
|
+
* - Other errors: Returns 500 with the error message or 'Internal error'
|
|
15
|
+
* Attaches requestId from thread-local storage if available.
|
|
16
|
+
*/
|
|
10
17
|
function buildApiResponse(error) {
|
|
11
18
|
const tls = (0, thread_local_storage_1.getRequestLocalStorage)();
|
|
12
19
|
const requestId = tls?.requestId;
|
|
@@ -32,7 +39,18 @@ function buildApiResponse(error) {
|
|
|
32
39
|
}
|
|
33
40
|
return response;
|
|
34
41
|
}
|
|
35
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* @Internal
|
|
44
|
+
* Wraps a route handler to catch and convert errors to API responses.
|
|
45
|
+
* Applied automatically to all routes registered via createRouteTable().
|
|
46
|
+
*
|
|
47
|
+
* Catches:
|
|
48
|
+
* - Errors thrown in validators ($path, $query, $body)
|
|
49
|
+
* - Errors thrown in the run() handler
|
|
50
|
+
* - Errors thrown in endpoint middlewares
|
|
51
|
+
*
|
|
52
|
+
* Logs errors to console (error level for 5xx).
|
|
53
|
+
*/
|
|
36
54
|
function catchRouteErrors(fn) {
|
|
37
55
|
return async (req, res, next) => {
|
|
38
56
|
try {
|
|
@@ -43,17 +61,33 @@ function catchRouteErrors(fn) {
|
|
|
43
61
|
if (apiResponse.status >= http_status_codes_1.HTTP_INTERNAL_SERVER_ERROR) {
|
|
44
62
|
console.error(`catchRouteErrors: ${req.path}`, error);
|
|
45
63
|
}
|
|
46
|
-
else {
|
|
47
|
-
console.log(`catchRouteErrors: ${req.path}`, error);
|
|
48
|
-
}
|
|
49
64
|
res.status(apiResponse.status);
|
|
50
65
|
res.send(apiResponse);
|
|
51
66
|
}
|
|
52
67
|
};
|
|
53
68
|
}
|
|
54
69
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
70
|
+
* Express error-handling middleware (4 parameters) for catching errors.
|
|
71
|
+
* Can be mounted at any level - global, path-specific, or router-specific.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Global
|
|
75
|
+
* app.use(catchAllMiddleware);
|
|
76
|
+
*
|
|
77
|
+
* // Path-specific
|
|
78
|
+
* app.use('/api', catchAllMiddleware);
|
|
79
|
+
*
|
|
80
|
+
* // Router-specific
|
|
81
|
+
* const router = express.Router();
|
|
82
|
+
* router.use(catchAllMiddleware);
|
|
83
|
+
*
|
|
84
|
+
* Catches:
|
|
85
|
+
* - Errors from Express middleware (passed via next(error))
|
|
86
|
+
* - JSON parsing errors (SyntaxError) - returns 400
|
|
87
|
+
* - Any errors that escape catchRouteErrors
|
|
88
|
+
*
|
|
89
|
+
* Note: Individual routes are already wrapped with catchRouteErrors(),
|
|
90
|
+
* so this middleware primarily catches middleware and parsing errors.
|
|
57
91
|
*/
|
|
58
92
|
async function catchAllMiddleware(error, _, res, next) {
|
|
59
93
|
if (!error) {
|
|
@@ -63,9 +97,75 @@ async function catchAllMiddleware(error, _, res, next) {
|
|
|
63
97
|
// Report as critical. This kind of error should never happen.
|
|
64
98
|
console.error('catchAllMiddleware:', (0, assertions_1.getMessageFromError)(error));
|
|
65
99
|
const apiResponse = error instanceof SyntaxError // JSON body parsing error.
|
|
66
|
-
? buildApiResponse(
|
|
100
|
+
? buildApiResponse(new api_types_1.HttpError(http_status_codes_1.HTTP_BAD_REQUEST, `Failed to parse request: ${error.message}`))
|
|
67
101
|
: buildApiResponse(error);
|
|
68
102
|
res.status(apiResponse.status);
|
|
69
103
|
res.send(apiResponse);
|
|
70
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Installs process-level handlers for errors and graceful shutdown.
|
|
107
|
+
* Call once at application startup, before app.listen().
|
|
108
|
+
*
|
|
109
|
+
* Error handling:
|
|
110
|
+
* - Uncaught exceptions (sync throws outside Express middleware)
|
|
111
|
+
* - Unhandled promise rejections (forgotten await, missing .catch())
|
|
112
|
+
*
|
|
113
|
+
* Graceful shutdown:
|
|
114
|
+
* - SIGTERM (Docker/K8s/systemd stop)
|
|
115
|
+
* - SIGINT (Ctrl+C)
|
|
116
|
+
* - Timeout protection (force exit if shutdown hangs)
|
|
117
|
+
* - Double-shutdown prevention
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* installProcessHandlers({
|
|
121
|
+
* onUncaughtException: (error) => sendToMonitoring(error),
|
|
122
|
+
* onUnhandledRejection: (reason) => sendToMonitoring(reason),
|
|
123
|
+
* onShutdown: async () => {
|
|
124
|
+
* await database.close();
|
|
125
|
+
* await server.close();
|
|
126
|
+
* },
|
|
127
|
+
* shutdownTimeout: 15000,
|
|
128
|
+
* });
|
|
129
|
+
*/
|
|
130
|
+
function installProcessHandlers(options) {
|
|
131
|
+
// Error handlers
|
|
132
|
+
process.on('uncaughtException', (error) => {
|
|
133
|
+
options?.onUncaughtException?.(error);
|
|
134
|
+
console.error('CRITICAL - Uncaught Exception:', error);
|
|
135
|
+
});
|
|
136
|
+
process.on('unhandledRejection', (reason) => {
|
|
137
|
+
options?.onUnhandledRejection?.(reason);
|
|
138
|
+
console.error('CRITICAL - Unhandled Rejection:', reason);
|
|
139
|
+
});
|
|
140
|
+
// Graceful shutdown
|
|
141
|
+
const onShutdown = options?.onShutdown;
|
|
142
|
+
if (onShutdown) {
|
|
143
|
+
let isShuttingDown = false;
|
|
144
|
+
const signals = options?.shutdownSignals ?? ['SIGTERM', 'SIGINT'];
|
|
145
|
+
const timeout = options?.shutdownTimeout ?? 10000;
|
|
146
|
+
const shutdown = async (signal) => {
|
|
147
|
+
if (isShuttingDown)
|
|
148
|
+
return;
|
|
149
|
+
isShuttingDown = true;
|
|
150
|
+
console.log(`${signal} received, shutting down gracefully...`);
|
|
151
|
+
const timer = setTimeout(() => {
|
|
152
|
+
console.error('Shutdown timeout, forcing exit');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}, timeout);
|
|
155
|
+
try {
|
|
156
|
+
await onShutdown();
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
console.error('Shutdown error:', err);
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
for (const signal of signals) {
|
|
167
|
+
process.on(signal, () => shutdown(signal));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
71
171
|
//# sourceMappingURL=error-handling.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handling.js","sourceRoot":"","sources":["../../src/error-handling.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"error-handling.js","sourceRoot":"","sources":["../../src/error-handling.ts"],"names":[],"mappings":"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"}
|
package/dist/cjs/index.d.ts
CHANGED
package/dist/cjs/index.js
CHANGED
|
@@ -29,4 +29,5 @@ __exportStar(require("./router"), exports);
|
|
|
29
29
|
__exportStar(require("./thread-local/thread-local-storage"), exports);
|
|
30
30
|
__exportStar(require("./thread-local/thread-local-storage-middleware"), exports);
|
|
31
31
|
__exportStar(require("./utils/express.utils"), exports);
|
|
32
|
+
__exportStar(require("./utils/type-validators"), exports);
|
|
32
33
|
//# sourceMappingURL=index.js.map
|
package/dist/cjs/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,8CAA4B;AAC5B,uDAAqC;AACrC,oDAAkC;AAClC,oDAAkC;AAClC,8DAA4C;AAC5C,2CAAyB;AACzB,mDAAiC;AACjC,sDAAoC;AACpC,sEAAoD;AACpD,gEAA8C;AAC9C,gDAA8B;AAC9B,2CAAyB;AACzB,sEAAoD;AACpD,iFAA+D;AAC/D,wDAAsC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,8CAA4B;AAC5B,uDAAqC;AACrC,oDAAkC;AAClC,oDAAkC;AAClC,8DAA4C;AAC5C,2CAAyB;AACzB,mDAAiC;AACjC,sDAAoC;AACpC,sEAAoD;AACpD,gEAA8C;AAC9C,gDAA8B;AAC9B,2CAAyB;AACzB,sEAAoD;AACpD,iFAA+D;AAC/D,wDAAsC;AACtC,0DAAwC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TypedValidatorMap } from './api.types';
|
|
1
2
|
import { DeleteEndpoint, GetEndpoint, PatchEndpoint, PostEndpoint, PutEndpoint, RequestContext, ResponseOrValue } from './router';
|
|
2
3
|
import { ExpressRouter } from './utils/express.utils';
|
|
3
4
|
/**
|
|
@@ -7,13 +8,20 @@ import { ExpressRouter } from './utils/express.utils';
|
|
|
7
8
|
export declare class RouteTable {
|
|
8
9
|
private readonly app;
|
|
9
10
|
constructor(app: ExpressRouter);
|
|
10
|
-
|
|
11
|
-
get<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
/** Register GET endpoint with full type inference for path/query params. */
|
|
12
|
+
get<Result, PathParams extends TypedValidatorMap = TypedValidatorMap, QueryParams extends TypedValidatorMap = TypedValidatorMap>(path: string, endpoint: GetEndpoint<Result, PathParams, QueryParams>): this;
|
|
13
|
+
/** Register GET endpoint with function shorthand. */
|
|
14
|
+
get<Result>(path: string, run: (ctx: RequestContext) => ResponseOrValue<Result> | Promise<ResponseOrValue<Result>>): this;
|
|
15
|
+
/** Register POST endpoint with full type inference for path/query params. */
|
|
16
|
+
post<Body, Result = void, PathParams extends TypedValidatorMap = TypedValidatorMap, QueryParams extends TypedValidatorMap = TypedValidatorMap>(path: string, endpoint: PostEndpoint<Body, Result, PathParams, QueryParams>): this;
|
|
17
|
+
/** Register PATCH endpoint with full type inference for path/query params. */
|
|
18
|
+
patch<Body, Result = void, PathParams extends TypedValidatorMap = TypedValidatorMap, QueryParams extends TypedValidatorMap = TypedValidatorMap>(path: string, endpoint: PatchEndpoint<Body, Result, PathParams, QueryParams>): this;
|
|
19
|
+
/** Register PUT endpoint with full type inference for path/query params. */
|
|
20
|
+
put<Body, Result = void, PathParams extends TypedValidatorMap = TypedValidatorMap, QueryParams extends TypedValidatorMap = TypedValidatorMap>(path: string, endpoint: PutEndpoint<Body, Result, PathParams, QueryParams>): this;
|
|
21
|
+
/** Register DELETE endpoint with full endpoint object. */
|
|
22
|
+
delete<PathParams extends TypedValidatorMap = TypedValidatorMap, QueryParams extends TypedValidatorMap = TypedValidatorMap>(path: string, endpoint: DeleteEndpoint<PathParams, QueryParams>): this;
|
|
23
|
+
/** Register DELETE endpoint with function shorthand. */
|
|
24
|
+
delete(path: string, run: (ctx: RequestContext) => void | Promise<void>): this;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* Factory function to create a new route table.
|
package/dist/cjs/route-table.js
CHANGED
|
@@ -16,14 +16,17 @@ class RouteTable {
|
|
|
16
16
|
(0, router_1.mountGet)(this.app, path, endpoint);
|
|
17
17
|
return this;
|
|
18
18
|
}
|
|
19
|
+
/** Register POST endpoint with full type inference for path/query params. */
|
|
19
20
|
post(path, endpoint) {
|
|
20
21
|
(0, router_1.mountPost)(this.app, path, endpoint);
|
|
21
22
|
return this;
|
|
22
23
|
}
|
|
24
|
+
/** Register PATCH endpoint with full type inference for path/query params. */
|
|
23
25
|
patch(path, endpoint) {
|
|
24
26
|
(0, router_1.mountPatch)(this.app, path, endpoint);
|
|
25
27
|
return this;
|
|
26
28
|
}
|
|
29
|
+
/** Register PUT endpoint with full type inference for path/query params. */
|
|
27
30
|
put(path, endpoint) {
|
|
28
31
|
(0, router_1.mountPut)(this.app, path, endpoint);
|
|
29
32
|
return this;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-table.js","sourceRoot":"","sources":["../../src/route-table.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"route-table.js","sourceRoot":"","sources":["../../src/route-table.ts"],"names":[],"mappings":";;;AA6GA,4CAEC;AA9GD,qCAakB;AAGlB;;;GAGG;AACH,MAAa,UAAU;IACrB,YAA6B,GAAkB;QAAlB,QAAG,GAAH,GAAG,CAAe;IAAG,CAAC;IAYnD,GAAG,CAKD,IAAY,EACZ,aAEyF;QAEzF,MAAM,QAAQ,GAAG,OAAO,aAAa,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;QAC9F,IAAA,iBAAQ,EAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,QAAgC,CAAC,CAAC;QAC3D,OAAO,IAAI,CAAC;IACd,CAAC;IAED,6EAA6E;IAC7E,IAAI,CAKF,IAAY,EAAE,QAA6D;QAC3E,IAAA,kBAAS,EAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,QAA4C,CAAC,CAAC;QACxE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,8EAA8E;IAC9E,KAAK,CAKH,IAAY,EAAE,QAA8D;QAC5E,IAAA,mBAAU,EAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,QAA6C,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;IAED,4EAA4E;IAC5E,GAAG,CAKD,IAAY,EAAE,QAA4D;QAC1E,IAAA,iBAAQ,EAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,QAA2C,CAAC,CAAC;QACtE,OAAO,IAAI,CAAC;IACd,CAAC;IAWD,MAAM,CAIJ,IAAY,EACZ,aAAwG;QAExG,MAAM,QAAQ,GAAG,OAAO,aAAa,KAAK,UAAU,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC;QAC9F,IAAA,oBAAW,EAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,QAA0B,CAAC,CAAC;QACxD,OAAO,IAAI,CAAC;IACd,CAAC;CACF;AAjFD,gCAiFC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,GAAkB;IACjD,OAAO,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC"}
|