ts-typed-api 0.2.21 → 0.2.23
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/.kilo/plans/1779977869703-shiny-squid.md +49 -0
- package/dist/definition.d.ts +2 -0
- package/dist/handler.d.ts +2 -2
- package/dist/handler.js +10 -2
- package/dist/index.d.ts +1 -1
- package/dist/object-handlers.d.ts +1 -1
- package/dist/object-handlers.js +2 -2
- package/examples/simple/definitions.ts +16 -0
- package/package.json +1 -1
- package/src/definition.ts +10 -0
- package/src/handler.ts +13 -3
- package/src/index.ts +1 -1
- package/src/object-handlers.ts +3 -2
- package/tests/error-handler.test.ts +146 -0
- package/tests/setup.ts +5 -1
- package/tests/simple-api.test.ts +69 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Fix PATCH/DELETE Body Parsing + Add Tests
|
|
2
|
+
|
|
3
|
+
## Problem
|
|
4
|
+
In `src/handler.ts:400`, the Express handler only validates body for `POST` and `PUT`. `PATCH` and `DELETE` bypass Zod validation entirely, receiving raw `expressReq.body`. This is inconsistent with:
|
|
5
|
+
- The Hono handler (`src/hono-cloudflare-workers.ts:377`) which validates all four methods
|
|
6
|
+
- The type system (`MethodsWithBody = 'POST' | 'PUT' | 'DELETE' | 'PATCH'` in `definition.ts`)
|
|
7
|
+
|
|
8
|
+
## Plan
|
|
9
|
+
|
|
10
|
+
### 1. Fix `src/handler.ts` — line 400
|
|
11
|
+
Change:
|
|
12
|
+
```typescript
|
|
13
|
+
const parsedBody = (method === 'POST' || method === 'PUT') && ('body' in routeDefinition && routeDefinition.body)
|
|
14
|
+
```
|
|
15
|
+
To:
|
|
16
|
+
```typescript
|
|
17
|
+
const parsedBody = (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH') && ('body' in routeDefinition && routeDefinition.body)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. Add PATCH endpoint to test setup (`tests/setup.ts`)
|
|
21
|
+
Add to `SimplePublicApiDefinition` (in `examples/simple/definitions.ts`) a new `patch` route with a body schema that validates fields:
|
|
22
|
+
```typescript
|
|
23
|
+
patch: {
|
|
24
|
+
method: 'PATCH',
|
|
25
|
+
path: '/patch',
|
|
26
|
+
body: z.object({
|
|
27
|
+
name: z.string().min(1),
|
|
28
|
+
value: z.number().min(0)
|
|
29
|
+
}),
|
|
30
|
+
responses: CreateResponses({
|
|
31
|
+
200: z.object({ message: z.string(), name: z.string(), value: z.number() })
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Add a corresponding handler in `simplePublicHandlers` in `tests/setup.ts` that returns the parsed body data.
|
|
37
|
+
|
|
38
|
+
### 3. Add tests (`tests/simple-api.test.ts`)
|
|
39
|
+
Add a `describe('PATCH body validation')` block inside the existing `describe('Public API')`:
|
|
40
|
+
- **Success case**: Send valid body, expect 200 with echoed data
|
|
41
|
+
- **Validation failure case**: Send body with missing fields or wrong types, expect 422 with error details
|
|
42
|
+
- **Extra property rejection**: Send body with an unknown property, expect 422 (strict mode)
|
|
43
|
+
|
|
44
|
+
These tests will run against both Express and Hono via the existing `describe.each(['Express', SIMPLE_PORT], ['Hono', HONO_PORT])`.
|
|
45
|
+
|
|
46
|
+
### 4. Verify
|
|
47
|
+
Run `npm test` to confirm:
|
|
48
|
+
- Existing tests still pass
|
|
49
|
+
- New PATCH validation tests pass for both Express and Hono
|
package/dist/definition.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z, ZodTypeAny } from 'zod';
|
|
2
|
+
import express from 'express';
|
|
2
3
|
export declare class TsTypeMarker<T> {
|
|
3
4
|
readonly _isTsTypeMarker = true;
|
|
4
5
|
readonly _type: T;
|
|
@@ -17,6 +18,7 @@ declare const unifiedErrorSchema: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
|
17
18
|
message: z.ZodString;
|
|
18
19
|
}, z.core.$strip>>>;
|
|
19
20
|
export type UnifiedError = z.infer<typeof unifiedErrorSchema>;
|
|
21
|
+
export type ErrorHandler = (error: unknown, routeDefinition: RouteSchema, method: string, path: string, expressRes: express.Response) => boolean | void;
|
|
20
22
|
declare const errorUnifiedResponseSchema: z.ZodObject<{
|
|
21
23
|
error: z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
22
24
|
field: z.ZodString;
|
package/dist/handler.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiDefinitionSchema } from "./definition";
|
|
1
|
+
import { ApiDefinitionSchema, ErrorHandler } from "./definition";
|
|
2
2
|
import { createRouteHandler } from "./router";
|
|
3
3
|
import { MiddlewareResponse } from "./object-handlers";
|
|
4
4
|
import express from "express";
|
|
@@ -15,4 +15,4 @@ export type EndpointMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionS
|
|
|
15
15
|
}[keyof TDef['endpoints']]) => void | Promise<void>;
|
|
16
16
|
export declare function registerRouteHandlers<TDef extends ApiDefinitionSchema>(app: express.Express, apiDefinition: TDef, // Pass the actual API definition object
|
|
17
17
|
routeHandlers: Array<SpecificRouteHandler<TDef>>, // Use the generic handler type
|
|
18
|
-
middlewares?: EndpointMiddleware<TDef>[]): void;
|
|
18
|
+
middlewares?: EndpointMiddleware<TDef>[], errorHandler?: ErrorHandler): void;
|
package/dist/handler.js
CHANGED
|
@@ -306,7 +306,7 @@ function createFileUploadMiddleware(config) {
|
|
|
306
306
|
// Register route handlers with Express, now generic over TDef
|
|
307
307
|
function registerRouteHandlers(app, apiDefinition, // Pass the actual API definition object
|
|
308
308
|
routeHandlers, // Use the generic handler type
|
|
309
|
-
middlewares) {
|
|
309
|
+
middlewares, errorHandler) {
|
|
310
310
|
routeHandlers.forEach((specificHandlerIterationItem) => {
|
|
311
311
|
const { domain, routeKey, handler } = specificHandlerIterationItem; // Use 'as any' for simplicity in destructuring union
|
|
312
312
|
const currentDomain = domain;
|
|
@@ -339,7 +339,7 @@ middlewares) {
|
|
|
339
339
|
const parsedQuery = ('query' in routeDefinition && routeDefinition.query)
|
|
340
340
|
? routeDefinition.query.parse(preprocessedQuery)
|
|
341
341
|
: preprocessedQuery;
|
|
342
|
-
const parsedBody = (method === 'POST' || method === 'PUT') && ('body' in routeDefinition && routeDefinition.body)
|
|
342
|
+
const parsedBody = (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') && ('body' in routeDefinition && routeDefinition.body)
|
|
343
343
|
? routeDefinition.body.parse(expressReq.body)
|
|
344
344
|
: expressReq.body;
|
|
345
345
|
// Construct TypedRequest using TDef, currentDomain, currentRouteKey
|
|
@@ -465,6 +465,14 @@ middlewares) {
|
|
|
465
465
|
await specificHandlerFn(finalTypedReq, typedExpressRes);
|
|
466
466
|
}
|
|
467
467
|
catch (error) {
|
|
468
|
+
// Check if custom error handler is provided
|
|
469
|
+
if (errorHandler) {
|
|
470
|
+
const handled = errorHandler(error, routeDefinition, method, path, expressRes);
|
|
471
|
+
if (handled) {
|
|
472
|
+
return; // Error was handled by custom handler
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// Default error handling
|
|
468
476
|
if (error instanceof zod_1.z.ZodError) {
|
|
469
477
|
const mappedErrors = error.issues.map(err => {
|
|
470
478
|
let errorType = 'general';
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { ApiClient, FetchHttpClientAdapter } from './client';
|
|
2
2
|
export { generateOpenApiSpec } from './openapi';
|
|
3
3
|
export { generateOpenApiSpec as generateOpenApiSpec2 } from './openapi-self';
|
|
4
|
-
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './definition';
|
|
4
|
+
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema, ErrorHandler } from './definition';
|
|
5
5
|
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo, MiddlewareResponse } from './object-handlers';
|
|
6
6
|
export { File as UploadedFile } from './router';
|
|
7
7
|
export { z as ZodSchema } from 'zod';
|
|
@@ -31,6 +31,6 @@ export type ObjectHandlers<TDef extends ApiDefinitionSchema, Ctx extends Record<
|
|
|
31
31
|
[TRouteKey in keyof TDef['endpoints'][TDomain]]: HandlerFunction<TDef, TDomain, TRouteKey, Ctx>;
|
|
32
32
|
};
|
|
33
33
|
};
|
|
34
|
-
export declare function RegisterHandlers<TDef extends ApiDefinitionSchema, Ctx extends Record<string, any> = Record<string, any>>(app: express.Express, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef, Ctx>, middlewares?: AnyMiddleware<TDef>[]): void;
|
|
34
|
+
export declare function RegisterHandlers<TDef extends ApiDefinitionSchema, Ctx extends Record<string, any> = Record<string, any>>(app: express.Express, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef, Ctx>, middlewares?: AnyMiddleware<TDef>[], errorHandler?: import('./definition').ErrorHandler): void;
|
|
35
35
|
export declare function makeObjectHandlerRegistrar<TDef extends ApiDefinitionSchema>(apiDefinition: TDef): (app: express.Express, objectHandlers: ObjectHandlers<TDef>, middlewares?: EndpointMiddleware<TDef>[]) => void;
|
|
36
36
|
export {};
|
package/dist/object-handlers.js
CHANGED
|
@@ -27,7 +27,7 @@ function transformObjectHandlersToArray(objectHandlers) {
|
|
|
27
27
|
return handlerArray;
|
|
28
28
|
}
|
|
29
29
|
// Main utility function that registers object-based handlers
|
|
30
|
-
function RegisterHandlers(app, apiDefinition, objectHandlers, middlewares) {
|
|
30
|
+
function RegisterHandlers(app, apiDefinition, objectHandlers, middlewares, errorHandler) {
|
|
31
31
|
const handlerArray = transformObjectHandlersToArray(objectHandlers);
|
|
32
32
|
// Convert AnyMiddleware to EndpointMiddleware by checking function arity
|
|
33
33
|
const endpointMiddlewares = middlewares?.map(middleware => {
|
|
@@ -43,7 +43,7 @@ function RegisterHandlers(app, apiDefinition, objectHandlers, middlewares) {
|
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
}) || [];
|
|
46
|
-
(0, handler_1.registerRouteHandlers)(app, apiDefinition, handlerArray, endpointMiddlewares);
|
|
46
|
+
(0, handler_1.registerRouteHandlers)(app, apiDefinition, handlerArray, endpointMiddlewares, errorHandler);
|
|
47
47
|
}
|
|
48
48
|
// Factory function to create a typed handler registrar for a specific API definition
|
|
49
49
|
function makeObjectHandlerRegistrar(apiDefinition) {
|
|
@@ -77,6 +77,22 @@ export const PublicApiDefinition = CreateApiDefinition({
|
|
|
77
77
|
200: z.string() // Raw SSE data
|
|
78
78
|
})
|
|
79
79
|
},
|
|
80
|
+
patchResource: {
|
|
81
|
+
method: 'PATCH',
|
|
82
|
+
path: '/patch-resource',
|
|
83
|
+
description: 'PATCH endpoint to test body validation for PATCH requests',
|
|
84
|
+
body: z.object({
|
|
85
|
+
name: z.string().min(1, 'Name is required'),
|
|
86
|
+
value: z.number().int().min(0, 'Value must be non-negative')
|
|
87
|
+
}),
|
|
88
|
+
responses: CreateResponses({
|
|
89
|
+
200: z.object({
|
|
90
|
+
message: z.string(),
|
|
91
|
+
name: z.string(),
|
|
92
|
+
value: z.number()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
},
|
|
80
96
|
disconnectTest: {
|
|
81
97
|
method: 'GET',
|
|
82
98
|
path: '/disconnect-test',
|
package/package.json
CHANGED
package/src/definition.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z, ZodTypeAny, ZodType } from 'zod';
|
|
2
|
+
import express from 'express';
|
|
2
3
|
|
|
3
4
|
// Marker class for raw TypeScript types
|
|
4
5
|
export class TsTypeMarker<T> {
|
|
@@ -30,6 +31,15 @@ const errorDetailSchema = z.object({
|
|
|
30
31
|
const unifiedErrorSchema = z.array(errorDetailSchema).nullable(); // Nullable if no errors
|
|
31
32
|
export type UnifiedError = z.infer<typeof unifiedErrorSchema>;
|
|
32
33
|
|
|
34
|
+
// Type for custom error handler function
|
|
35
|
+
export type ErrorHandler = (
|
|
36
|
+
error: unknown,
|
|
37
|
+
routeDefinition: RouteSchema,
|
|
38
|
+
method: string,
|
|
39
|
+
path: string,
|
|
40
|
+
expressRes: express.Response
|
|
41
|
+
) => boolean | void; // Return true if handled, void/null to use default
|
|
42
|
+
|
|
33
43
|
// Helper function to create the success-specific unified response schema
|
|
34
44
|
// This wraps the original data schema with a 'data' field and sets 'error' to null.
|
|
35
45
|
function createSuccessUnifiedResponseSchema<TData extends ZodTypeAny>(dataSchema: TData) {
|
package/src/handler.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ApiDefinitionSchema, RouteSchema, UnifiedError, FileUploadConfig } from "./definition";
|
|
2
|
+
import { ApiDefinitionSchema, RouteSchema, UnifiedError, FileUploadConfig, ErrorHandler } from "./definition";
|
|
3
3
|
import { createRouteHandler, TypedRequest, TypedResponse } from "./router";
|
|
4
4
|
import { MiddlewareResponse } from "./object-handlers";
|
|
5
5
|
import express from "express";
|
|
@@ -350,7 +350,8 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
350
350
|
app: express.Express,
|
|
351
351
|
apiDefinition: TDef, // Pass the actual API definition object
|
|
352
352
|
routeHandlers: Array<SpecificRouteHandler<TDef>>, // Use the generic handler type
|
|
353
|
-
middlewares?: EndpointMiddleware<TDef>[]
|
|
353
|
+
middlewares?: EndpointMiddleware<TDef>[],
|
|
354
|
+
errorHandler?: ErrorHandler
|
|
354
355
|
) {
|
|
355
356
|
routeHandlers.forEach((specificHandlerIterationItem) => {
|
|
356
357
|
const { domain, routeKey, handler } = specificHandlerIterationItem as any; // Use 'as any' for simplicity in destructuring union
|
|
@@ -396,7 +397,7 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
396
397
|
? (routeDefinition.query as z.ZodTypeAny).parse(preprocessedQuery)
|
|
397
398
|
: preprocessedQuery;
|
|
398
399
|
|
|
399
|
-
const parsedBody = (method === 'POST' || method === 'PUT') && ('body' in routeDefinition && routeDefinition.body)
|
|
400
|
+
const parsedBody = (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE') && ('body' in routeDefinition && routeDefinition.body)
|
|
400
401
|
? (routeDefinition.body as z.ZodTypeAny).parse(expressReq.body)
|
|
401
402
|
: expressReq.body;
|
|
402
403
|
|
|
@@ -541,6 +542,15 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
541
542
|
await specificHandlerFn(finalTypedReq, typedExpressRes);
|
|
542
543
|
|
|
543
544
|
} catch (error) {
|
|
545
|
+
// Check if custom error handler is provided
|
|
546
|
+
if (errorHandler) {
|
|
547
|
+
const handled = errorHandler(error, routeDefinition, method, path, expressRes);
|
|
548
|
+
if (handled) {
|
|
549
|
+
return; // Error was handled by custom handler
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Default error handling
|
|
544
554
|
if (error instanceof z.ZodError) {
|
|
545
555
|
const mappedErrors: UnifiedError = error.issues.map(err => {
|
|
546
556
|
let errorType: 'param' | 'query' | 'body' | 'general' = 'general';
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { ApiClient, FetchHttpClientAdapter } from './client';
|
|
2
2
|
export { generateOpenApiSpec } from './openapi'
|
|
3
3
|
export { generateOpenApiSpec as generateOpenApiSpec2 } from './openapi-self'
|
|
4
|
-
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './definition';
|
|
4
|
+
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema, ErrorHandler } from './definition';
|
|
5
5
|
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo, MiddlewareResponse } from './object-handlers';
|
|
6
6
|
export { File as UploadedFile } from './router';
|
|
7
7
|
export { z as ZodSchema } from 'zod';
|
package/src/object-handlers.ts
CHANGED
|
@@ -125,7 +125,8 @@ export function RegisterHandlers<
|
|
|
125
125
|
app: express.Express,
|
|
126
126
|
apiDefinition: TDef,
|
|
127
127
|
objectHandlers: ObjectHandlers<TDef, Ctx>,
|
|
128
|
-
middlewares?: AnyMiddleware<TDef>[]
|
|
128
|
+
middlewares?: AnyMiddleware<TDef>[],
|
|
129
|
+
errorHandler?: import('./definition').ErrorHandler
|
|
129
130
|
): void {
|
|
130
131
|
const handlerArray = transformObjectHandlersToArray(objectHandlers);
|
|
131
132
|
|
|
@@ -143,7 +144,7 @@ export function RegisterHandlers<
|
|
|
143
144
|
}
|
|
144
145
|
}) || [];
|
|
145
146
|
|
|
146
|
-
registerRouteHandlers(app, apiDefinition, handlerArray, endpointMiddlewares);
|
|
147
|
+
registerRouteHandlers(app, apiDefinition, handlerArray, endpointMiddlewares, errorHandler);
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
// Factory function to create a typed handler registrar for a specific API definition
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll, afterAll } from '@jest/globals';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { Server } from 'http';
|
|
4
|
+
import { RegisterHandlers, CreateApiDefinition, CreateResponses, ErrorHandler } from '../src';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const ErrorHandlerTestApiDefinition = CreateApiDefinition({
|
|
8
|
+
prefix: '/api',
|
|
9
|
+
endpoints: {
|
|
10
|
+
test: {
|
|
11
|
+
success: {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
path: '/success',
|
|
14
|
+
responses: CreateResponses({
|
|
15
|
+
200: z.object({ message: z.string() })
|
|
16
|
+
})
|
|
17
|
+
},
|
|
18
|
+
validationError: {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
path: '/validation-error',
|
|
21
|
+
body: z.object({
|
|
22
|
+
name: z.string().min(3, 'Name must be at least 3 characters'),
|
|
23
|
+
email: z.string().email('Invalid email format')
|
|
24
|
+
}),
|
|
25
|
+
responses: CreateResponses({
|
|
26
|
+
200: z.object({ message: z.string() })
|
|
27
|
+
})
|
|
28
|
+
},
|
|
29
|
+
customError: {
|
|
30
|
+
method: 'GET',
|
|
31
|
+
path: '/custom-error',
|
|
32
|
+
responses: CreateResponses({
|
|
33
|
+
200: z.object({ message: z.string() })
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const testHandlers = {
|
|
41
|
+
test: {
|
|
42
|
+
success: async (req: any, res: any) => {
|
|
43
|
+
res.respond(200, { message: 'success' });
|
|
44
|
+
},
|
|
45
|
+
validationError: async () => {
|
|
46
|
+
// This will cause a Zod validation error - validation happens before handler
|
|
47
|
+
throw new Error('Should not reach handler');
|
|
48
|
+
},
|
|
49
|
+
customError: async (req: any, res: any) => {
|
|
50
|
+
// Throw a custom error to test error handler
|
|
51
|
+
throw new Error('Custom application error');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe('Error Handler Tests', () => {
|
|
57
|
+
let server: Server;
|
|
58
|
+
let port: number;
|
|
59
|
+
|
|
60
|
+
beforeAll(async () => {
|
|
61
|
+
port = 3009; // Use a different port
|
|
62
|
+
const app = express();
|
|
63
|
+
app.use(express.json());
|
|
64
|
+
|
|
65
|
+
// Custom error handler
|
|
66
|
+
const customErrorHandler: ErrorHandler = (error, routeDefinition, method, path, expressRes) => {
|
|
67
|
+
if (error instanceof z.ZodError) {
|
|
68
|
+
// Custom Zod error handling
|
|
69
|
+
const customErrors = error.issues.map(issue => ({
|
|
70
|
+
field: issue.path.join('.'),
|
|
71
|
+
message: `Custom: ${issue.message}`,
|
|
72
|
+
type: issue.path[0] === 'body' ? 'body' : issue.path[0] === 'query' ? 'query' : 'param'
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
expressRes.status(422).json({
|
|
76
|
+
data: null,
|
|
77
|
+
error: customErrors,
|
|
78
|
+
customHandled: true
|
|
79
|
+
});
|
|
80
|
+
return true; // Handled
|
|
81
|
+
} else if (error instanceof Error && error.message === 'Custom application error') {
|
|
82
|
+
// Custom application error handling
|
|
83
|
+
expressRes.status(400).json({
|
|
84
|
+
data: null,
|
|
85
|
+
error: [{ field: 'general', message: 'Custom error message', type: 'general' }],
|
|
86
|
+
customHandled: true
|
|
87
|
+
});
|
|
88
|
+
return true; // Handled
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return false; // Not handled, use default
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
RegisterHandlers(app, ErrorHandlerTestApiDefinition, testHandlers, undefined, customErrorHandler);
|
|
95
|
+
|
|
96
|
+
server = app.listen(port);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterAll(async () => {
|
|
100
|
+
if (server) {
|
|
101
|
+
server.close();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('should handle success response normally', async () => {
|
|
106
|
+
const response = await fetch(`http://localhost:${port}/api/success`);
|
|
107
|
+
expect(response.status).toBe(200);
|
|
108
|
+
const data = await response.json();
|
|
109
|
+
expect(data).toEqual({
|
|
110
|
+
data: { message: 'success' }
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('should use custom error handler for Zod validation errors', async () => {
|
|
115
|
+
const response = await fetch(`http://localhost:${port}/api/validation-error`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ name: 'A', email: 'invalid-email' })
|
|
119
|
+
});
|
|
120
|
+
expect(response.status).toBe(422);
|
|
121
|
+
const data = await response.json() as any;
|
|
122
|
+
expect(data.customHandled).toBe(true);
|
|
123
|
+
expect(data.error).toBeDefined();
|
|
124
|
+
expect(data.error.length).toBeGreaterThan(0);
|
|
125
|
+
expect(data.error[0].message).toContain('Custom:');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('should use custom error handler for application errors', async () => {
|
|
129
|
+
const response = await fetch(`http://localhost:${port}/api/custom-error`);
|
|
130
|
+
expect(response.status).toBe(400);
|
|
131
|
+
const data = await response.json() as any;
|
|
132
|
+
expect(data.customHandled).toBe(true);
|
|
133
|
+
expect(data.error).toEqual([{
|
|
134
|
+
field: 'general',
|
|
135
|
+
message: 'Custom error message',
|
|
136
|
+
type: 'general'
|
|
137
|
+
}]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('should fall back to default error handler for unhandled errors', async () => {
|
|
141
|
+
// Test with a route that doesn't exist to trigger a different error
|
|
142
|
+
const response = await fetch(`http://localhost:${port}/api/nonexistent`);
|
|
143
|
+
expect(response.status).toBe(404);
|
|
144
|
+
// This would be handled by Express default 404 handler, not our error handler
|
|
145
|
+
});
|
|
146
|
+
});
|
package/tests/setup.ts
CHANGED
|
@@ -69,7 +69,11 @@ const simplePublicHandlers = {
|
|
|
69
69
|
message: disconnected ? 'Client disconnected' : 'Operation completed',
|
|
70
70
|
disconnected
|
|
71
71
|
});
|
|
72
|
-
}
|
|
72
|
+
},
|
|
73
|
+
patchResource: async (req: any, res: any) => {
|
|
74
|
+
const { name, value } = req.body;
|
|
75
|
+
res.respond(200, { message: 'Patched', name, value });
|
|
76
|
+
},
|
|
73
77
|
},
|
|
74
78
|
status: {
|
|
75
79
|
probe1: async (req: any, res: any) => {
|
package/tests/simple-api.test.ts
CHANGED
|
@@ -328,6 +328,75 @@ describe.each([
|
|
|
328
328
|
});
|
|
329
329
|
});
|
|
330
330
|
|
|
331
|
+
describe('PATCH Body Validation', () => {
|
|
332
|
+
const client = new ApiClient(baseUrl, PublicApiDefinition);
|
|
333
|
+
|
|
334
|
+
test('should validate and parse PATCH body on success', async () => {
|
|
335
|
+
const result = await client.callApi('common', 'patchResource', {
|
|
336
|
+
body: { name: 'test-item', value: 42 }
|
|
337
|
+
}, {
|
|
338
|
+
200: ({ data }) => {
|
|
339
|
+
expect(data.message).toBe('Patched');
|
|
340
|
+
expect(data.name).toBe('test-item');
|
|
341
|
+
expect(data.value).toBe(42);
|
|
342
|
+
return data;
|
|
343
|
+
},
|
|
344
|
+
422: ({ error }) => {
|
|
345
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(result.message).toBe('Patched');
|
|
350
|
+
expect(result.name).toBe('test-item');
|
|
351
|
+
expect(result.value).toBe(42);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('should reject PATCH body with missing required fields', async () => {
|
|
355
|
+
await expect(
|
|
356
|
+
client.callApi('common', 'patchResource', {
|
|
357
|
+
body: { name: 'test' } as any
|
|
358
|
+
}, {
|
|
359
|
+
200: ({ data }) => data,
|
|
360
|
+
422: ({ error }) => {
|
|
361
|
+
expect(error).toBeDefined();
|
|
362
|
+
expect(Array.isArray(error)).toBe(true);
|
|
363
|
+
throw new Error('Validation failed as expected');
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
).rejects.toThrow('Validation failed as expected');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test('should reject PATCH body with invalid field types', async () => {
|
|
370
|
+
await expect(
|
|
371
|
+
client.callApi('common', 'patchResource', {
|
|
372
|
+
body: { name: 'test', value: 'not-a-number' } as any
|
|
373
|
+
}, {
|
|
374
|
+
200: ({ data }) => data,
|
|
375
|
+
422: ({ error }) => {
|
|
376
|
+
expect(error).toBeDefined();
|
|
377
|
+
expect(Array.isArray(error)).toBe(true);
|
|
378
|
+
throw new Error('Validation failed as expected');
|
|
379
|
+
}
|
|
380
|
+
})
|
|
381
|
+
).rejects.toThrow('Validation failed as expected');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('should reject PATCH body with extra unknown properties (strict)', async () => {
|
|
385
|
+
await expect(
|
|
386
|
+
client.callApi('common', 'patchResource', {
|
|
387
|
+
body: { name: 'test', value: 1, extraField: 'should not be allowed' } as any
|
|
388
|
+
}, {
|
|
389
|
+
200: ({ data }) => data,
|
|
390
|
+
422: ({ error }) => {
|
|
391
|
+
expect(error).toBeDefined();
|
|
392
|
+
expect(Array.isArray(error)).toBe(true);
|
|
393
|
+
throw new Error('Validation failed as expected');
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
).rejects.toThrow('Validation failed as expected');
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
331
400
|
describe('Private API', () => {
|
|
332
401
|
const client = new ApiClient(baseUrl, PrivateApiDefinition);
|
|
333
402
|
|