Skip to content

Commit c8c34b8

Browse files
authored
standard-schema (#155)
* Add standard-schema * WIP: 型エラーは消えた * Fix test * Fix examples * Remove zod and valibot unused types * Remove express zod, valibot type utils * Remove OpenAPI zod, valibot types * Simplify req/res validator * Simplify validator * Fix eslint * format * Fix examples * Simplify OpenAPI * Move ss * Simplify * Remove unused code * Fix CI * Reorganize ss validator * ApiEndpointsSchema * Fix error * update doc * Fix example
1 parent 32b55d3 commit c8c34b8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+1176
-1709
lines changed

examples/misc/express/valibot/express.ts

+25-24
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import express from "express";
22
import { asAsync } from "@notainc/typed-api-spec/express";
3+
import { ToHandlers, typed } from "@notainc/typed-api-spec/express";
34
import { pathMap } from "../../spec/valibot";
4-
import { ToHandlers, typed } from "@notainc/typed-api-spec/express/valibot";
55

66
const emptyMiddleware = (
77
req: express.Request,
@@ -20,7 +20,7 @@ const newApp = () => {
2020
// const wApp = app as TRouter<typeof pathMap>;
2121
// ```
2222
const wApp = asAsync(typed(pathMap, app));
23-
wApp.get("/users", emptyMiddleware, (req, res) => {
23+
wApp.get("/users", emptyMiddleware, async (req, res) => {
2424
// eslint-disable-next-line no-constant-condition
2525
if (false) {
2626
// @ts-expect-error params is not defined because pathMap["/users"]["get"].params is not defined
@@ -29,41 +29,42 @@ const newApp = () => {
2929

3030
// validate method is available in res.locals
3131
// validate(req).query() is equals to pathMap["/users"]["get"].query.safeParse(req.query)
32-
const { data, error } = res.locals.validate(req).query();
33-
if (data !== undefined) {
34-
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
35-
res.status(200).json({ userNames: [`page${data.page}#user1`] });
36-
} else {
32+
const r = await res.locals.validate(req).query();
33+
if (r.issues) {
3734
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["get"].res["400"]
38-
res.status(400).json({ errorMessage: error.toString() });
35+
return res.status(400).json({ errorMessage: r.issues.toString() });
3936
}
37+
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
38+
return res.status(200).json({ userNames: [`page${r.value.page}#user1`] });
4039
});
41-
wApp.post("/users", (req, res) => {
42-
// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
43-
const { data, error } = res.locals.validate(req).body();
40+
41+
wApp.post("/users", async (req, res) => {
4442
{
4543
// Request header also can be validated
4644
res.locals.validate(req).headers();
4745
}
48-
if (data !== undefined) {
49-
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
50-
res.status(200).json({ userId: data.userName + "#0" });
51-
} else {
46+
47+
// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
48+
const r = await res.locals.validate(req).body();
49+
if (r.issues) {
5250
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["post"].res["400"]
53-
res.status(400).json({ errorMessage: error.toString() });
51+
return res.status(400).json({ errorMessage: r.issues.toString() });
5452
}
53+
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
54+
return res.status(200).json({ userId: r.value.userName + "#0" });
5555
});
5656

57-
const getUserHandler: Handlers["/users/:userId"]["get"] = (req, res) => {
58-
const { data: params, error } = res.locals.validate(req).params();
59-
60-
if (params !== undefined) {
61-
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
62-
res.status(200).json({ userName: "user#" + params.userId });
63-
} else {
57+
const getUserHandler: Handlers["/users/:userId"]["get"] = async (
58+
req,
59+
res,
60+
) => {
61+
const r = await res.locals.validate(req).params();
62+
if (r.issues) {
6463
// res.status(400).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["400"]
65-
res.status(400).json({ errorMessage: error.toString() });
64+
return res.status(400).json({ errorMessage: r.issues.toString() });
6665
}
66+
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
67+
return res.status(200).json({ userName: "user#" + r.value.userId });
6768
};
6869
wApp.get("/users/:userId", getUserHandler);
6970

examples/misc/express/valibot/openapi/index.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import express from "express";
22
import * as v from "valibot";
33
import cors from "cors";
44
import { OpenAPIV3_1 } from "openapi-types";
5-
import { ValibotOpenApiEndpoints } from "@notainc/typed-api-spec/valibot/openapi";
6-
import { toOpenApiDoc } from "@notainc/typed-api-spec/valibot";
5+
import {
6+
OpenApiEndpointsSchema,
7+
toOpenApiDoc,
8+
} from "@notainc/typed-api-spec/core";
79

810
const openapiBaseDoc: Omit<OpenAPIV3_1.Document, "paths"> = {
911
openapi: "3.1.0",
@@ -45,7 +47,7 @@ const apiEndpoints = {
4547
},
4648
},
4749
},
48-
} satisfies ValibotOpenApiEndpoints;
50+
} satisfies OpenApiEndpointsSchema;
4951

5052
const newApp = () => {
5153
const app = express();

examples/misc/express/zod/express.ts

+25-24
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import express from "express";
22
import { pathMap } from "../../spec/zod";
3-
import { ToHandlers, typed } from "@notainc/typed-api-spec/express/zod";
3+
import { ToHandlers, typed } from "@notainc/typed-api-spec/express";
44
import { asAsync } from "@notainc/typed-api-spec/express";
55

66
const emptyMiddleware = (
@@ -20,7 +20,7 @@ const newApp = () => {
2020
// const wApp = app as TRouter<typeof pathMap>;
2121
// ```
2222
const wApp = asAsync(typed(pathMap, app));
23-
wApp.get("/users", emptyMiddleware, (req, res) => {
23+
wApp.get("/users", emptyMiddleware, async (req, res) => {
2424
// eslint-disable-next-line no-constant-condition
2525
if (false) {
2626
// @ts-expect-error params is not defined because pathMap["/users"]["get"].params is not defined
@@ -29,41 +29,42 @@ const newApp = () => {
2929

3030
// validate method is available in res.locals
3131
// validate(req).query() is equals to pathMap["/users"]["get"].query.safeParse(req.query)
32-
const { data, error } = res.locals.validate(req).query();
33-
if (data !== undefined) {
34-
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
35-
res.status(200).json({ userNames: [`page${data.page}#user1`] });
36-
} else {
32+
const r = await res.locals.validate(req).query();
33+
if (r.issues) {
3734
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["get"].res["400"]
38-
res.status(400).json({ errorMessage: error.toString() });
35+
return res.status(400).json({ errorMessage: r.issues.toString() });
3936
}
37+
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
38+
return res.status(200).json({ userNames: [`page${r.value.page}#user1`] });
4039
});
41-
wApp.post("/users", (req, res) => {
42-
// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
43-
const { data, error } = res.locals.validate(req).body();
40+
41+
wApp.post("/users", async (req, res) => {
4442
{
4543
// Request header also can be validated
4644
res.locals.validate(req).headers();
4745
}
48-
if (data !== undefined) {
49-
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
50-
res.status(200).json({ userId: data.userName + "#0" });
51-
} else {
46+
47+
// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
48+
const r = await res.locals.validate(req).body();
49+
if (r.issues) {
5250
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["post"].res["400"]
53-
res.status(400).json({ errorMessage: error.toString() });
51+
return res.status(400).json({ errorMessage: r.issues.toString() });
5452
}
53+
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
54+
return res.status(200).json({ userId: r.value.userName + "#0" });
5555
});
5656

57-
const getUserHandler: Handlers["/users/:userId"]["get"] = (req, res) => {
58-
const { data: params, error } = res.locals.validate(req).params();
59-
60-
if (params !== undefined) {
61-
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
62-
res.status(200).json({ userName: "user#" + params.userId });
63-
} else {
57+
const getUserHandler: Handlers["/users/:userId"]["get"] = async (
58+
req,
59+
res,
60+
) => {
61+
const r = await res.locals.validate(req).params();
62+
if (r.issues) {
6463
// res.status(400).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["400"]
65-
res.status(400).json({ errorMessage: error.toString() });
64+
return res.status(400).json({ errorMessage: r.issues.toString() });
6665
}
66+
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
67+
return res.status(200).json({ userName: "user#" + r.value.userId });
6768
};
6869
wApp.get("/users/:userId", getUserHandler);
6970

examples/misc/express/zod/openapi/index.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import cors from "cors";
33
import { OpenAPIV3_1 } from "openapi-types";
44
import "zod-openapi/extend";
55
import z from "zod";
6-
import { toOpenApiDoc } from "@notainc/typed-api-spec/zod/openapi";
7-
import { ZodOpenApiEndpoints } from "@notainc/typed-api-spec/zod/openapi";
6+
import {
7+
OpenApiEndpointsSchema,
8+
toOpenApiDoc,
9+
} from "@notainc/typed-api-spec/core";
810

911
const openapiBaseDoc: Omit<OpenAPIV3_1.Document, "paths"> = {
1012
openapi: "3.1.0",
@@ -48,15 +50,15 @@ const apiEndpoints = {
4850
},
4951
},
5052
},
51-
} satisfies ZodOpenApiEndpoints;
53+
} satisfies OpenApiEndpointsSchema;
5254

5355
const newApp = () => {
5456
const app = express();
5557
app.use(express.json());
5658
app.use(cors());
5759
// const wApp = asAsync(typed(apiEndpoints, app));
58-
app.get("/openapi", (req, res) => {
59-
const openapi = toOpenApiDoc(openapiBaseDoc, apiEndpoints);
60+
app.get("/openapi", async (req, res) => {
61+
const openapi = await toOpenApiDoc(openapiBaseDoc, apiEndpoints);
6062
res.status(200).json(openapi);
6163
});
6264
return app;

examples/misc/fastify/zod/fastify.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ZodTypeProvider,
77
} from "fastify-type-provider-zod";
88
import { pathMap } from "../../spec/zod";
9-
import { toRoutes } from "@notainc/typed-api-spec/fastify/zod";
9+
import { toRoutes } from "@notainc/typed-api-spec/fastify";
1010
import { ZodError } from "zod";
1111
const fastify = Fastify({ logger: true });
1212

@@ -31,7 +31,8 @@ const routes = toRoutes(pathMap);
3131
server.route({
3232
...routes["/users"]["get"],
3333
handler: async (request, reply) => {
34-
const page = request.query.page;
34+
type Query = { page: string };
35+
const { page } = request.query as Query;
3536
if (Number.isNaN(Number(page))) {
3637
return reply.status(400).send({ errorMessage: "page is not a number" });
3738
}
@@ -52,7 +53,8 @@ const _noExecution = () => {
5253
// @ts-expect-error noexist is not defined in pathMap["/users"]["get"]
5354
request.query.noexist;
5455
}
55-
const page = request.query.page;
56+
type Query = { page: string };
57+
const { page } = request.query as Query;
5658
return { userNames: [`page${page}#user1`] };
5759
},
5860
);
@@ -61,7 +63,8 @@ const _noExecution = () => {
6163
server.route({
6264
...routes["/users"]["post"],
6365
handler: async (request, reply) => {
64-
const userName = request.body.userName;
66+
type Body = { userName: string };
67+
const { userName } = request.body as Body;
6568
return reply
6669
.header("Content-Type", "application/json")
6770
.send({ userId: userName + "#0" });
@@ -71,7 +74,8 @@ server.route({
7174
server.route({
7275
...routes["/users/:userId"]["get"],
7376
handler: async (request) => {
74-
const userId = request.params.userId;
77+
type Params = { userId: string };
78+
const { userId } = request.params as Params;
7579
return { userName: `user#${userId}` };
7680
},
7781
});

examples/misc/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"scripts": {
3-
"format": "prettier --write ",
3+
"format": "prettier --write .",
44
"watch:type-check": "npx tsc --noEmit --watch",
55
"test": "run-p test:*",
6-
"test:lint": "eslint ",
7-
"test:format": "prettier --check ",
6+
"test:lint": "eslint .",
7+
"test:format": "prettier --check .",
88
"test:type-check": "tsc --noEmit",
99
"ex:express:zod:server": "tsx express/zod/express.ts",
1010
"ex:express:zod:client": "tsx express/zod/fetch.ts",

examples/misc/simple/withValidation.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { newZodValidator, ZodApiEndpoints } from "@notainc/typed-api-spec/zod";
21
import { withValidation } from "@notainc/typed-api-spec/fetch";
32
import { z } from "zod";
43
import { SpecValidatorError } from "@notainc/typed-api-spec/fetch";
4+
import { ApiEndpointsSchema } from "@notainc/typed-api-spec/core";
55

66
const GITHUB_API_ORIGIN = "https://api.github.com";
77

@@ -12,21 +12,20 @@ const spec = {
1212
responses: { 200: { body: z.object({ names: z.string().array() }) } },
1313
},
1414
},
15-
} satisfies ZodApiEndpoints;
15+
} satisfies ApiEndpointsSchema;
1616
const spec2 = {
1717
"/repos/:owner/:repo/topics": {
1818
get: {
1919
// Invalid response schema
2020
responses: { 200: { body: z.object({ noexist: z.string() }) } },
2121
},
2222
},
23-
} satisfies ZodApiEndpoints;
23+
} satisfies ApiEndpointsSchema;
2424

2525
const main = async () => {
2626
{
2727
// const fetchT = fetch as FetchT<typeof GITHUB_API_ORIGIN, Spec>;
28-
const { req: reqValidator, res: resValidator } = newZodValidator(spec);
29-
const fetchWithV = withValidation(fetch, spec, reqValidator, resValidator);
28+
const fetchWithV = withValidation(fetch, spec);
3029
const response = await fetchWithV(
3130
`${GITHUB_API_ORIGIN}/repos/nota/typed-api-spec/topics?page=1`,
3231
{ headers: { Accept: "application/vnd.github+json" } },
@@ -41,16 +40,15 @@ const main = async () => {
4140

4241
{
4342
// const fetchT = fetch as FetchT<typeof GITHUB_API_ORIGIN, Spec>;
44-
const { req: reqValidator, res: resValidator } = newZodValidator(spec2);
45-
const fetchWithV = withValidation(fetch, spec2, reqValidator, resValidator);
43+
const fetchWithV = withValidation(fetch, spec2);
4644
try {
4745
await fetchWithV(
4846
`${GITHUB_API_ORIGIN}/repos/nota/typed-api-spec/topics?page=1`,
4947
{ headers: { Accept: "application/vnd.github+json" } },
5048
);
5149
} catch (e) {
5250
if (e instanceof SpecValidatorError) {
53-
console.log("error thrown", e.error);
51+
console.log("error thrown", e);
5452
} else {
5553
throw e;
5654
}

examples/misc/spec/valibot.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import * as v from "valibot";
21
import {
2+
ApiEndpointsSchema,
33
ToApiEndpoints,
4-
ValibotApiEndpoints,
5-
} from "@notainc/typed-api-spec/valibot";
4+
} from "@notainc/typed-api-spec/core";
5+
import * as v from "valibot";
66

77
const JsonHeader = v.object({
88
"Content-Type": v.literal("application/json"),
@@ -44,5 +44,5 @@ export const pathMap = {
4444
},
4545
},
4646
},
47-
} satisfies ValibotApiEndpoints;
47+
} satisfies ApiEndpointsSchema;
4848
export type PathMap = ToApiEndpoints<typeof pathMap>;

examples/misc/spec/zod.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import {
2+
ApiEndpointsSchema,
3+
ToApiEndpoints,
4+
} from "@notainc/typed-api-spec/core";
15
import { z } from "zod";
2-
import { ToApiEndpoints, ZodApiEndpoints } from "@notainc/typed-api-spec/zod";
36

47
const JsonHeader = z.union([
58
z.object({ "content-type": z.literal("application/json") }),
@@ -42,5 +45,5 @@ export const pathMap = {
4245
},
4346
},
4447
},
45-
} satisfies ZodApiEndpoints;
48+
} satisfies ApiEndpointsSchema;
4649
export type PathMap = ToApiEndpoints<typeof pathMap>;

examples/misc/tsconfig.json

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
{
2-
"exclude": [
3-
"../../pkgs/docs"
4-
],
2+
"exclude": ["../../pkgs/docs"],
53
"compilerOptions": {
64
/* Visit https://aka.ms/tsconfig to read more about this file */
75

0 commit comments

Comments
 (0)