Skip to content

Commit ad2373c

Browse files
committed
feat: add typebox route
1 parent 33cc4b5 commit ad2373c

File tree

16 files changed

+266
-47
lines changed

16 files changed

+266
-47
lines changed

.changeset/famous-dolphins-report.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ddadaal/next-typed-api-routes-runtime": minor
3+
"@ddadaal/next-typed-api-routes-cli": minor
4+
---
5+
6+
add support for typebox route

example/src/apis/api.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/* eslint-disable max-len */
22

3-
import { fromApi, fromZodRoute } from "@ddadaal/next-typed-api-routes-runtime/lib/client";
3+
import { fromApi, fromTypeboxRoute, fromZodRoute } from "@ddadaal/next-typed-api-routes-runtime/lib/client";
44
import { join } from "path";
55
import type { LoginSchema } from "src/pages/api/login/[username]";
66
import type { RegisterSchema } from "src/pages/api/register/index";
7+
import type { TypeboxRouteSchema } from "src/pages/api/typeboxRoute/[test]";
78
import type { ZodRouteSchema } from "src/pages/api/zodRoute/[test]";
89

910

@@ -14,5 +15,7 @@ const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
1415
export const api = {
1516
login: fromApi<LoginSchema>("GET", join(basePath, "/api/login/[username]")),
1617
register: fromApi<RegisterSchema>("POST", join(basePath, "/api/register")),
18+
typeboxRoute: fromTypeboxRoute<typeof TypeboxRouteSchema>("POST", join(basePath, "/api/typeboxRoute/[test]")),
1719
zodRoute: fromZodRoute<typeof ZodRouteSchema>("POST", join(basePath, "/api/zodRoute/[test]")),
1820
};
21+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Type, typeboxRoute, typeboxRouteSchema } from "@ddadaal/next-typed-api-routes-runtime";
2+
3+
export const TypeboxRouteSchema = typeboxRouteSchema({
4+
method: "POST",
5+
body: Type.Object({ error: Type.Boolean() }),
6+
query: Type.Object({ test: Type.String() }),
7+
responses: {
8+
200: Type.Object({ hello: Type.String() }),
9+
404: Type.Object({ error: Type.String() }),
10+
},
11+
});
12+
13+
export default typeboxRoute(TypeboxRouteSchema, async (req) => {
14+
15+
if (req.body.error) {
16+
return { 404: { error: "123" } };
17+
} else {
18+
return { 200: { hello: `${req.query.test}` } };
19+
}
20+
});

example/src/pages/api/zodRoute/[test].ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const ZodRouteSchema = zodRouteSchema({
1212

1313
export default zodRoute(ZodRouteSchema, async (req) => {
1414
if (req.body.error) {
15-
return { 404: { error: "error" }};
15+
return { 404: { error: "123" } };
1616
} else {
1717
return { 200: { hello: `${req.query.test}` } };
1818
}

example/src/pages/index.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,27 @@ export const ZodRouteTestDiv = () => {
2323
);
2424
};
2525

26+
export const TypeboxRouteTestDiv = () => {
27+
28+
const [resp, setResp] = useState("");
29+
30+
return (
31+
<div id="typeboxRoute">
32+
<button id="button" onClick={() => {
33+
api.typeboxRoute({ body: { error: false }, query: { test: "123" } })
34+
.then((resp) => {
35+
setResp(resp.hello);
36+
});
37+
}}>
38+
Call TypeboxRoute
39+
</button>
40+
<p id="p">
41+
{resp}
42+
</p>
43+
</div>
44+
);
45+
};
46+
2647
export const AjvRouteTestDiv = () => {
2748

2849
const [username, setUsername] = useState("");
@@ -81,10 +102,12 @@ const Home: NextPage = () => {
81102
<div>
82103
<AjvRouteTestDiv />
83104
</div>
84-
85105
<div>
86106
<ZodRouteTestDiv />
87107
</div>
108+
<div>
109+
<TypeboxRouteTestDiv />
110+
</div>
88111
</div>
89112
);
90113
};

example/tests/apis.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ test("should call zodRoute from browser", async ({ page }) => {
3030
expect(await text?.innerText()).toBe("123");
3131
});
3232

33+
test("should call typeboxRoute", async () => {
34+
const resp = await api.typeboxRoute({ body: { error: false }, query: { test: "123" } });
35+
36+
expect(resp.hello).toBe("123");
37+
});
38+
39+
test("typeboxRoute should handler error", async () => {
40+
const error = await api.typeboxRoute({ body: { error: true }, query: { test: "123" } })
41+
.httpError(404, (err) => {
42+
expect(err.error).toBe("error");
43+
return err;
44+
})
45+
.then(() => undefined)
46+
.catch((e) => e);
47+
48+
expect(error).toBeDefined();
49+
});
50+
51+
test("should call typeboxRoute from browser", async ({ page }) => {
52+
await page.goto("http://localhost:3000");
53+
54+
await page.click("#typeboxRoute button");
55+
56+
const text = await page.$("#typeboxRoute p");
57+
58+
expect(await text?.innerText()).toBe("123");
59+
});
60+
3361
test("should call register", async () => {
3462
const resp = await api.register({ body: { username: "123", password: "123" } });
3563

packages/cli/src/generateClients.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,13 @@ interface Import {
2121
relativePath: string;
2222
}
2323

24-
export type ApiType = "zod" | "ajv";
24+
export const ApiTypes = {
25+
zod: { libImport: "fromZodRoute", typeFormat: (typeName: string) => `typeof ${typeName}` },
26+
typebox: { libImport: "fromTypeboxRoute", typeFormat: (typeName: string) => `typeof ${typeName}` },
27+
ajv: { libImport: "fromApi", typeFormat: (typeName: string) => typeName },
28+
};
29+
30+
export type ApiType = keyof typeof ApiTypes;
2531

2632
interface Endpoint {
2733
functionName: string;
@@ -57,15 +63,19 @@ async function getApiObject(
5763

5864
for (const statement of sourceFile.statements) {
5965

60-
// if there is a object that ends with Schema, consider it as a zod schema
6166
if (ts.isVariableStatement(statement)) {
6267
const declaration = statement.declarationList.declarations[0];
6368

6469
if (ts.isIdentifier(declaration.name)
6570
&& declaration.name.text.endsWith("Schema") && declaration.name.text.length > "Schema".length
6671
&& declaration.initializer && ts.isCallExpression(declaration.initializer)
67-
&& ts.isObjectLiteralExpression(declaration.initializer.arguments[0])
72+
&& ts.isIdentifier(declaration.initializer.expression)
73+
&& ts.isObjectLiteralExpression(declaration.initializer.arguments[0])
6874
) {
75+
76+
const schemaFunction = declaration.initializer.expression.text;
77+
78+
// zodRouteSchema or typeboxRouteSchema
6979
const schema = declaration.initializer.arguments[0];
7080

7181
for (const property of schema.properties) {
@@ -76,7 +86,7 @@ async function getApiObject(
7686
&& ts.isStringLiteral(property.initializer)
7787
) {
7888
found = {
79-
apiType: "zod",
89+
apiType: schemaFunction === "zodRouteSchema" ? "zod" : "typebox",
8090
typeName: declaration.name.text,
8191
method: property.initializer.text,
8292
};
@@ -177,28 +187,25 @@ export async function generateClients({
177187
// use string instead of ts factories to easily style the code and reduce complexity
178188
const apiObjDeclaration = `
179189
export const ${apiObjectName} = {
180-
${endpoints.map((e) =>
181-
// eslint-disable-next-line max-len
182-
` ${e.functionName}: ${e.type === "ajv"
183-
? `fromApi<${e.importName}>`
184-
: `fromZodRoute<typeof ${e.importName}>`
185-
}("${e.method}", join(basePath, "${e.url}")),`,
190+
${endpoints.map((e) => {
191+
192+
const { libImport, typeFormat } = ApiTypes[e.type];
193+
return (
194+
` ${e.functionName}: ${libImport}<${typeFormat(e.importName)}>("${e.method}", join(basePath, "${e.url}")),`
195+
);
196+
},
186197
).join(EOL)}
187198
};
188199
`;
189200

190-
const importsFromRootPackage: string[] = [];
191-
192-
if (endpoints.find((x) => x.type === "ajv")) {
193-
importsFromRootPackage.push("fromApi");
194-
}
201+
const importsFromRootPackage = new Set<string>();
195202

196-
if (endpoints.find((x) => x.type === "zod")) {
197-
importsFromRootPackage.push("fromZodRoute");
203+
for (const endpoint of endpoints) {
204+
importsFromRootPackage.add(ApiTypes[endpoint.type].libImport);
198205
}
199206

200207
const fetchApiImportDeclaration = `
201-
import { ${importsFromRootPackage.join(", ")} } from "${fetchImport}";
208+
import { ${Array.from(importsFromRootPackage.values()).join(", ")} } from "${fetchImport}";
202209
import { join } from "path";
203210
`;
204211

packages/runtime/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"ajv-formats-draft2019": "1.6.1",
3434
"fast-json-stringify": "5.7.0",
3535
"tslib": "2.5.2",
36-
"zod": "3.21.4"
36+
"zod": "3.21.4",
37+
"@sinclair/typebox": "0.28.13"
3738
},
3839
"devDependencies": {
3940
"@rollup/plugin-commonjs": "25.0.0",

packages/runtime/src/fetch/fetch.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { TypeboxRouteSchema, TypeboxRouteSchemaToSchema } from "../route";
12
import { ZodRouteSchema, ZodRouteSchemaToSchema } from "../route/zodRoute";
23
import type {
34
Querystring, RequestArgs,
@@ -242,3 +243,8 @@ export function fromApi<TSchema extends AnySchema>(method: HttpMethod, url: stri
242243
export function fromZodRoute<TSchema extends ZodRouteSchema>(method: HttpMethod, url: string) {
243244
return fromApi<ZodRouteSchemaToSchema<TSchema>>(method, url);
244245
}
246+
247+
export function fromTypeboxRoute<TSchema extends TypeboxRouteSchema>(method: HttpMethod, url: string) {
248+
return fromApi<TypeboxRouteSchemaToSchema<TSchema>>(method, url);
249+
}
250+

packages/runtime/src/route/ajv.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Ajv, { Options } from "ajv";
2+
import addFormats from "ajv-formats";
3+
import addDraft2019Format from "ajv-formats-draft2019";
4+
5+
export const ajvOptions: Options = { useDefaults: true, allowUnionTypes: true, coerceTypes: "array" };
6+
7+
export const createAjv = () => {
8+
// add shared models
9+
const ajv = new Ajv(ajvOptions);
10+
11+
// add formats support
12+
addFormats(ajv);
13+
addDraft2019Format(ajv);
14+
15+
return ajv;
16+
};
17+
18+

0 commit comments

Comments
 (0)