ts-typed-api 0.2.22 → 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.
@@ -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/handler.js CHANGED
@@ -339,7 +339,7 @@ middlewares, errorHandler) {
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-typed-api",
3
- "version": "0.2.22",
3
+ "version": "0.2.23",
4
4
  "description": "A lightweight, type-safe RPC library for TypeScript with Zod validation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/handler.ts CHANGED
@@ -397,7 +397,7 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
397
397
  ? (routeDefinition.query as z.ZodTypeAny).parse(preprocessedQuery)
398
398
  : preprocessedQuery;
399
399
 
400
- 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)
401
401
  ? (routeDefinition.body as z.ZodTypeAny).parse(expressReq.body)
402
402
  : expressReq.body;
403
403
 
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) => {
@@ -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