Skip to content

Commit 26240b7

Browse files
committed
fix: rebase
1 parent cf58307 commit 26240b7

File tree

9 files changed

+125
-45
lines changed

9 files changed

+125
-45
lines changed

packages/openapi-code-generator/src/typescript/common/import-builder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ export class ImportBuilder {
195195
isType: boolean,
196196
): void {
197197
// biome-ignore lint/style/noParameterAssign: normalization
198-
from = this.normalizeFrom(from)
198+
from = this.normalizeFrom(from, this.config.unit?.filename)
199199

200200
if (!this.imports[from]) {
201201
this.imports[from] = {
@@ -225,21 +225,21 @@ export class ImportBuilder {
225225
}
226226
}
227227

228-
normalizeFrom(from: string) {
228+
normalizeFrom(from: string, filename?: string) {
229229
if (!this.config.includeFileExtensions && from.endsWith(".ts")) {
230230
// biome-ignore lint/style/noParameterAssign: normalization
231231
from = from.substring(0, from.length - ".ts".length)
232232
}
233233

234234
// TODO: does this work on windows?
235-
if (this.config.unit && from.startsWith("./")) {
235+
if (filename && from.startsWith("./")) {
236236
if (this.config.importAlias) {
237237
return (
238238
this.config.importAlias + from.split(path.sep).slice(1).join(path.sep)
239239
)
240240
}
241241

242-
const unitDirname = path.dirname(this.config.unit.filename)
242+
const unitDirname = path.dirname(filename)
243243
const fromDirname = path.dirname(from)
244244

245245
const relative = path.relative(unitDirname, fromDirname)

packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-app-router-builder.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,18 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable {
9696
})
9797

9898
// Replace the params based on what inputs we have
99-
// biome-ignore lint/style/noNonNullAssertion: <explanation>
99+
// biome-ignore lint/style/noNonNullAssertion: ignore
100100
const declarations = variableDeclaration.getDeclarations()[0]!
101101
const callExpression = declarations.getInitializerIfKindOrThrow(
102102
SyntaxKind.CallExpression,
103103
)
104104

105-
// biome-ignore lint/style/noNonNullAssertion: <explanation>
105+
// biome-ignore lint/style/noNonNullAssertion: ignore
106106
const implementationFunction = callExpression
107107
.getArguments()[0]!
108108
.asKind(SyntaxKind.ArrowFunction)!
109109

110-
// biome-ignore lint/complexity/noForEach: <explanation>
110+
// biome-ignore lint/complexity/noForEach: ignore
111111
implementationFunction?.getParameters().forEach((parameter) => {
112112
parameter.remove()
113113
})
@@ -123,6 +123,10 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable {
123123
.filter(isDefined)
124124
.join(",")}}`,
125125
})
126+
} else {
127+
implementationFunction?.addParameter({
128+
name: "_params",
129+
})
126130
}
127131

128132
implementationFunction?.addParameter({name: "respond"})
@@ -169,10 +173,9 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable {
169173
const imports = this.sourceFile.getImportDeclarations()
170174
const from = this.imports.normalizeFrom(
171175
`./${this.companionFilename}`,
172-
// todo
173-
// `./${this.filename}`,
176+
`./${this.filename}`,
174177
)
175-
// biome-ignore lint/complexity/noForEach: <explanation>
178+
// biome-ignore lint/complexity/noForEach: ignore
176179
imports
177180
.filter((it) => it.getModuleSpecifierValue().includes(from))
178181
.forEach((it) => {
@@ -187,7 +190,7 @@ export class TypescriptNextjsAppRouterBuilder implements ICompilable {
187190
})
188191

189192
// Remove any methods that were removed from the spec
190-
// biome-ignore lint/complexity/noForEach: <explanation>
193+
// biome-ignore lint/complexity/noForEach: ignore
191194
this.sourceFile
192195
.getVariableDeclarations()
193196
.filter((it) => {

packages/openapi-code-generator/src/typescript/server/typescript-nextjs/typescript-nextjs-router-builder.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder {
1717
statements: string[]
1818
}[] = []
1919

20-
// biome-ignore lint/complexity/noUselessConstructor: <explanation>
20+
// biome-ignore lint/complexity/noUselessConstructor: ignore
2121
constructor(
2222
filename: string,
2323
name: string,
@@ -32,8 +32,8 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder {
3232
protected buildImports(): void {
3333
this.imports
3434
.from("@nahkies/typescript-nextjs-runtime/server")
35-
.add(
36-
"OpenAPIRuntimeResponse",
35+
.add("OpenAPIRuntimeResponse")
36+
.addType(
3737
"OpenAPIRuntimeResponder",
3838
"Params",
3939
"StatusCode2xx",
@@ -43,7 +43,7 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder {
4343
"StatusCode",
4444
)
4545

46-
this.imports.from("next/server").add("NextRequest", "NextResponse")
46+
this.imports.from("next/server").addType("NextRequest", "NextResponse")
4747

4848
this.imports
4949
.from("@nahkies/typescript-nextjs-runtime/errors")
@@ -58,10 +58,15 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder {
5858
.add("parseRequestInput", "responseValidationFactory")
5959
break
6060
}
61-
case "zod-v3":
61+
case "zod-v3": {
62+
this.imports
63+
.from("@nahkies/typescript-nextjs-runtime/zod-v3")
64+
.add("parseRequestInput", "responseValidationFactory")
65+
break
66+
}
6267
case "zod-v4": {
6368
this.imports
64-
.from("@nahkies/typescript-nextjs-runtime/zod")
69+
.from("@nahkies/typescript-nextjs-runtime/zod-v4")
6570
.add("parseRequestInput", "responseValidationFactory")
6671
break
6772
}
@@ -106,7 +111,7 @@ export class TypescriptNextjsRouterBuilder extends AbstractRouterBuilder {
106111
buildExport({
107112
name: symbols.implTypeName,
108113
value: `(${[
109-
params.hasParams ? `params: ${params.type}` : undefined,
114+
`params: ${params.type}`,
110115
`respond: ${symbols.responderName}`,
111116
"request: NextRequest",
112117
]
@@ -129,14 +134,13 @@ try {
129134
? `parseRequestInput(${params.path.name}, await params, RequestInputType.RouteParam)`
130135
: "undefined"
131136
},
132-
// TODO: this swallows repeated parameters
137+
// todo: this swallows repeated parameters
133138
query: ${
134139
params.query.schema
135140
? `parseRequestInput(${params.query.name}, Object.fromEntries(request.nextUrl.searchParams.entries()), RequestInputType.QueryString)`
136141
: "undefined"
137142
},
138-
${params.body.schema && !params.body.isSupported ? `// todo: request bodies with content-type '${params.body.contentType}' not yet supported\n` : ""}
139-
body: ${
143+
${params.body.schema && !params.body.isSupported ? `// todo: request bodies with content-type '${params.body.contentType}' not yet supported\n` : ""}body: ${
140144
params.body.schema
141145
? `parseRequestInput(${params.body.schema}, await request.json(), RequestInputType.RequestBody)${!params.body.isSupported ? " as never" : ""}`
142146
: "undefined"
@@ -150,16 +154,9 @@ try {
150154
151155
const responder = ${responder.implementation}
152156
153-
const res = await implementation(${[params.hasParams ? "input" : undefined, "responder", "request"].filter(isDefined).join(",")})
154-
.then(it => {
155-
if(it instanceof Response) {
156-
return it
157-
}
158-
const {status, body} = it.unpack()
159-
160-
return body !== undefined ? Response.json(body, {status}) : new Response(undefined, {status})
161-
})
162-
.catch(err => { throw OpenAPIRuntimeError.HandlerError(err) })
157+
const res = await implementation(input, responder, request)
158+
.then(OpenAPIRuntimeResponse.unwrap)
159+
.catch(OpenAPIRuntimeError.wrapped(OpenAPIRuntimeError.HandlerError))
163160
164161
return res
165162
} catch (err) {

packages/typescript-nextjs-runtime/package.json

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@
3232
"import": "./dist/joi.js",
3333
"types": "./dist/joi.d.ts"
3434
},
35-
"./zod": {
36-
"require": "./dist/zod.js",
37-
"import": "./dist/zod.js",
38-
"types": "./dist/zod.d.ts"
35+
"./zod-v3": {
36+
"require": "./dist/zod-v3.js",
37+
"import": "./dist/zod-v3.js",
38+
"types": "./dist/zod-v3.d.ts"
39+
},
40+
"./zod-v4": {
41+
"require": "./dist/zod-v4.js",
42+
"import": "./dist/zod-v4.js",
43+
"types": "./dist/zod-v4.d.ts"
3944
}
4045
},
4146
"scripts": {
@@ -44,9 +49,9 @@
4449
"test": "jest"
4550
},
4651
"peerDependencies": {
47-
"joi": "^17.1.1",
52+
"joi": "^17.13.3 || ^18.0.1",
4853
"next": "^16.1.0",
49-
"zod": "^3.20.6"
54+
"zod": "^3.25.74 || ^4.1.12"
5055
},
5156
"peerDependenciesMeta": {
5257
"joi": {
@@ -57,9 +62,9 @@
5762
}
5863
},
5964
"devDependencies": {
60-
"jest": "^30.0.4",
61-
"joi": "^17.13.3",
62-
"typescript": "~5.8.3",
65+
"jest": "^30.2.0",
66+
"joi": "^18.0.2",
67+
"typescript": "^5.9.3",
6368
"zod": "^3.25.74"
6469
},
6570
"files": [

packages/typescript-nextjs-runtime/src/errors.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,10 @@ export class OpenAPIRuntimeError extends Error {
4747
static isOpenAPIError(err: unknown): err is OpenAPIRuntimeError {
4848
return err instanceof OpenAPIRuntimeError
4949
}
50+
51+
static wrapped(fn: (err: unknown) => OpenAPIRuntimeError) {
52+
return (err: unknown) => {
53+
throw fn(err)
54+
}
55+
}
5056
}

packages/typescript-nextjs-runtime/src/joi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export function parseRequestInput<Schema extends JoiSchema>(
77
schema: Schema,
88
input: unknown,
99
type: RequestInputType,
10-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
10+
// biome-ignore lint/suspicious/noExplicitAny: needed
1111
): any
1212
export function parseRequestInput(
1313
schema: undefined,
@@ -18,7 +18,7 @@ export function parseRequestInput<Schema extends JoiSchema>(
1818
schema: Schema | undefined,
1919
input: unknown,
2020
type: RequestInputType,
21-
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
21+
// biome-ignore lint/suspicious/noExplicitAny: needed
2222
): any {
2323
try {
2424
if (!schema) {

packages/typescript-nextjs-runtime/src/server.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// from https://stackoverflow.com/questions/39494689/is-it-possible-to-restrict-number-to-a-certain-range
2+
import {OpenAPIRuntimeError} from "./errors"
3+
24
type Enumerate<
35
N extends number,
46
Acc extends number[] = [],
@@ -24,7 +26,7 @@ export type StatusCode =
2426
| StatusCode4xx
2527
| StatusCode5xx
2628

27-
export type Response<Status extends StatusCode, Type> = {
29+
export type Res<Status extends StatusCode, Type> = {
2830
status: Status
2931
body: Type
3032
}
@@ -39,9 +41,20 @@ export class OpenAPIRuntimeResponse<Type> {
3941
return this
4042
}
4143

42-
unpack(): Response<StatusCode, Type | undefined> {
44+
unpack(): Res<StatusCode, Type | undefined> {
4345
return {status: this.status, body: this._body}
4446
}
47+
48+
static unwrap(it: OpenAPIRuntimeResponse<unknown> | Response): Response {
49+
if (it instanceof Response) {
50+
return it
51+
}
52+
const {status, body} = it.unpack()
53+
54+
return body !== undefined
55+
? Response.json(body, {status})
56+
: new Response(undefined, {status})
57+
}
4558
}
4659

4760
export type OpenAPIRuntimeResponder<

packages/typescript-nextjs-runtime/src/zod.ts renamed to packages/typescript-nextjs-runtime/src/zod-v3.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {z} from "zod"
1+
import type {z} from "zod/v3"
22
import {OpenAPIRuntimeError, type RequestInputType} from "./errors"
33

44
export function parseRequestInput<Schema extends z.ZodTypeAny>(
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type {z} from "zod/v4"
2+
import {OpenAPIRuntimeError, type RequestInputType} from "./errors"
3+
4+
export function parseRequestInput<Schema extends z.ZodTypeAny>(
5+
schema: Schema,
6+
input: unknown,
7+
type: RequestInputType,
8+
): z.infer<Schema>
9+
export function parseRequestInput(
10+
schema: undefined,
11+
input: unknown,
12+
type: RequestInputType,
13+
): undefined
14+
export function parseRequestInput<Schema extends z.ZodTypeAny>(
15+
schema: Schema | undefined,
16+
input: unknown,
17+
type: RequestInputType,
18+
): z.infer<Schema> | undefined {
19+
try {
20+
return schema?.parse(input)
21+
} catch (err) {
22+
throw OpenAPIRuntimeError.RequestError(err, type)
23+
}
24+
}
25+
26+
// TODO: optional response validation
27+
export function responseValidationFactory(
28+
possibleResponses: [string, z.ZodTypeAny][],
29+
defaultResponse?: z.ZodTypeAny,
30+
) {
31+
// Exploit the natural ordering matching the desired specificity of eg: 404 vs 4xx
32+
possibleResponses.sort((x, y) => (x[0] < y[0] ? -1 : 1))
33+
34+
return (status: number, value: unknown) => {
35+
try {
36+
for (const [match, schema] of possibleResponses) {
37+
const isMatch =
38+
(/^\d+$/.test(match) && String(status) === match) ||
39+
(/^\d[xX]{2}$/.test(match) && String(status)[0] === match[0])
40+
41+
if (isMatch) {
42+
return schema.parse(value)
43+
}
44+
}
45+
46+
if (defaultResponse) {
47+
return defaultResponse.parse(value)
48+
}
49+
50+
// TODO: throw on unmatched response
51+
return value
52+
} catch (err) {
53+
throw OpenAPIRuntimeError.ResponseError(err)
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)