nextlove 0.2.0

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/.eslintrc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "next/core-web-vitals"
3
+ }
package/README.md ADDED
@@ -0,0 +1,6 @@
1
+ # NextJS API
2
+
3
+ This repo consists of NextJS utility modules used by Seam, namely:
4
+ - nextjs-server-modules
5
+ - withRouteSpec
6
+ - nextjs-exception-middleware
package/bin.js ADDED
@@ -0,0 +1,28 @@
1
+ #! /usr/bin/node
2
+
3
+ const argv = require("minimist")(process.argv.slice(2))
4
+ const { register } = require("esbuild-register/dist/node")
5
+
6
+ const { unregister } = register({
7
+ target: `node${process.version.slice(1)}`,
8
+ })
9
+
10
+ if (argv._[0] === "generate-openapi") {
11
+ if (argv._.length === 2) {
12
+ argv.packageDir = argv._[1]
13
+ }
14
+ if (argv["package-dir"]) {
15
+ argv.packageDir = argv["package-dir"]
16
+ }
17
+ if (!argv["packageDir"]) throw new Error("Missing --packageDir")
18
+
19
+ require("./dist/generate-openapi")
20
+ .generateOpenAPI({
21
+ packageDir: argv["packageDir"],
22
+ })
23
+ .then((result) => {
24
+ if (!argv.outputFile) {
25
+ console.log(result)
26
+ }
27
+ })
28
+ }
package/next-env.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
package/next.config.js ADDED
@@ -0,0 +1,6 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ }
5
+
6
+ module.exports = nextConfig
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "nextlove",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "repository": "https://github.com/seamapi/nextlove",
6
+ "publishConfig": {
7
+ "access": "public",
8
+ "registry": "https://registry.npmjs.org/"
9
+ },
10
+ "bin": {
11
+ "nsm": "node_modules/nextjs-server-modules/bin.js",
12
+ "nextlove": "bin.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "scripts": {
16
+ "test": "tsc --noEmit",
17
+ "build": "tsup --dts --sourcemap inline src"
18
+ },
19
+ "dependencies": {
20
+ "esbuild": "^0.14.7",
21
+ "esbuild-runner": "^2.2.1",
22
+ "lodash": "^4.17.21",
23
+ "next": "12.2.0",
24
+ "nextjs-exception-middleware": "^1.1.2",
25
+ "nextjs-middleware-wrappers": "^1.3.0",
26
+ "nextjs-server-modules": "^1.7.0",
27
+ "react": "18.2.0",
28
+ "react-dom": "18.2.0",
29
+ "zod": "^3.17.3"
30
+ },
31
+ "devDependencies": {
32
+ "@anatine/zod-openapi": "^1.10.0",
33
+ "@types/lodash": "^4.14.182",
34
+ "@types/node": "18.0.0",
35
+ "@types/react": "18.0.14",
36
+ "@types/react-dom": "18.0.5",
37
+ "chalk": "^5.1.2",
38
+ "esbuild-register": "^3.3.3",
39
+ "eslint": "8.18.0",
40
+ "eslint-config-next": "12.2.0",
41
+ "expect-type": "^0.15.0",
42
+ "minimist": "^1.2.7",
43
+ "nextjs-middleware-wrappers": "^1.3.0",
44
+ "openapi3-ts": "^3.1.1",
45
+ "ts-toolbelt": "^9.6.0",
46
+ "turbo": "^1.3.1",
47
+ "type-fest": "^3.1.0",
48
+ "typescript": "4.7.4"
49
+ }
50
+ }
@@ -0,0 +1,224 @@
1
+ import fs from "node:fs/promises"
2
+ import path from "node:path"
3
+ import globby from "globby"
4
+ import { generateSchema } from "@anatine/zod-openapi"
5
+ import {
6
+ OpenApiBuilder,
7
+ OperationObject,
8
+ ParameterObject,
9
+ SecurityRequirementObject,
10
+ } from "openapi3-ts"
11
+ import { RouteSpec, SetupParams } from "../types"
12
+ import { Entries } from "type-fest"
13
+ import chalk from "chalk"
14
+
15
+ export const defaultMapFilePathToHTTPRoute = (file_path: string) => {
16
+ const route = file_path.replace(/^\.\/pages\/api\//, "")
17
+ return route.replace(/\.ts$/, "").replace("public", "")
18
+ }
19
+
20
+ const getSecurityObject = (
21
+ auth_type: RouteSpec["auth"]
22
+ ): SecurityRequirementObject[] => {
23
+ switch (auth_type) {
24
+ case "key_or_session":
25
+ return [
26
+ {
27
+ apiKeyAuth: [],
28
+ },
29
+ {
30
+ sessionAuth: [],
31
+ },
32
+ ]
33
+ case "key":
34
+ return [
35
+ {
36
+ apiKeyAuth: [],
37
+ },
38
+ ]
39
+ case "session":
40
+ return [
41
+ {
42
+ sessionAuth: [],
43
+ },
44
+ ]
45
+ case "none":
46
+ return []
47
+ default:
48
+ throw new Error(`Unknown auth type: ${auth_type}`)
49
+ }
50
+ }
51
+
52
+ interface GenerateOpenAPIOpts {
53
+ packageDir: string
54
+ outputFile?: string
55
+ pathGlob?: string
56
+ mapFilePathToHTTPRoute?: (file_path: string) => string
57
+ }
58
+
59
+ /**
60
+ * This function generates an OpenAPI spec from the Next.js API routes.
61
+ *
62
+ * You normally invoke this with `nextapi generate-openapi` in a
63
+ * "build:openapi" package.json script.
64
+ */
65
+ export async function generateOpenAPI(opts: GenerateOpenAPIOpts) {
66
+ const {
67
+ packageDir,
68
+ outputFile,
69
+ pathGlob = "/pages/api/**/*.ts",
70
+ mapFilePathToHTTPRoute = defaultMapFilePathToHTTPRoute,
71
+ } = opts
72
+
73
+ // Load all route specs
74
+ console.log(`searching "${packageDir}${pathGlob}"...`)
75
+ const filepaths = await globby(`${packageDir}${pathGlob}`)
76
+ console.log(`found ${filepaths.length} files`)
77
+ const filepathToRouteFn = new Map<
78
+ string,
79
+ {
80
+ setupParams: SetupParams
81
+ routeSpec: RouteSpec
82
+ routeFn: Function
83
+ }
84
+ >()
85
+
86
+ await Promise.all(
87
+ filepaths.map(async (p) => {
88
+ const { default: routeFn } = await require(path.resolve(p))
89
+
90
+ if (routeFn) {
91
+ if (!routeFn._setupParams) {
92
+ console.warn(
93
+ chalk.yellow(
94
+ `Ignoring "${p}" because it wasn't created with withRouteSpec`
95
+ )
96
+ )
97
+ return
98
+ }
99
+ filepathToRouteFn.set(p, {
100
+ setupParams: routeFn._setupParams,
101
+ routeSpec: routeFn._routeSpec,
102
+ routeFn,
103
+ })
104
+ } else {
105
+ console.warn(chalk.yellow(`Couldn't find route ${p}`))
106
+ }
107
+ })
108
+ )
109
+
110
+ // TODO detect if there are multiple setups and output different APIs
111
+ const { setupParams: globalSetupParams } = filepathToRouteFn.values().next()
112
+ .value as { setupParams: SetupParams }
113
+
114
+ const securitySchemes = {}
115
+ const securityObjectsForAuthType = {}
116
+ for (const authName of Object.keys(globalSetupParams.authMiddlewareMap)) {
117
+ const mw = globalSetupParams.authMiddlewareMap[authName]
118
+ if (mw.securitySchema) {
119
+ securitySchemes[authName] = (mw as any).securitySchema
120
+ } else {
121
+ console.warn(
122
+ chalk.yellow(
123
+ `Authentication middleware "${authName}" has no securitySchema. You can define this on the function (e.g. after the export do... \n\nmyMiddleware.securitySchema = {\n type: "http"\n scheme: "bearer"\n bearerFormat: "JWT"\n // or API Token etc.\n}\n\nYou can also define "securityObjects" this way, if you want to make the endpoint support multiple modes of authentication.\n\n`
124
+ )
125
+ )
126
+ }
127
+
128
+ securityObjectsForAuthType[authName] = (mw as any).securityObjects || [
129
+ {
130
+ [authName]: [],
131
+ },
132
+ ]
133
+ }
134
+
135
+ // Build OpenAPI spec
136
+ const builder = OpenApiBuilder.create({
137
+ openapi: "3.0.0",
138
+ info: {
139
+ title: globalSetupParams.apiName,
140
+ version: "1.0.0",
141
+ },
142
+ servers: [
143
+ {
144
+ url: globalSetupParams.productionServerUrl || "https://example.com",
145
+ },
146
+ ],
147
+ paths: {},
148
+ components: {
149
+ securitySchemes,
150
+ },
151
+ })
152
+
153
+ for (const [file_path, { setupParams, routeSpec }] of filepathToRouteFn) {
154
+ const route: OperationObject = {
155
+ summary: mapFilePathToHTTPRoute(file_path),
156
+ responses: {
157
+ 200: {
158
+ description: "OK",
159
+ },
160
+ 400: {
161
+ description: "Bad Request",
162
+ },
163
+ 401: {
164
+ description: "Unauthorized",
165
+ },
166
+ },
167
+ security: securityObjectsForAuthType[routeSpec.auth],
168
+ }
169
+
170
+ if (routeSpec.jsonBody || routeSpec.commonParams) {
171
+ route.requestBody = {
172
+ content: {
173
+ "application/json": {
174
+ schema: generateSchema(
175
+ (routeSpec.jsonBody as any) || routeSpec.commonParams
176
+ ),
177
+ },
178
+ },
179
+ }
180
+ }
181
+
182
+ if (routeSpec.queryParams) {
183
+ const schema = generateSchema(routeSpec.queryParams)
184
+
185
+ if (schema.properties) {
186
+ const parameters: ParameterObject[] = Object.keys(
187
+ schema.properties as any
188
+ ).map((name) => {
189
+ return {
190
+ name,
191
+ in: "query",
192
+ schema: schema.properties![name],
193
+ required: schema.required?.includes(name),
194
+ }
195
+ })
196
+
197
+ route.parameters = parameters
198
+ }
199
+ }
200
+
201
+ if (routeSpec.jsonResponse) {
202
+ route.responses[200].content = {
203
+ "application/json": {
204
+ schema: generateSchema(routeSpec.jsonResponse),
205
+ },
206
+ }
207
+ }
208
+
209
+ // Some routes accept multiple methods
210
+ builder.addPath(mapFilePathToHTTPRoute(file_path), {
211
+ ...routeSpec.methods
212
+ .map((method) => ({
213
+ [method.toLowerCase()]: route,
214
+ }))
215
+ .reduceRight((acc, cur) => ({ ...acc, ...cur }), {}),
216
+ })
217
+ }
218
+
219
+ if (outputFile) {
220
+ await fs.writeFile(outputFile, builder.getSpecAsJson())
221
+ }
222
+
223
+ return builder.getSpecAsJson()
224
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "nextjs-exception-middleware"
2
+ export * from "./with-route-spec"
3
+ export { wrappers } from "nextjs-middleware-wrappers"
4
+ export * from "./types"
@@ -0,0 +1,107 @@
1
+ import { NextApiResponse, NextApiRequest } from "next"
2
+ import { Middleware as WrapperMiddleware } from "nextjs-middleware-wrappers"
3
+ import { z } from "zod"
4
+ import { HTTPMethods } from "../with-route-spec/middlewares/with-methods"
5
+ import { SecuritySchemeObject, SecurityRequirementObject } from "openapi3-ts"
6
+
7
+ export type Middleware<T, Dep = {}> = WrapperMiddleware<T, Dep> & {
8
+ securitySchema?: SecuritySchemeObject
9
+ securityObjects?: SecurityRequirementObject[]
10
+ }
11
+
12
+ type ParamDef = z.ZodTypeAny | z.ZodEffects<z.ZodTypeAny>
13
+
14
+ export interface RouteSpec<
15
+ Auth extends string = string,
16
+ Methods extends HTTPMethods[] = any,
17
+ JsonBody extends ParamDef = z.ZodTypeAny,
18
+ QueryParams extends ParamDef = z.ZodTypeAny,
19
+ CommonParams extends ParamDef = z.ZodTypeAny,
20
+ Middlewares extends readonly Middleware<any, any>[] = any[],
21
+ JsonResponse extends ParamDef = z.ZodTypeAny
22
+ > {
23
+ methods: Methods
24
+ auth: Auth
25
+ jsonBody?: JsonBody
26
+ queryParams?: QueryParams
27
+ commonParams?: CommonParams
28
+ middlewares?: Middlewares
29
+ jsonResponse?: JsonResponse
30
+ }
31
+
32
+ export type MiddlewareChainOutput<
33
+ MWChain extends readonly Middleware<any, any>[]
34
+ > = MWChain extends readonly []
35
+ ? {}
36
+ : MWChain extends readonly [infer First, ...infer Rest]
37
+ ? First extends Middleware<infer T, any>
38
+ ? T &
39
+ (Rest extends readonly Middleware<any, any>[]
40
+ ? MiddlewareChainOutput<Rest>
41
+ : never)
42
+ : never
43
+ : never
44
+
45
+ export type AuthMiddlewares = {
46
+ [auth_type: string]: Middleware<any, any>
47
+ }
48
+
49
+ export interface SetupParams<
50
+ AuthMW extends AuthMiddlewares = AuthMiddlewares,
51
+ GlobalMW extends Middleware<any, any>[] = any[]
52
+ > {
53
+ authMiddlewareMap: AuthMW
54
+ globalMiddlewares: GlobalMW
55
+ exceptionHandlingMiddleware?: ((next: Function) => Function) | null
56
+
57
+ // These improve OpenAPI generation
58
+ apiName: string
59
+ productionServerUrl: string
60
+ }
61
+
62
+ const defaultMiddlewareMap = {
63
+ none: (next) => next,
64
+ } as const
65
+
66
+ export type RouteFunction<
67
+ SP extends SetupParams<AuthMiddlewares>,
68
+ RS extends RouteSpec
69
+ > = (
70
+ req: (SP["authMiddlewareMap"] &
71
+ typeof defaultMiddlewareMap)[RS["auth"]] extends Middleware<
72
+ infer AuthMWOut,
73
+ any
74
+ >
75
+ ? Omit<NextApiRequest, "query" | "body"> &
76
+ AuthMWOut &
77
+ MiddlewareChainOutput<
78
+ RS["middlewares"] extends readonly Middleware<any, any>[]
79
+ ? [...SP["globalMiddlewares"], ...RS["middlewares"]]
80
+ : SP["globalMiddlewares"]
81
+ > & {
82
+ body: (RS["jsonBody"] extends z.ZodTypeAny
83
+ ? z.infer<RS["jsonBody"]>
84
+ : {}) &
85
+ (RS["commonParams"] extends z.ZodTypeAny
86
+ ? z.infer<RS["commonParams"]>
87
+ : {})
88
+ query: (RS["queryParams"] extends z.ZodTypeAny
89
+ ? z.infer<RS["queryParams"]>
90
+ : {}) &
91
+ (RS["commonParams"] extends z.ZodTypeAny
92
+ ? z.infer<RS["commonParams"]>
93
+ : {})
94
+ }
95
+ : `unknown auth type: ${RS["auth"]}. You should configure this auth type in your auth_middlewares w/ createWithRouteSpec, or maybe you need to add "as const" to your route spec definition.`,
96
+ res: NextApiResponse<
97
+ RS["jsonResponse"] extends z.ZodTypeAny ? z.infer<RS["jsonResponse"]> : any
98
+ >
99
+ ) => Promise<void>
100
+
101
+ export type CreateWithRouteSpecFunction = <
102
+ SP extends SetupParams<AuthMiddlewares, any>
103
+ >(
104
+ setupParams: SP
105
+ ) => <RS extends RouteSpec<any, any, any, any, any, any, any>>(
106
+ route_spec: RS
107
+ ) => (next: RouteFunction<SP, RS>) => any
@@ -0,0 +1,96 @@
1
+ import { NextApiResponse, NextApiRequest } from "next"
2
+ import { withExceptionHandling } from "nextjs-exception-middleware"
3
+ import wrappers, { Middleware } from "nextjs-middleware-wrappers"
4
+ import { CreateWithRouteSpecFunction, RouteSpec } from "../types"
5
+ import withMethods, { HTTPMethods } from "./middlewares/with-methods"
6
+ import withValidation from "./middlewares/with-validation"
7
+ import { z } from "zod"
8
+
9
+ type ParamDef = z.ZodTypeAny | z.ZodEffects<z.ZodTypeAny>
10
+
11
+ export const checkRouteSpec = <
12
+ AuthType extends string = string,
13
+ Methods extends HTTPMethods[] = HTTPMethods[],
14
+ JsonBody extends ParamDef = z.ZodTypeAny,
15
+ QueryParams extends ParamDef = z.ZodTypeAny,
16
+ CommonParams extends ParamDef = z.ZodTypeAny,
17
+ Middlewares extends readonly Middleware<any, any>[] = readonly Middleware<
18
+ any,
19
+ any
20
+ >[],
21
+ Spec extends RouteSpec<
22
+ AuthType,
23
+ Methods,
24
+ JsonBody,
25
+ QueryParams,
26
+ CommonParams,
27
+ Middlewares
28
+ > = RouteSpec<
29
+ AuthType,
30
+ Methods,
31
+ JsonBody,
32
+ QueryParams,
33
+ CommonParams,
34
+ Middlewares
35
+ >
36
+ >(
37
+ spec: Spec
38
+ ): string extends Spec["auth"]
39
+ ? `your route spec is underspecified, add "as const"`
40
+ : Spec => spec as any
41
+
42
+ export const createWithRouteSpec: CreateWithRouteSpecFunction = ((
43
+ setupParams
44
+ ) => {
45
+ const {
46
+ authMiddlewareMap = {},
47
+ globalMiddlewares = [],
48
+ exceptionHandlingMiddleware = withExceptionHandling({
49
+ addOkStatus: true,
50
+ }) as any,
51
+ } = setupParams
52
+
53
+ const withRouteSpec = (spec: RouteSpec) => {
54
+ const createRouteExport = (userDefinedRouteFn) => {
55
+ const rootRequestHandler = async (
56
+ req: NextApiRequest,
57
+ res: NextApiResponse
58
+ ) => {
59
+ authMiddlewareMap["none"] = (next) => next
60
+
61
+ const auth_middleware = authMiddlewareMap[spec.auth]
62
+ if (!auth_middleware) throw new Error(`Unknown auth type: ${spec.auth}`)
63
+
64
+ return wrappers(
65
+ ...((exceptionHandlingMiddleware
66
+ ? [exceptionHandlingMiddleware]
67
+ : []) as [any]),
68
+ ...((globalMiddlewares || []) as []),
69
+ auth_middleware,
70
+ ...((spec.middlewares || []) as []),
71
+ withMethods(spec.methods),
72
+ withValidation({
73
+ jsonBody: spec.jsonBody,
74
+ queryParams: spec.queryParams,
75
+ commonParams: spec.commonParams,
76
+ }),
77
+ userDefinedRouteFn
78
+ )(req as any, res)
79
+ }
80
+
81
+ rootRequestHandler._setupParams = setupParams
82
+ rootRequestHandler._routeSpec = spec
83
+
84
+ return rootRequestHandler
85
+ }
86
+
87
+ createRouteExport._setupParams = setupParams
88
+ createRouteExport._routeSpec = spec
89
+
90
+ return createRouteExport
91
+ }
92
+
93
+ withRouteSpec._setupParams = setupParams
94
+
95
+ return withRouteSpec
96
+ }) as any
@@ -0,0 +1,22 @@
1
+ import { MethodNotAllowedException } from "nextjs-exception-middleware"
2
+
3
+ export type HTTPMethods =
4
+ | "GET"
5
+ | "POST"
6
+ | "DELETE"
7
+ | "PUT"
8
+ | "PATCH"
9
+ | "HEAD"
10
+ | "OPTIONS"
11
+
12
+ export const withMethods = (methods: HTTPMethods[]) => (next) => (req, res) => {
13
+ if (!methods.includes(req.method)) {
14
+ throw new MethodNotAllowedException({
15
+ type: "method_not_allowed",
16
+ message: `only ${methods.join(",")} accepted`,
17
+ })
18
+ }
19
+ return next(req, res)
20
+ }
21
+
22
+ export default withMethods
@@ -0,0 +1,101 @@
1
+ import type { NextApiRequest, NextApiResponse } from "next"
2
+ import { z } from "zod"
3
+ import { BadRequestException } from "nextjs-exception-middleware"
4
+ import { isEmpty } from "lodash"
5
+
6
+ const parseCommaSeparateArrays = (
7
+ schema: z.ZodTypeAny,
8
+ input: Record<string, unknown>
9
+ ) => {
10
+ const parsed_input = Object.assign({}, input)
11
+
12
+ // todo: iterate over Zod top level keys, if there's an array, parse it
13
+
14
+ return schema.parse(parsed_input)
15
+ }
16
+
17
+ export interface RequestInput<
18
+ JsonBody extends z.ZodTypeAny,
19
+ QueryParams extends z.ZodTypeAny,
20
+ CommonParams extends z.ZodTypeAny
21
+ > {
22
+ jsonBody?: JsonBody
23
+ queryParams?: QueryParams
24
+ commonParams?: CommonParams
25
+ }
26
+
27
+ const zodIssueToString = (issue: z.ZodIssue) => {
28
+ if (issue.path.join(".") === "") {
29
+ return issue.message
30
+ }
31
+ if (issue.message === "Required") {
32
+ return `${issue.path.join(".")} is required`
33
+ }
34
+ return `${issue.message} for "${issue.path.join(".")}"`
35
+ }
36
+
37
+ export const withValidation =
38
+ <
39
+ JsonBody extends z.ZodTypeAny,
40
+ QueryParams extends z.ZodTypeAny,
41
+ CommonParams extends z.ZodTypeAny
42
+ >(
43
+ input: RequestInput<JsonBody, QueryParams, CommonParams>
44
+ ) =>
45
+ (next) =>
46
+ async (req: NextApiRequest, res: NextApiResponse) => {
47
+ if (
48
+ (req.method === "POST" || req.method === "PATCH") &&
49
+ !req.headers["content-type"]?.includes("application/json") &&
50
+ !isEmpty(req.body)
51
+ ) {
52
+ throw new BadRequestException({
53
+ type: "invalid_content_type",
54
+ message: `POST requests must have Content-Type header with "application/json"`,
55
+ })
56
+ }
57
+
58
+ try {
59
+ const original_combined_params = { ...req.query, ...req.body }
60
+ req.body = input.jsonBody?.parse(req.body)
61
+ req.query = input.queryParams?.parse(req.query)
62
+
63
+ if (input.commonParams) {
64
+ ;(req as any).commonParams = parseCommaSeparateArrays(
65
+ input.commonParams,
66
+ original_combined_params
67
+ )
68
+ }
69
+ } catch (error: any) {
70
+ if (error.name === "ZodError") {
71
+ let message
72
+ if (error.issues.length === 1) {
73
+ const issue = error.issues[0]
74
+ message = zodIssueToString(issue)
75
+ } else {
76
+ const message_components: string[] = []
77
+ for (const issue of error.issues) {
78
+ message_components.push(zodIssueToString(issue))
79
+ }
80
+ message =
81
+ `${error.issues.length} Input Errors: ` +
82
+ message_components.join(", ")
83
+ }
84
+
85
+ throw new BadRequestException({
86
+ type: "invalid_input",
87
+ message,
88
+ validation_errors: error.format(),
89
+ })
90
+ }
91
+
92
+ throw new BadRequestException({
93
+ type: "invalid_input",
94
+ message: "Error while parsing input",
95
+ })
96
+ }
97
+
98
+ return next(req, res)
99
+ }
100
+
101
+ export default withValidation
@@ -0,0 +1,154 @@
1
+ import { MiddlewareChainOutput } from "../src/types"
2
+ import {
3
+ checkRouteSpec,
4
+ createWithRouteSpec,
5
+ Middleware,
6
+ RouteSpec,
7
+ } from "../src"
8
+ import { expectTypeOf } from "expect-type"
9
+ import { z } from "zod"
10
+
11
+ const authTokenMiddleware: Middleware<{
12
+ auth: {
13
+ authorized_by: "auth_token"
14
+ }
15
+ }> = (next) => (req, res) => {
16
+ req.auth = {
17
+ authorized_by: "auth_token",
18
+ }
19
+ return next(req, res)
20
+ }
21
+ const bearerMiddleware: Middleware<{
22
+ auth: {
23
+ authorized_by: "bearer"
24
+ }
25
+ }> = (next) => (req, res) => {
26
+ req.auth = {
27
+ authorized_by: "bearer",
28
+ }
29
+ return next(req, res)
30
+ }
31
+
32
+ const dbMiddleware: Middleware<{
33
+ db: {
34
+ client: any
35
+ }
36
+ }> = (next) => (req, res) => {
37
+ req.db = { client: "..." }
38
+ return next(req, res)
39
+ }
40
+
41
+ const userMiddleware: Middleware<
42
+ {
43
+ user: {
44
+ user_id: string
45
+ }
46
+ },
47
+ {}
48
+ > = (next) => (req, res) => {
49
+ req.user = { user_id: "..." }
50
+ return next(req, res)
51
+ }
52
+
53
+ const chain = [dbMiddleware, bearerMiddleware] as const
54
+
55
+ const chainOutput: MiddlewareChainOutput<typeof chain> = null as any
56
+
57
+ expectTypeOf(chainOutput).toEqualTypeOf<{
58
+ db: {
59
+ client: any
60
+ }
61
+ auth: {
62
+ authorized_by: "bearer"
63
+ }
64
+ }>
65
+
66
+ const projSetup = {
67
+ authMiddlewareMap: {
68
+ auth_token: authTokenMiddleware,
69
+ bearer: bearerMiddleware,
70
+ },
71
+ globalMiddlewares: [dbMiddleware],
72
+
73
+ apiName: "test",
74
+ productionServerUrl: "https://example.com",
75
+ } as const
76
+
77
+ const withRouteSpec = createWithRouteSpec(projSetup)
78
+
79
+ export const myRoute1Spec = checkRouteSpec({
80
+ auth: "none",
81
+ methods: ["POST"],
82
+ jsonBody: z.object({
83
+ count: z.number(),
84
+ }),
85
+ })
86
+
87
+ export const myRoute1 = withRouteSpec(myRoute1Spec)(async (req, res) => {
88
+ expectTypeOf(req.db).toMatchTypeOf<{ client: any }>()
89
+ expectTypeOf(req.body).toMatchTypeOf<{ count: number }>()
90
+ })
91
+
92
+ export const myRoute2Spec = checkRouteSpec({
93
+ auth: "auth_token",
94
+ methods: ["GET"],
95
+ queryParams: z.object({
96
+ id: z.string(),
97
+ }),
98
+ })
99
+
100
+ export const myRoute2 = withRouteSpec(myRoute2Spec)(async (req, res) => {
101
+ expectTypeOf(req.auth).toMatchTypeOf<{ authorized_by: "auth_token" }>()
102
+ expectTypeOf(req.query).toMatchTypeOf<{ id: string }>()
103
+ })
104
+
105
+ export const myRoute3Spec = checkRouteSpec({
106
+ auth: "none",
107
+ methods: ["POST"],
108
+ jsonBody: z.object({
109
+ A: z.string(),
110
+ }),
111
+ commonParams: z.object({
112
+ B: z.string(),
113
+ }),
114
+ })
115
+
116
+ export const myRoute3 = withRouteSpec(myRoute3Spec)(async (req, res) => {
117
+ expectTypeOf(req.body).toMatchTypeOf<{ A: string; B: string }>()
118
+ })
119
+
120
+ const withImproperMWRouteSpec = createWithRouteSpec({
121
+ authMiddlewareMap: {
122
+ // @ts-expect-error - improperly defined middleware
123
+ asd: () => {},
124
+ },
125
+ globalMiddlewares: [],
126
+ })
127
+
128
+ const middlewares = [userMiddleware] as const
129
+
130
+ export const myRoute4Spec = checkRouteSpec({
131
+ auth: "none",
132
+ methods: ["POST"],
133
+ middlewares,
134
+ })
135
+
136
+ export const myRoute4 = withRouteSpec(myRoute4Spec)(async (req, res) => {
137
+ expectTypeOf(req.db).toMatchTypeOf<{ client: any }>()
138
+ expectTypeOf(req.user).toMatchTypeOf<{ user_id: string }>()
139
+ })
140
+
141
+ export const myRoute5Spec = checkRouteSpec({
142
+ auth: "none",
143
+ methods: ["POST"],
144
+ jsonResponse: z.object({
145
+ id: z.string(),
146
+ }),
147
+ })
148
+
149
+ export const myRoute5 = withRouteSpec(myRoute5Spec)(async (req, res) => {
150
+ // @ts-expect-error - should be a string
151
+ res.status(200).json({ id: 123 })
152
+
153
+ res.status(200).json({ id: "123" })
154
+ })
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2020",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noImplicitAny": false,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "noEmit": true,
11
+ "esModuleInterop": true,
12
+ "module": "esnext",
13
+ "moduleResolution": "node",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "jsx": "preserve",
17
+ "incremental": false
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
20
+ "exclude": ["node_modules"]
21
+ }