lambda-reactor 1.0.0 → 1.0.2
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/{src/build-handlers.ts → dist/build-handlers.d.ts} +3 -16
- package/dist/build-handlers.js +13 -0
- package/dist/build-handlers.js.map +10 -0
- package/dist/dispatch.d.ts +21 -0
- package/dist/dispatch.js +55 -0
- package/dist/dispatch.js.map +10 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +13 -0
- package/dist/env.js.map +10 -0
- package/dist/handler.d.ts +26 -0
- package/dist/handler.js +29 -0
- package/dist/handler.js.map +10 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.js +19 -0
- package/dist/logger.js.map +10 -0
- package/dist/method.d.ts +53 -0
- package/dist/method.js +57 -0
- package/dist/method.js.map +10 -0
- package/{src/middleware.ts → dist/middleware.d.ts} +5 -22
- package/dist/middleware.js +19 -0
- package/dist/middleware.js.map +10 -0
- package/dist/response.d.ts +48 -0
- package/dist/response.js +46 -0
- package/dist/response.js.map +10 -0
- package/{src/route-handler.ts → dist/route-handler.d.ts} +12 -14
- package/dist/route-handler.js +2 -0
- package/dist/route-handler.js.map +9 -0
- package/dist/router-class.d.ts +54 -0
- package/dist/router-class.js +46 -0
- package/dist/router-class.js.map +10 -0
- package/{src/router.ts → dist/router.d.ts} +4 -16
- package/dist/router.js +18 -0
- package/dist/router.js.map +10 -0
- package/package.json +12 -2
- package/.prettierrc +0 -8
- package/AGENTS.md +0 -7
- package/bun.lock +0 -477
- package/eslint.config.ts +0 -31
- package/examples/cdk-stack.ts +0 -20
- package/examples/health.ts +0 -7
- package/examples/items.ts +0 -25
- package/examples/users-get.ts +0 -20
- package/examples/users-post.ts +0 -26
- package/lefthook.yml +0 -16
- package/src/dispatch.ts +0 -91
- package/src/env.ts +0 -23
- package/src/handler.ts +0 -58
- package/src/logger.ts +0 -19
- package/src/method.ts +0 -92
- package/src/response.ts +0 -86
- package/src/router-class.ts +0 -98
- package/tests/api-get-methods.test.ts +0 -14
- package/tests/api-router-factory.test.ts +0 -46
- package/tests/api-router.test.ts +0 -72
- package/tests/config-cors.test.ts +0 -50
- package/tests/config.test.ts +0 -79
- package/tests/dispatch-error-logging.test.ts +0 -61
- package/tests/env.test.ts +0 -38
- package/tests/handler-error-logging.test.ts +0 -61
- package/tests/handler-routing-validation.test.ts +0 -86
- package/tests/handler-routing.test.ts +0 -37
- package/tests/handler.test.ts +0 -48
- package/tests/method.test.ts +0 -40
- package/tests/response.test.ts +0 -29
- package/tsconfig.build.json +0 -26
- package/tsconfig.json +0 -3
- package/tsconfig.node.json +0 -24
- package/vitest.config.ts +0 -21
- package/vitest.setup.ts +0 -2
package/tests/api-router.test.ts
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import {join} from "path"
|
|
2
|
-
|
|
3
|
-
import type {IRestApi} from "aws-cdk-lib/aws-apigateway"
|
|
4
|
-
import type {IFunction} from "aws-cdk-lib/aws-lambda"
|
|
5
|
-
import {beforeEach, describe, expect, it, vi} from "vitest"
|
|
6
|
-
|
|
7
|
-
vi.mock("fs", () => ({readFileSync: vi.fn()}))
|
|
8
|
-
|
|
9
|
-
vi.mock("aws-cdk-lib/aws-apigateway", () => ({
|
|
10
|
-
LambdaIntegration: vi.fn((handler: unknown) => ({handler})),
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
|
-
class FakeResource {
|
|
14
|
-
children = new Map<string, FakeResource>()
|
|
15
|
-
methods: {method: string; integration: unknown}[] = []
|
|
16
|
-
getResource(part: string) {
|
|
17
|
-
return this.children.get(part)
|
|
18
|
-
}
|
|
19
|
-
addResource(part: string) {
|
|
20
|
-
const child = new FakeResource()
|
|
21
|
-
this.children.set(part, child)
|
|
22
|
-
return child
|
|
23
|
-
}
|
|
24
|
-
addMethod(method: string, value: unknown) {
|
|
25
|
-
this.methods.push({method, integration: value})
|
|
26
|
-
return this
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
describe("router defineRestApi", () => {
|
|
31
|
-
beforeEach(async () => {
|
|
32
|
-
const {LambdaIntegration} = await import("aws-cdk-lib/aws-apigateway")
|
|
33
|
-
;(LambdaIntegration as ReturnType<typeof vi.fn>).mockClear()
|
|
34
|
-
const {readFileSync} = await import("fs")
|
|
35
|
-
;(readFileSync as ReturnType<typeof vi.fn>).mockReturnValue(
|
|
36
|
-
`export const handler = createHandler({GET, POST})`,
|
|
37
|
-
)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it("defines nested rest resources on an existing api instance", async () => {
|
|
41
|
-
const {router} = await import("#src/router")
|
|
42
|
-
const root = new FakeResource()
|
|
43
|
-
const api = {root} as unknown as IRestApi
|
|
44
|
-
const getUser = {name: "getUser"} as unknown as IFunction
|
|
45
|
-
const createPost = {name: "createPost"} as unknown as IFunction
|
|
46
|
-
const factory = vi.fn((_entry: string, id: string) => {
|
|
47
|
-
if (id === "/user/{user_id}") return getUser
|
|
48
|
-
return createPost
|
|
49
|
-
})
|
|
50
|
-
router().route("/user/{user_id}").route("/posts").defineRestApi(api, factory)
|
|
51
|
-
expect(factory).toHaveBeenCalledWith(
|
|
52
|
-
join(process.cwd(), "src", "/user/{user_id}.ts"),
|
|
53
|
-
"/user/{user_id}",
|
|
54
|
-
)
|
|
55
|
-
expect(factory).toHaveBeenCalledWith(
|
|
56
|
-
join(process.cwd(), "src", "/posts.ts"),
|
|
57
|
-
"/posts",
|
|
58
|
-
)
|
|
59
|
-
const {LambdaIntegration} = await import("aws-cdk-lib/aws-apigateway")
|
|
60
|
-
expect(LambdaIntegration).toHaveBeenCalledWith(getUser)
|
|
61
|
-
expect(LambdaIntegration).toHaveBeenCalledWith(createPost)
|
|
62
|
-
const user = root.getResource("user")
|
|
63
|
-
expect(user?.getResource("{user_id}")?.methods).toEqual([
|
|
64
|
-
{method: "GET", integration: {handler: getUser}},
|
|
65
|
-
{method: "POST", integration: {handler: getUser}},
|
|
66
|
-
])
|
|
67
|
-
expect(root.getResource("posts")?.methods).toEqual([
|
|
68
|
-
{method: "GET", integration: {handler: createPost}},
|
|
69
|
-
{method: "POST", integration: {handler: createPost}},
|
|
70
|
-
])
|
|
71
|
-
})
|
|
72
|
-
})
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import {dispatch} from "#src/dispatch"
|
|
2
|
-
import {method} from "#src/method"
|
|
3
|
-
import {cors} from "#src/middleware"
|
|
4
|
-
import type {APIGatewayProxyEvent, Context} from "aws-lambda"
|
|
5
|
-
import {describe, expect, it} from "vitest"
|
|
6
|
-
|
|
7
|
-
const context = {} as Context
|
|
8
|
-
|
|
9
|
-
const event = {
|
|
10
|
-
body: null,
|
|
11
|
-
headers: {},
|
|
12
|
-
multiValueHeaders: {},
|
|
13
|
-
httpMethod: "GET",
|
|
14
|
-
isBase64Encoded: false,
|
|
15
|
-
path: "/",
|
|
16
|
-
pathParameters: null,
|
|
17
|
-
queryStringParameters: null,
|
|
18
|
-
multiValueQueryStringParameters: null,
|
|
19
|
-
stageVariables: null,
|
|
20
|
-
resource: "/",
|
|
21
|
-
requestContext: {} as APIGatewayProxyEvent["requestContext"],
|
|
22
|
-
} satisfies APIGatewayProxyEvent
|
|
23
|
-
|
|
24
|
-
describe("cors middleware", () => {
|
|
25
|
-
it("adds Access-Control headers to response", async () => {
|
|
26
|
-
const route = method()
|
|
27
|
-
.use(cors({"Allow-Origin": "*", "Allow-Methods": "GET,POST"}))
|
|
28
|
-
.handle(async () => "ok")
|
|
29
|
-
const result = await dispatch(route, event, context)
|
|
30
|
-
expect(result.headers?.["Access-Control-Allow-Origin"]).toBe("*")
|
|
31
|
-
expect(result.headers?.["Access-Control-Allow-Methods"]).toBe("GET,POST")
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
it("does not add cors headers when no middleware used", async () => {
|
|
35
|
-
const route = method().handle(async () => "ok")
|
|
36
|
-
const result = await dispatch(route, event, context)
|
|
37
|
-
const keys = Object.keys(result.headers ?? {})
|
|
38
|
-
expect(keys.some((k) => k.startsWith("Access-Control"))).toBe(false)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it("applies multiple cors middlewares in order", async () => {
|
|
42
|
-
const route = method()
|
|
43
|
-
.use(cors({"Allow-Origin": "*"}))
|
|
44
|
-
.use(cors({"Allow-Methods": "GET"}))
|
|
45
|
-
.handle(async () => "ok")
|
|
46
|
-
const result = await dispatch(route, event, context)
|
|
47
|
-
expect(result.headers?.["Access-Control-Allow-Origin"]).toBe("*")
|
|
48
|
-
expect(result.headers?.["Access-Control-Allow-Methods"]).toBe("GET")
|
|
49
|
-
})
|
|
50
|
-
})
|
package/tests/config.test.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import {method} from "#src/method"
|
|
2
|
-
import {cors} from "#src/middleware"
|
|
3
|
-
import {router} from "#src/router"
|
|
4
|
-
import {describe, expect, it} from "vitest"
|
|
5
|
-
import {z} from "zod"
|
|
6
|
-
|
|
7
|
-
describe("method middlewares", () => {
|
|
8
|
-
it("has empty middlewares by default", () => {
|
|
9
|
-
expect(method().middlewares).toEqual([])
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
it("adds middleware via .use()", () => {
|
|
13
|
-
const m = cors({"Allow-Origin": "*"})
|
|
14
|
-
expect(method().use(m).middlewares).toHaveLength(1)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
it("accumulates multiple middlewares", () => {
|
|
18
|
-
const m1 = cors({"Allow-Origin": "*"})
|
|
19
|
-
const m2 = cors({"Allow-Methods": "GET"})
|
|
20
|
-
expect(method().use(m1).use(m2).middlewares).toHaveLength(2)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it("propagates middlewares through input chain", () => {
|
|
24
|
-
const m = cors({"Allow-Origin": "*"})
|
|
25
|
-
expect(
|
|
26
|
-
method()
|
|
27
|
-
.use(m)
|
|
28
|
-
.input(z.object({x: z.string()})).middlewares,
|
|
29
|
-
).toHaveLength(1)
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it("propagates middlewares through output chain", () => {
|
|
33
|
-
const m = cors({"Allow-Origin": "*"})
|
|
34
|
-
expect(
|
|
35
|
-
method()
|
|
36
|
-
.use(m)
|
|
37
|
-
.output(z.object({x: z.string()})).middlewares,
|
|
38
|
-
).toHaveLength(1)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it("propagates middlewares through handle chain", () => {
|
|
42
|
-
const m = cors({"Allow-Origin": "*"})
|
|
43
|
-
expect(
|
|
44
|
-
method()
|
|
45
|
-
.use(m)
|
|
46
|
-
.handle(async () => "ok").middlewares,
|
|
47
|
-
).toHaveLength(1)
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe("router srcDir", () => {
|
|
52
|
-
it("defaults srcDir to src", () => {
|
|
53
|
-
expect(router().srcDir).toBe("src")
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
it("accepts custom srcDir", () => {
|
|
57
|
-
expect(router("src/api").srcDir).toBe("src/api")
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it("propagates srcDir through route chain", () => {
|
|
61
|
-
expect(router("src/api").route("/health").srcDir).toBe("src/api")
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
describe("router cors", () => {
|
|
66
|
-
it("stores corsOptions via .cors()", () => {
|
|
67
|
-
const options = {allowOrigins: ["*"], allowMethods: ["GET"]}
|
|
68
|
-
expect(router().cors(options).corsOptions).toEqual(options)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it("propagates corsOptions through route chain", () => {
|
|
72
|
-
const options = {allowOrigins: ["*"], allowMethods: ["GET"]}
|
|
73
|
-
expect(router().cors(options).route("/health").corsOptions).toEqual(options)
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
it("has no corsOptions by default", () => {
|
|
77
|
-
expect(router().corsOptions).toBeUndefined()
|
|
78
|
-
})
|
|
79
|
-
})
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import {dispatch} from "#src/dispatch"
|
|
2
|
-
import * as logger from "#src/logger"
|
|
3
|
-
import {method} from "#src/method"
|
|
4
|
-
import type {APIGatewayProxyEvent, Context} from "aws-lambda"
|
|
5
|
-
import {describe, expect, it, vi} from "vitest"
|
|
6
|
-
import {z} from "zod"
|
|
7
|
-
|
|
8
|
-
const context = {} as Context
|
|
9
|
-
|
|
10
|
-
const event = {
|
|
11
|
-
body: null,
|
|
12
|
-
headers: {},
|
|
13
|
-
multiValueHeaders: {},
|
|
14
|
-
httpMethod: "GET",
|
|
15
|
-
isBase64Encoded: false,
|
|
16
|
-
path: "/",
|
|
17
|
-
pathParameters: null,
|
|
18
|
-
queryStringParameters: null,
|
|
19
|
-
multiValueQueryStringParameters: null,
|
|
20
|
-
stageVariables: null,
|
|
21
|
-
resource: "/",
|
|
22
|
-
requestContext: {} as APIGatewayProxyEvent["requestContext"],
|
|
23
|
-
} satisfies APIGatewayProxyEvent
|
|
24
|
-
|
|
25
|
-
describe("dispatch error logging", () => {
|
|
26
|
-
it("logs error when route has no callback", async () => {
|
|
27
|
-
const logError = vi.spyOn(logger, "logError").mockImplementation(() => "")
|
|
28
|
-
const route = method()
|
|
29
|
-
await expect(dispatch(route, event, context)).resolves.toMatchObject({
|
|
30
|
-
statusCode: 500,
|
|
31
|
-
})
|
|
32
|
-
expect(logError).toHaveBeenCalledWith(expect.any(Error))
|
|
33
|
-
logError.mockRestore()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it("logs error when output schema validation fails on Response body", async () => {
|
|
37
|
-
const logError = vi.spyOn(logger, "logError").mockImplementation(() => "")
|
|
38
|
-
const route = method()
|
|
39
|
-
.output(z.object({n: z.number()}))
|
|
40
|
-
.handle(() => ({n: "not-a-number"}) as unknown as {n: number})
|
|
41
|
-
await expect(dispatch(route, event, context)).resolves.toMatchObject({
|
|
42
|
-
statusCode: 500,
|
|
43
|
-
})
|
|
44
|
-
expect(logError).toHaveBeenCalled()
|
|
45
|
-
logError.mockRestore()
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it("logs error when output schema validation fails on plain result", async () => {
|
|
49
|
-
const logError = vi.spyOn(logger, "logError").mockImplementation(() => "")
|
|
50
|
-
const route = method()
|
|
51
|
-
.output(z.object({n: z.number()}))
|
|
52
|
-
.handle(() =>
|
|
53
|
-
Promise.resolve({n: "not-a-number"} as unknown as {n: number}),
|
|
54
|
-
)
|
|
55
|
-
await expect(dispatch(route, event, context)).resolves.toMatchObject({
|
|
56
|
-
statusCode: 500,
|
|
57
|
-
})
|
|
58
|
-
expect(logError).toHaveBeenCalled()
|
|
59
|
-
logError.mockRestore()
|
|
60
|
-
})
|
|
61
|
-
})
|
package/tests/env.test.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import {isProduction} from "#src/env"
|
|
2
|
-
import {afterEach, describe, expect, it} from "vitest"
|
|
3
|
-
|
|
4
|
-
const ENV_KEYS = ["NODE_ENV", "STAGE", "ENV", "ENVIRONMENT"] as const
|
|
5
|
-
|
|
6
|
-
describe("isProduction", () => {
|
|
7
|
-
afterEach(() => {
|
|
8
|
-
for (const key of ENV_KEYS) {
|
|
9
|
-
delete process.env[key]
|
|
10
|
-
}
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it("returns false when no env vars are set", () => {
|
|
14
|
-
expect(isProduction()).toBe(false)
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
for (const key of ENV_KEYS) {
|
|
18
|
-
it(`returns true when ${key}=production`, () => {
|
|
19
|
-
process.env[key] = "production"
|
|
20
|
-
expect(isProduction()).toBe(true)
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it(`returns true when ${key}=prod`, () => {
|
|
24
|
-
process.env[key] = "prod"
|
|
25
|
-
expect(isProduction()).toBe(true)
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
it(`returns true when ${key}=PRODUCTION (case-insensitive)`, () => {
|
|
29
|
-
process.env[key] = "PRODUCTION"
|
|
30
|
-
expect(isProduction()).toBe(true)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it(`returns false when ${key}=development`, () => {
|
|
34
|
-
process.env[key] = "development"
|
|
35
|
-
expect(isProduction()).toBe(false)
|
|
36
|
-
})
|
|
37
|
-
}
|
|
38
|
-
})
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import {createHandler} from "#src/handler"
|
|
2
|
-
import {method} from "#src/method"
|
|
3
|
-
import type {APIGatewayProxyEvent, Context} from "aws-lambda"
|
|
4
|
-
import {afterEach, describe, expect, it, vi} from "vitest"
|
|
5
|
-
|
|
6
|
-
vi.mock("source-map-support", () => ({install: vi.fn()}))
|
|
7
|
-
|
|
8
|
-
const context = {} as Context
|
|
9
|
-
|
|
10
|
-
function event(httpMethod: string): APIGatewayProxyEvent {
|
|
11
|
-
return {
|
|
12
|
-
body: null,
|
|
13
|
-
headers: {},
|
|
14
|
-
multiValueHeaders: {},
|
|
15
|
-
httpMethod,
|
|
16
|
-
isBase64Encoded: false,
|
|
17
|
-
path: "/",
|
|
18
|
-
pathParameters: null,
|
|
19
|
-
queryStringParameters: null,
|
|
20
|
-
multiValueQueryStringParameters: null,
|
|
21
|
-
stageVariables: null,
|
|
22
|
-
resource: "/",
|
|
23
|
-
requestContext: {} as APIGatewayProxyEvent["requestContext"],
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe("createHandler error logging", () => {
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
delete process.env["NODE_ENV"]
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it("logs the error message and returns it in the body outside production", async () => {
|
|
33
|
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
34
|
-
const boom = new Error("boom")
|
|
35
|
-
const handler = createHandler({
|
|
36
|
-
GET: method().handle(() => {
|
|
37
|
-
throw boom
|
|
38
|
-
}),
|
|
39
|
-
})
|
|
40
|
-
const result = await handler(event("GET"), context)
|
|
41
|
-
expect(result.statusCode).toBe(500)
|
|
42
|
-
expect(result.body).toContain("boom")
|
|
43
|
-
expect(consoleError).toHaveBeenCalledWith(expect.stringContaining("boom"))
|
|
44
|
-
consoleError.mockRestore()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it("returns generic message in production", async () => {
|
|
48
|
-
process.env["NODE_ENV"] = "production"
|
|
49
|
-
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {})
|
|
50
|
-
const handler = createHandler({
|
|
51
|
-
GET: method().handle(() => {
|
|
52
|
-
throw new Error("secret details")
|
|
53
|
-
}),
|
|
54
|
-
})
|
|
55
|
-
const result = await handler(event("GET"), context)
|
|
56
|
-
expect(result.statusCode).toBe(500)
|
|
57
|
-
expect(result.body).toBe("Internal Server Error")
|
|
58
|
-
expect(result.body).not.toContain("secret details")
|
|
59
|
-
consoleError.mockRestore()
|
|
60
|
-
})
|
|
61
|
-
})
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import {createHandler} from "#src/handler"
|
|
2
|
-
import {method} from "#src/method"
|
|
3
|
-
import {Response} from "#src/response"
|
|
4
|
-
import type {APIGatewayProxyEvent, Context} from "aws-lambda"
|
|
5
|
-
import {describe, expect, it} from "vitest"
|
|
6
|
-
import {z} from "zod"
|
|
7
|
-
|
|
8
|
-
function event(httpMethod: string, body: unknown = undefined): APIGatewayProxyEvent {
|
|
9
|
-
return {
|
|
10
|
-
body:
|
|
11
|
-
body === undefined ? null
|
|
12
|
-
: typeof body === "string" ? body
|
|
13
|
-
: JSON.stringify(body),
|
|
14
|
-
headers: {},
|
|
15
|
-
multiValueHeaders: {},
|
|
16
|
-
httpMethod,
|
|
17
|
-
isBase64Encoded: false,
|
|
18
|
-
path: "/",
|
|
19
|
-
pathParameters: null,
|
|
20
|
-
queryStringParameters: null,
|
|
21
|
-
multiValueQueryStringParameters: null,
|
|
22
|
-
stageVariables: null,
|
|
23
|
-
resource: "/",
|
|
24
|
-
requestContext: {} as APIGatewayProxyEvent["requestContext"],
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const context = {} as Context
|
|
29
|
-
|
|
30
|
-
describe("createHandler validation", () => {
|
|
31
|
-
it("validates input and returns 400 on bad input", async () => {
|
|
32
|
-
const handler = createHandler({
|
|
33
|
-
POST: method()
|
|
34
|
-
.input(z.object({username: z.string()}))
|
|
35
|
-
.handle(({body}) => ({username: body.username})),
|
|
36
|
-
})
|
|
37
|
-
const invalid = await handler(event("POST", {username: 7}), context)
|
|
38
|
-
expect(invalid).toMatchObject({
|
|
39
|
-
statusCode: 400,
|
|
40
|
-
headers: {"Content-Type": "text/plain; charset=utf-8"},
|
|
41
|
-
})
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it("validates output schema and returns json on success", async () => {
|
|
45
|
-
const handler = createHandler({
|
|
46
|
-
POST: method()
|
|
47
|
-
.input(z.object({username: z.string()}))
|
|
48
|
-
.output(z.object({username: z.string()}))
|
|
49
|
-
.handle(({body}) => ({username: body.username})),
|
|
50
|
-
})
|
|
51
|
-
await expect(
|
|
52
|
-
handler(event("POST", {username: "neo"}), context),
|
|
53
|
-
).resolves.toEqual({
|
|
54
|
-
statusCode: 200,
|
|
55
|
-
body: JSON.stringify({username: "neo"}),
|
|
56
|
-
headers: {"Content-Type": "application/json; charset=utf-8"},
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it("validates output schema against raw Response body and returns 500 on mismatch", async () => {
|
|
61
|
-
const handler = createHandler({
|
|
62
|
-
POST: method()
|
|
63
|
-
.output(z.object({username: z.string()}))
|
|
64
|
-
.handle(() => Response.json(200, {username: 42})),
|
|
65
|
-
})
|
|
66
|
-
const result = await handler(event("POST"), context)
|
|
67
|
-
expect(result).toMatchObject({
|
|
68
|
-
statusCode: 500,
|
|
69
|
-
headers: {"Content-Type": "text/plain; charset=utf-8"},
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it("validates output schema against raw Response body and passes on match", async () => {
|
|
74
|
-
const handler = createHandler({
|
|
75
|
-
POST: method()
|
|
76
|
-
.output(z.object({username: z.string()}))
|
|
77
|
-
.handle(() => Response.json(201, {username: "neo"})),
|
|
78
|
-
})
|
|
79
|
-
const result = await handler(event("POST"), context)
|
|
80
|
-
expect(result).toEqual({
|
|
81
|
-
statusCode: 201,
|
|
82
|
-
body: JSON.stringify({username: "neo"}),
|
|
83
|
-
headers: {"Content-Type": "application/json; charset=utf-8"},
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
})
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import {createHandler} from "#src/handler"
|
|
2
|
-
import {method} from "#src/method"
|
|
3
|
-
import type {APIGatewayProxyEvent, Context} from "aws-lambda"
|
|
4
|
-
import {describe, expect, it} from "vitest"
|
|
5
|
-
|
|
6
|
-
function event(httpMethod: string, body: unknown = undefined): APIGatewayProxyEvent {
|
|
7
|
-
return {
|
|
8
|
-
body:
|
|
9
|
-
body === undefined ? null
|
|
10
|
-
: typeof body === "string" ? body
|
|
11
|
-
: JSON.stringify(body),
|
|
12
|
-
headers: {},
|
|
13
|
-
multiValueHeaders: {},
|
|
14
|
-
httpMethod,
|
|
15
|
-
isBase64Encoded: false,
|
|
16
|
-
path: "/",
|
|
17
|
-
pathParameters: null,
|
|
18
|
-
queryStringParameters: null,
|
|
19
|
-
multiValueQueryStringParameters: null,
|
|
20
|
-
stageVariables: null,
|
|
21
|
-
resource: "/",
|
|
22
|
-
requestContext: {} as APIGatewayProxyEvent["requestContext"],
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const context = {} as Context
|
|
27
|
-
|
|
28
|
-
describe("createHandler routing", () => {
|
|
29
|
-
it("returns 405 with allow header for unsupported methods", async () => {
|
|
30
|
-
const handler = createHandler({GET: method().handle(() => ({ok: true}))})
|
|
31
|
-
await expect(handler(event("POST"), context)).resolves.toEqual({
|
|
32
|
-
statusCode: 405,
|
|
33
|
-
body: "Method Not Allowed",
|
|
34
|
-
headers: {"Content-Type": "text/plain; charset=utf-8", Allow: "GET"},
|
|
35
|
-
})
|
|
36
|
-
})
|
|
37
|
-
})
|
package/tests/handler.test.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import {createHandler} from "#src/handler"
|
|
2
|
-
import {method} from "#src/method"
|
|
3
|
-
import {Response} from "#src/response"
|
|
4
|
-
import type {APIGatewayProxyEvent, Context} from "aws-lambda"
|
|
5
|
-
import {describe, expect, it, vi} from "vitest"
|
|
6
|
-
|
|
7
|
-
vi.mock("source-map-support", () => ({install: vi.fn()}))
|
|
8
|
-
|
|
9
|
-
function event(httpMethod: string): APIGatewayProxyEvent {
|
|
10
|
-
return {
|
|
11
|
-
body: null,
|
|
12
|
-
headers: {},
|
|
13
|
-
multiValueHeaders: {},
|
|
14
|
-
httpMethod,
|
|
15
|
-
isBase64Encoded: false,
|
|
16
|
-
path: "/",
|
|
17
|
-
pathParameters: null,
|
|
18
|
-
queryStringParameters: null,
|
|
19
|
-
multiValueQueryStringParameters: null,
|
|
20
|
-
stageVariables: null,
|
|
21
|
-
resource: "/",
|
|
22
|
-
requestContext: {} as APIGatewayProxyEvent["requestContext"],
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const context = {} as Context
|
|
27
|
-
|
|
28
|
-
describe("createHandler", () => {
|
|
29
|
-
it("passes through Response instances", async () => {
|
|
30
|
-
const callback = vi.fn(() => Response.text(204, ""))
|
|
31
|
-
const handler = createHandler({GET: method().handle(callback)})
|
|
32
|
-
await expect(handler(event("GET"), context)).resolves.toEqual({
|
|
33
|
-
statusCode: 204,
|
|
34
|
-
body: "",
|
|
35
|
-
headers: {"Content-Type": "text/plain; charset=utf-8"},
|
|
36
|
-
})
|
|
37
|
-
expect(callback).toHaveBeenCalledOnce()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it("returns 500 when callback is not defined", async () => {
|
|
41
|
-
const handler = createHandler({GET: method()})
|
|
42
|
-
await expect(handler(event("GET"), context)).resolves.toEqual({
|
|
43
|
-
statusCode: 500,
|
|
44
|
-
body: "Route has no handler defined",
|
|
45
|
-
headers: {"Content-Type": "text/plain; charset=utf-8"},
|
|
46
|
-
})
|
|
47
|
-
})
|
|
48
|
-
})
|
package/tests/method.test.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import {method} from "#src/method"
|
|
2
|
-
import {describe, expect, it} from "vitest"
|
|
3
|
-
import {z} from "zod"
|
|
4
|
-
|
|
5
|
-
describe("method", () => {
|
|
6
|
-
it("creates a method with no schema", () => {
|
|
7
|
-
const m = method()
|
|
8
|
-
expect(m.bodySchema).toBeUndefined()
|
|
9
|
-
expect(m.outputSchema).toBeUndefined()
|
|
10
|
-
expect(m.callback).toBeUndefined()
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
it("chains input schema", () => {
|
|
14
|
-
const schema = z.object({name: z.string()})
|
|
15
|
-
const m = method().input(schema)
|
|
16
|
-
expect(m.bodySchema).toBe(schema)
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
it("chains output schema", () => {
|
|
20
|
-
const schema = z.object({id: z.number()})
|
|
21
|
-
const m = method().output(schema)
|
|
22
|
-
expect(m.outputSchema).toBe(schema)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it("chains handle callback", () => {
|
|
26
|
-
const cb = async () => ({ok: true})
|
|
27
|
-
const m = method().handle(cb)
|
|
28
|
-
expect(m.callback).toBe(cb)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it("preserves schemas through handle chain", () => {
|
|
32
|
-
const input = z.object({x: z.string()})
|
|
33
|
-
const output = z.object({y: z.number()})
|
|
34
|
-
const cb = async () => ({y: 1})
|
|
35
|
-
const m = method().input(input).output(output).handle(cb)
|
|
36
|
-
expect(m.bodySchema).toBe(input)
|
|
37
|
-
expect(m.outputSchema).toBe(output)
|
|
38
|
-
expect(m.callback).toBe(cb)
|
|
39
|
-
})
|
|
40
|
-
})
|
package/tests/response.test.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import {Response} from "#src/response"
|
|
2
|
-
import {describe, expect, it} from "vitest"
|
|
3
|
-
|
|
4
|
-
describe("Response", () => {
|
|
5
|
-
it("serializes json bodies", () => {
|
|
6
|
-
expect(
|
|
7
|
-
Response.json(201, {ok: true})
|
|
8
|
-
.header("X-Test", "1")
|
|
9
|
-
.toAPIGatewayProxyResult(),
|
|
10
|
-
).toEqual({
|
|
11
|
-
statusCode: 201,
|
|
12
|
-
body: JSON.stringify({ok: true}),
|
|
13
|
-
headers: {
|
|
14
|
-
"Content-Type": "application/json; charset=utf-8",
|
|
15
|
-
"X-Test": "1",
|
|
16
|
-
},
|
|
17
|
-
})
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
it("serializes text bodies", () => {
|
|
21
|
-
expect(Response.text(400, "nope").toAPIGatewayProxyResult()).toEqual({
|
|
22
|
-
statusCode: 400,
|
|
23
|
-
body: "nope",
|
|
24
|
-
headers: {
|
|
25
|
-
"Content-Type": "text/plain; charset=utf-8",
|
|
26
|
-
},
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
})
|
package/tsconfig.build.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"module": "esnext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["esnext"],
|
|
7
|
-
"types": ["node"],
|
|
8
|
-
"strict": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"noUncheckedIndexedAccess": true,
|
|
11
|
-
"noImplicitOverride": true,
|
|
12
|
-
"noImplicitAny": true,
|
|
13
|
-
"noPropertyAccessFromIndexSignature": true,
|
|
14
|
-
"exactOptionalPropertyTypes": true,
|
|
15
|
-
"noFallthroughCasesInSwitch": true,
|
|
16
|
-
"noImplicitReturns": true,
|
|
17
|
-
"noUnusedLocals": true,
|
|
18
|
-
"noUnusedParameters": true,
|
|
19
|
-
"declaration": true,
|
|
20
|
-
"emitDeclarationOnly": true,
|
|
21
|
-
"sourceMap": true,
|
|
22
|
-
"rootDir": "./src/",
|
|
23
|
-
"outDir": "./dist/"
|
|
24
|
-
},
|
|
25
|
-
"include": ["./src/"]
|
|
26
|
-
}
|
package/tsconfig.json
DELETED
package/tsconfig.node.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "esnext",
|
|
4
|
-
"module": "esnext",
|
|
5
|
-
"moduleResolution": "bundler",
|
|
6
|
-
"lib": ["esnext"],
|
|
7
|
-
"types": ["node", "vitest/globals"],
|
|
8
|
-
"strict": true,
|
|
9
|
-
"skipLibCheck": true,
|
|
10
|
-
"noUncheckedIndexedAccess": true,
|
|
11
|
-
"noImplicitOverride": true,
|
|
12
|
-
"noImplicitAny": true,
|
|
13
|
-
"noPropertyAccessFromIndexSignature": true,
|
|
14
|
-
"exactOptionalPropertyTypes": false,
|
|
15
|
-
"noFallthroughCasesInSwitch": true,
|
|
16
|
-
"noImplicitReturns": true,
|
|
17
|
-
"noUnusedLocals": true,
|
|
18
|
-
"noUnusedParameters": true,
|
|
19
|
-
"noEmit": true,
|
|
20
|
-
"rootDir": "."
|
|
21
|
-
},
|
|
22
|
-
"include": ["."],
|
|
23
|
-
"exclude": ["./src/", "./dist/"]
|
|
24
|
-
}
|