Skip to content

Commit aa5aff6

Browse files
authored
Release/0.11.0 (#35)
* v0.11.0 - see CHANGELOG for details
1 parent 37ac46a commit aa5aff6

File tree

7 files changed

+146
-12
lines changed

7 files changed

+146
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## [0.11.0] - 2024-09-11
2+
3+
### Changed
4+
5+
- if a handler function throws because of a `Zod` validation error (ie. from
6+
`ZodSchema.parse()`), the response will automatically have status `400` and
7+
the whole `ZodError` as the response text
8+
19
## [0.10.0] - 2024-09-08
210

311
### Added

deps.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
export { join } from "jsr:@std/path@^1.0.4";
22

3-
export { Router } from "jsr:@oak/oak@^17.0.0";
3+
export { Router, Status } from "jsr:@oak/oak@^17.0.0";
44

55
export type {
66
Application,
77
Context,
8+
ErrorStatus,
89
Next,
910
RouteContext,
1011
} from "jsr:@oak/oak@^17.0.0";
@@ -90,6 +91,7 @@ type SubsetOfZ = Pick<
9091
| "union"
9192
| "unknown"
9293
| "util"
94+
| "ZodError"
9395
>;
9496
/**
9597
* entry to the `Zod` API, enhanced with `@asteasolutions/zod-to-openapi`;

dev_deps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export { Body } from "jsr:@oak/oak@^17.0.0/body";
1818

1919
export {
2020
assertSpyCall,
21+
assertSpyCallArg,
2122
assertSpyCalls,
2223
type MethodSpy,
2324
type Spy,

jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@dklab/oak-routing-ctrl",
3-
"version": "0.10.0",
3+
"version": "0.11.0",
44
"exports": {
55
".": "./mod.ts",
66
"./mod": "./mod.ts"

src/__snapshots__/useOakServer_test.ts.snap

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,5 +163,40 @@ snapshot[`useOakServer - fully decorated Controller 1`] = `
163163
path: "/test/uah",
164164
regexp: /^\\/test\\/uah[\\/#\\?]?\$/i,
165165
},
166+
{
167+
methods: [
168+
"HEAD",
169+
"GET",
170+
],
171+
middleware: [
172+
[AsyncFunction (anonymous)],
173+
],
174+
options: {
175+
end: undefined,
176+
ignoreCaptures: undefined,
177+
sensitive: undefined,
178+
strict: undefined,
179+
},
180+
paramNames: [],
181+
path: "/test/zodError",
182+
regexp: /^\\/test\\/zodError[\\/#\\?]?\$/i,
183+
},
184+
{
185+
methods: [
186+
"POST",
187+
],
188+
middleware: [
189+
[AsyncFunction (anonymous)],
190+
],
191+
options: {
192+
end: undefined,
193+
ignoreCaptures: undefined,
194+
sensitive: undefined,
195+
strict: undefined,
196+
},
197+
paramNames: [],
198+
path: "/test/arbitraryError",
199+
regexp: /^\\/test\\/arbitraryError[\\/#\\?]?\$/i,
200+
},
166201
]
167202
`;

src/useOakServer.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { debug } from "./utils/logger.ts";
2-
import { type Application, Router } from "../deps.ts";
2+
import { type Application, Router, Status, z } from "../deps.ts";
33
import type { ControllerClass } from "./Controller.ts";
44
import { store } from "./Store.ts";
55

@@ -28,12 +28,19 @@ export const useOakServer = (
2828
Ctrl.prototype,
2929
propName,
3030
)?.value;
31-
const handlerRetVal = await handler.call(ctrl, ctx);
32-
// some developers set body within the handler,
33-
// some developers return something from the handler
34-
// and expect that it gets assigned to the response,
35-
// so by doing the following, we satisfy both use cases
36-
ctx.response.body = ctx.response.body ?? handlerRetVal;
31+
try {
32+
const handlerRetVal = await handler.call(ctrl, ctx);
33+
// some developers set body within the handler,
34+
// some developers return something from the handler
35+
// and expect that it gets assigned to the response,
36+
// so by doing the following, we satisfy both use cases
37+
ctx.response.body = ctx.response.body ?? handlerRetVal;
38+
} catch (e) {
39+
if (e instanceof z.ZodError) {
40+
return ctx.throw(Status.BadRequest, e.toString());
41+
}
42+
throw e;
43+
}
3744
await next();
3845
},
3946
);

src/useOakServer_test.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import type { SupportedVerb } from "./Store.ts";
2-
import { type Context, RouteContext } from "../deps.ts";
3-
import { assertEquals, assertSnapshot, oakTesting } from "../dev_deps.ts";
2+
import {
3+
type Context,
4+
type ErrorStatus,
5+
RouteContext,
6+
Status,
7+
z,
8+
} from "../deps.ts";
9+
import {
10+
assertEquals,
11+
assertSnapshot,
12+
assertSpyCallArg,
13+
assertSpyCalls,
14+
oakTesting,
15+
spy,
16+
} from "../dev_deps.ts";
417
import {
518
Controller,
619
type ControllerMethodArg,
@@ -94,6 +107,14 @@ class TestController {
94107
uah(body: ArrayBuffer) {
95108
return `hello, ArrayBuffer body with byteLength=${body.byteLength}`;
96109
}
110+
@Get("/zodError")
111+
zodError() {
112+
z.enum(["alice", "bob"]).parse("camela");
113+
}
114+
@Post("/arbitraryError")
115+
arbitraryError() {
116+
throw new Error("nah");
117+
}
97118
}
98119

99120
/**
@@ -109,6 +130,9 @@ type TestCaseDefinition = {
109130
mockRequestPathParams?: Record<string, string>;
110131
mockRequestBody?: MockRequestBodyDefinition;
111132
expectedResponse: unknown;
133+
expectedCtxThrow?: boolean;
134+
expectedError?: unknown;
135+
expectedResponseStatus?: Status;
112136
};
113137

114138
Deno.test("useOakServer - noop Controller", () => {
@@ -218,6 +242,32 @@ Deno.test({
218242
},
219243
expectedResponse: "hello, ArrayBuffer body with byteLength=42",
220244
},
245+
{
246+
caseDescription: "handler where a ZodError (validation error) happens",
247+
method: "get",
248+
expectedCtxThrow: true,
249+
expectedError: `[
250+
{
251+
"received": "camela",
252+
"code": "invalid_enum_value",
253+
"options": [
254+
"alice",
255+
"bob"
256+
],
257+
"path": [],
258+
"message": "Invalid enum value. Expected 'alice' | 'bob', received 'camela'"
259+
}
260+
]`,
261+
expectedResponse: undefined,
262+
expectedResponseStatus: Status.BadRequest,
263+
},
264+
{
265+
caseDescription: "handler where an arbitrary error happens",
266+
method: "post",
267+
expectedError: "nah",
268+
expectedResponse: undefined,
269+
expectedResponseStatus: Status.InternalServerError,
270+
},
221271
];
222272

223273
await Promise.all(
@@ -228,6 +278,9 @@ Deno.test({
228278
mockRequestPathParams = undefined,
229279
mockRequestBody = undefined,
230280
expectedResponse,
281+
expectedCtxThrow,
282+
expectedError,
283+
expectedResponseStatus,
231284
}, i) =>
232285
t.step({
233286
name: `case ${i + 1}: ${caseDescription}`,
@@ -245,7 +298,35 @@ Deno.test({
245298
const next = oakTesting.createMockNext();
246299
useOakServer(ctx.app, [TestController]);
247300
const routes = Array.from(useOakServerInternal.oakRouter.values());
248-
await routes[i].middleware[0]?.(ctx, next); // simulate the route being requested
301+
const spyCtxThrow = spy(ctx, "throw");
302+
try {
303+
// simulate the route being requested
304+
await routes[i].middleware[0]?.(ctx, next);
305+
} catch (e) {
306+
const theErrMsg = (e as Error).message;
307+
if (expectedCtxThrow) {
308+
assertSpyCalls(spyCtxThrow, 1);
309+
assertSpyCallArg(
310+
spyCtxThrow,
311+
0,
312+
0,
313+
expectedResponseStatus as ErrorStatus,
314+
);
315+
assertSpyCallArg(
316+
spyCtxThrow,
317+
0,
318+
1,
319+
JSON.stringify(
320+
JSON.parse(expectedError as string),
321+
undefined,
322+
2,
323+
),
324+
);
325+
} else {
326+
assertSpyCalls(spyCtxThrow, 0);
327+
assertEquals(theErrMsg, expectedError);
328+
}
329+
}
249330
assertEquals(ctx.response.body, expectedResponse);
250331
},
251332
sanitizeOps: false,

0 commit comments

Comments
 (0)