Skip to content

Commit

Permalink
Merge pull request #95 from seamapi/components-schema
Browse files Browse the repository at this point in the history
add components schemas to openapi generator
  • Loading branch information
itelo authored Jul 25, 2023
2 parents 5c66593 + f9f0754 commit 063b558
Show file tree
Hide file tree
Showing 14 changed files with 103 additions and 32 deletions.
13 changes: 13 additions & 0 deletions apps/example-todo-app/lib/middlewares/with-route-spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { createWithRouteSpec } from "nextlove"
import { withAuthToken } from "./with-auth-token"
export { checkRouteSpec } from "nextlove"
import * as ZT from "lib/zod"

export const withRouteSpec = createWithRouteSpec({
authMiddlewareMap: { auth_token: withAuthToken },
globalMiddlewares: [],
apiName: "TODO API",
productionServerUrl: "https://example.com",
shouldValidateResponses: true,
globalSchemas: {
todo: ZT.todo,
ok: ZT.ok,
},
} as const)

export const withRouteSpecWithoutValidateGetRequestBody = createWithRouteSpec({
Expand All @@ -16,11 +21,19 @@ export const withRouteSpecWithoutValidateGetRequestBody = createWithRouteSpec({
apiName: "TODO API",
productionServerUrl: "https://example.com",
shouldValidateGetRequestBody: false,
globalSchemas: {
todo: ZT.todo,
ok: ZT.ok,
},
} as const)

export const withRouteSpecWithoutValidateResponse = createWithRouteSpec({
authMiddlewareMap: { auth_token: withAuthToken },
globalMiddlewares: [],
apiName: "TODO API",
productionServerUrl: "https://example.com",
globalSchemas: {
todo: ZT.todo,
ok: ZT.ok,
},
} as const)
7 changes: 7 additions & 0 deletions apps/example-todo-app/lib/zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from "zod"

export const todo = z.object({
id: z.string().uuid(),
})

export const ok = z.boolean()
5 changes: 3 additions & 2 deletions apps/example-todo-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"scripts": {
"dev": "next dev",
"build": "rimraf dist && nsm build && tsup ./index.ts --dts --sourcemap && cpy .next dist/.next",
"build:openapi": "nextlove generate-openapi --packageDir . --apiName 'Example TODO API'",
"build:type": "nextlove generate-route-types --packageDir .",
"build:openapi": "nextlove-generators generate-openapi --packageDir . --apiName 'Example TODO API'",
"build:type": "nextlove-generators generate-route-types --packageDir .",
"start": "next start",
"lint": "next lint",
"test": "ava"
Expand All @@ -18,6 +18,7 @@
"debug": "^4.3.4",
"escape-string-regexp": "4.0.0",
"eslint-plugin-nextlove": "*",
"nextlove-generators": "*",
"glob-promise": "4.2.2",
"micro": "9.3.4",
"mkdirp": "1.0.4",
Expand Down
5 changes: 2 additions & 3 deletions apps/example-todo-app/pages/api/todo/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { checkRouteSpec, withRouteSpec } from "lib/middlewares"
import { NotFoundException } from "nextlove"
import { TODO_ID } from "tests/fixtures"
import { z } from "zod"
import * as ZT from "lib/zod"

export const queryParams = z.object({
id: z.string().uuid(),
Expand All @@ -18,9 +19,7 @@ export const route_spec = checkRouteSpec({
queryParams,
jsonResponse: z.object({
ok: z.boolean(),
todo: z.object({
id: z.string().uuid(),
}),
todo: ZT.todo,
error: z
.object({
type: z.string(),
Expand Down
5 changes: 2 additions & 3 deletions apps/example-todo-app/pages/api/todo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { checkRouteSpec, withRouteSpec } from "lib/middlewares"
import { NotFoundException } from "nextlove"
import { TODO_ID } from "tests/fixtures"
import { z } from "zod"
import * as ZT from "lib/zod"

export const queryParams = z.object({
id: z.string().uuid(),
Expand All @@ -18,9 +19,7 @@ export const route_spec = checkRouteSpec({
queryParams,
jsonResponse: z.object({
ok: z.boolean(),
todo: z.object({
id: z.string().uuid(),
}),
todo: ZT.todo,
error: z
.object({
type: z.string(),
Expand Down
7 changes: 2 additions & 5 deletions apps/example-todo-app/pages/api/todo/list-optional-ids.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { checkRouteSpec, withRouteSpec } from "lib/middlewares"
import { z } from "zod"
import * as ZT from "lib/zod"

export const commonParams = z.object({
ids: z.array(z.string().uuid()).optional(),
Expand All @@ -11,11 +12,7 @@ export const route_spec = checkRouteSpec({
commonParams,
jsonResponse: z.object({
ok: z.boolean(),
todos: z
.object({
id: z.string().uuid(),
})
.array(),
todos: z.array(ZT.todo),
}),
})

Expand Down
7 changes: 2 additions & 5 deletions apps/example-todo-app/pages/api/todo/list-with-refine.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { checkRouteSpec, withRouteSpec } from "lib/middlewares"
import { z } from "zod"
import * as ZT from "lib/zod"

export const commonParams = z
.object({
Expand Down Expand Up @@ -29,11 +30,7 @@ export const route_spec = checkRouteSpec({
commonParams,
jsonResponse: z.object({
ok: z.boolean(),
todos: z
.object({
id: z.string().uuid(),
})
.array(),
todos: z.array(ZT.todo),
}),
})

Expand Down
7 changes: 2 additions & 5 deletions apps/example-todo-app/pages/api/todo/list.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { checkRouteSpec, withRouteSpec } from "lib/middlewares"
import { z } from "zod"
import * as ZT from "lib/zod"

export const commonParams = z.object({
ids: z.array(z.string().uuid()),
Expand All @@ -11,11 +12,7 @@ export const route_spec = checkRouteSpec({
commonParams,
jsonResponse: z.object({
ok: z.boolean(),
todos: z
.object({
id: z.string().uuid(),
})
.array(),
todos: z.array(ZT.todo),
}),
})

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import _ from "lodash"

function findKeyInObj(obj: any, value: any): string | null {
let resultKey: string | null = null
_.forOwn(obj, (v, k) => {
if (_.isObject(v) && _.isMatch(v, value)) {
resultKey = k
// Stop the iteration
return false
}
})
return resultKey
}

export function embedSchemaReferences(obj1: any, obj2: any): any {
return _.transform(obj1, (result, value, key) => {
if (_.isObject(value)) {
const matchingKey = findKeyInObj(obj2, value)
if (matchingKey) {
result[key] = {
$ref: `#/components/schemas/${matchingKey}`,
}
} else {
result[key] = embedSchemaReferences(value, obj2)
}
} else {
result[key] = value
}
})
}
38 changes: 33 additions & 5 deletions packages/nextlove-generators/src/generate-openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SetupParams } from "nextlove"
import chalk from "chalk"
import { z } from "zod"
import { parseRoutesInPackage } from "../lib/parse-routes-in-package"
import { embedSchemaReferences } from "./embed-schema-references"

function transformPathToOperationId(path: string): string {
function replaceFirstCharToLowercase(str: string) {
Expand Down Expand Up @@ -106,6 +107,13 @@ export async function generateOpenAPI(opts: GenerateOpenAPIOpts) {
]
}

const globalSchemas = {}
for (const [schemaName, zodSchema] of Object.entries(
globalSetupParams.globalSchemas ?? {}
)) {
globalSchemas[schemaName] = generateSchema(zodSchema)
}

// Build OpenAPI spec
const builder = OpenApiBuilder.create({
openapi: "3.0.0",
Expand All @@ -122,6 +130,7 @@ export async function generateOpenAPI(opts: GenerateOpenAPIOpts) {
paths: {},
components: {
securitySchemes,
schemas: globalSchemas,
},
})

Expand Down Expand Up @@ -226,14 +235,33 @@ export async function generateOpenAPI(opts: GenerateOpenAPIOpts) {

const { jsonResponse } = routeSpec
const { addOkStatus = true } = setupParams

if (jsonResponse) {
if (
!jsonResponse._def ||
!jsonResponse._def.typeName ||
jsonResponse._def.typeName !== "ZodObject"
) {
console.warn(
chalk.yellow(
`Skipping route ${routePath} because the response is not a ZodObject.`
)
)
continue
}

const responseSchema = generateSchema(
addOkStatus ? jsonResponse.extend({ ok: z.boolean() }) : jsonResponse
)

const schemaWithReferences = embedSchemaReferences(
responseSchema,
globalSchemas
)

route.responses[200].content = {
"application/json": {
schema: generateSchema(
addOkStatus
? jsonResponse.extend({ ok: z.boolean() })
: jsonResponse
),
schema: schemaWithReferences,
},
}
}
Expand Down
5 changes: 1 addition & 4 deletions packages/nextlove/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@
"registry": "https://registry.npmjs.org/"
},
"bin": {
"nsm": "nsm-bin.js",
"nextlove": "bin.js"
"nsm": "nsm-bin.js"
},
"main": "./dist/index.js",
"scripts": {
Expand Down Expand Up @@ -51,8 +50,6 @@
"eslint": "8.18.0",
"eslint-config-next": "12.2.0",
"expect-type": "^0.15.0",
"globby": "11.1.0",
"minimist": "^1.2.7",
"next": "12.2.0",
"nextjs-middleware-wrappers": "^1.3.0",
"nextjs-server-modules": "^2.1.0",
Expand Down
1 change: 1 addition & 0 deletions packages/nextlove/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface SetupParams<
shouldValidateResponses?: boolean
shouldValidateGetRequestBody?: boolean
securitySchemas?: Record<string, SecuritySchemeObject>
globalSchemas?: Record<string, z.ZodTypeAny>
}

const defaultMiddlewareMap = {
Expand Down
5 changes: 5 additions & 0 deletions packages/nextlove/src/with-route-spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ export const createWithRouteSpec: CreateWithRouteSpecFunction = ((
},
},
}) as any,
globalSchemas = setupParams.addOkStatus
? {
ok: z.boolean(),
}
: {},
} = setupParams

const withRouteSpec = (spec: RouteSpec) => {
Expand Down

0 comments on commit 063b558

Please sign in to comment.