Skip to content

Commit

Permalink
Release/0.11.0 (#35)
Browse files Browse the repository at this point in the history
* v0.11.0 - see CHANGELOG for details
  • Loading branch information
Thesephi authored Sep 11, 2024
1 parent 37ac46a commit aa5aff6
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 12 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
## [0.11.0] - 2024-09-11

### Changed

- if a handler function throws because of a `Zod` validation error (ie. from
`ZodSchema.parse()`), the response will automatically have status `400` and
the whole `ZodError` as the response text

## [0.10.0] - 2024-09-08

### Added
Expand Down
4 changes: 3 additions & 1 deletion deps.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export { join } from "jsr:@std/path@^1.0.4";

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

export type {
Application,
Context,
ErrorStatus,
Next,
RouteContext,
} from "jsr:@oak/oak@^17.0.0";
Expand Down Expand Up @@ -90,6 +91,7 @@ type SubsetOfZ = Pick<
| "union"
| "unknown"
| "util"
| "ZodError"
>;
/**
* entry to the `Zod` API, enhanced with `@asteasolutions/zod-to-openapi`;
Expand Down
1 change: 1 addition & 0 deletions dev_deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export { Body } from "jsr:@oak/oak@^17.0.0/body";

export {
assertSpyCall,
assertSpyCallArg,
assertSpyCalls,
type MethodSpy,
type Spy,
Expand Down
2 changes: 1 addition & 1 deletion jsr.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dklab/oak-routing-ctrl",
"version": "0.10.0",
"version": "0.11.0",
"exports": {
".": "./mod.ts",
"./mod": "./mod.ts"
Expand Down
35 changes: 35 additions & 0 deletions src/__snapshots__/useOakServer_test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,40 @@ snapshot[`useOakServer - fully decorated Controller 1`] = `
path: "/test/uah",
regexp: /^\\/test\\/uah[\\/#\\?]?\$/i,
},
{
methods: [
"HEAD",
"GET",
],
middleware: [
[AsyncFunction (anonymous)],
],
options: {
end: undefined,
ignoreCaptures: undefined,
sensitive: undefined,
strict: undefined,
},
paramNames: [],
path: "/test/zodError",
regexp: /^\\/test\\/zodError[\\/#\\?]?\$/i,
},
{
methods: [
"POST",
],
middleware: [
[AsyncFunction (anonymous)],
],
options: {
end: undefined,
ignoreCaptures: undefined,
sensitive: undefined,
strict: undefined,
},
paramNames: [],
path: "/test/arbitraryError",
regexp: /^\\/test\\/arbitraryError[\\/#\\?]?\$/i,
},
]
`;
21 changes: 14 additions & 7 deletions src/useOakServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { debug } from "./utils/logger.ts";
import { type Application, Router } from "../deps.ts";
import { type Application, Router, Status, z } from "../deps.ts";
import type { ControllerClass } from "./Controller.ts";
import { store } from "./Store.ts";

Expand Down Expand Up @@ -28,12 +28,19 @@ export const useOakServer = (
Ctrl.prototype,
propName,
)?.value;
const handlerRetVal = await handler.call(ctrl, ctx);
// some developers set body within the handler,
// some developers return something from the handler
// and expect that it gets assigned to the response,
// so by doing the following, we satisfy both use cases
ctx.response.body = ctx.response.body ?? handlerRetVal;
try {
const handlerRetVal = await handler.call(ctrl, ctx);
// some developers set body within the handler,
// some developers return something from the handler
// and expect that it gets assigned to the response,
// so by doing the following, we satisfy both use cases
ctx.response.body = ctx.response.body ?? handlerRetVal;
} catch (e) {
if (e instanceof z.ZodError) {
return ctx.throw(Status.BadRequest, e.toString());
}
throw e;
}
await next();
},
);
Expand Down
87 changes: 84 additions & 3 deletions src/useOakServer_test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import type { SupportedVerb } from "./Store.ts";
import { type Context, RouteContext } from "../deps.ts";
import { assertEquals, assertSnapshot, oakTesting } from "../dev_deps.ts";
import {
type Context,
type ErrorStatus,
RouteContext,
Status,
z,
} from "../deps.ts";
import {
assertEquals,
assertSnapshot,
assertSpyCallArg,
assertSpyCalls,
oakTesting,
spy,
} from "../dev_deps.ts";
import {
Controller,
type ControllerMethodArg,
Expand Down Expand Up @@ -94,6 +107,14 @@ class TestController {
uah(body: ArrayBuffer) {
return `hello, ArrayBuffer body with byteLength=${body.byteLength}`;
}
@Get("/zodError")
zodError() {
z.enum(["alice", "bob"]).parse("camela");
}
@Post("/arbitraryError")
arbitraryError() {
throw new Error("nah");
}
}

/**
Expand All @@ -109,6 +130,9 @@ type TestCaseDefinition = {
mockRequestPathParams?: Record<string, string>;
mockRequestBody?: MockRequestBodyDefinition;
expectedResponse: unknown;
expectedCtxThrow?: boolean;
expectedError?: unknown;
expectedResponseStatus?: Status;
};

Deno.test("useOakServer - noop Controller", () => {
Expand Down Expand Up @@ -218,6 +242,32 @@ Deno.test({
},
expectedResponse: "hello, ArrayBuffer body with byteLength=42",
},
{
caseDescription: "handler where a ZodError (validation error) happens",
method: "get",
expectedCtxThrow: true,
expectedError: `[
{
"received": "camela",
"code": "invalid_enum_value",
"options": [
"alice",
"bob"
],
"path": [],
"message": "Invalid enum value. Expected 'alice' | 'bob', received 'camela'"
}
]`,
expectedResponse: undefined,
expectedResponseStatus: Status.BadRequest,
},
{
caseDescription: "handler where an arbitrary error happens",
method: "post",
expectedError: "nah",
expectedResponse: undefined,
expectedResponseStatus: Status.InternalServerError,
},
];

await Promise.all(
Expand All @@ -228,6 +278,9 @@ Deno.test({
mockRequestPathParams = undefined,
mockRequestBody = undefined,
expectedResponse,
expectedCtxThrow,
expectedError,
expectedResponseStatus,
}, i) =>
t.step({
name: `case ${i + 1}: ${caseDescription}`,
Expand All @@ -245,7 +298,35 @@ Deno.test({
const next = oakTesting.createMockNext();
useOakServer(ctx.app, [TestController]);
const routes = Array.from(useOakServerInternal.oakRouter.values());
await routes[i].middleware[0]?.(ctx, next); // simulate the route being requested
const spyCtxThrow = spy(ctx, "throw");
try {
// simulate the route being requested
await routes[i].middleware[0]?.(ctx, next);
} catch (e) {
const theErrMsg = (e as Error).message;
if (expectedCtxThrow) {
assertSpyCalls(spyCtxThrow, 1);
assertSpyCallArg(
spyCtxThrow,
0,
0,
expectedResponseStatus as ErrorStatus,
);
assertSpyCallArg(
spyCtxThrow,
0,
1,
JSON.stringify(
JSON.parse(expectedError as string),
undefined,
2,
),
);
} else {
assertSpyCalls(spyCtxThrow, 0);
assertEquals(theErrMsg, expectedError);
}
}
assertEquals(ctx.response.body, expectedResponse);
},
sanitizeOps: false,
Expand Down

0 comments on commit aa5aff6

Please sign in to comment.