Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

standard-schema #155

Merged
merged 22 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 25 additions & 24 deletions examples/misc/express/valibot/express.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import express from "express";
import { asAsync } from "@notainc/typed-api-spec/express";
import { ToHandlers, typed } from "@notainc/typed-api-spec/express";
import { pathMap } from "../../spec/valibot";
import { ToHandlers, typed } from "@notainc/typed-api-spec/express/valibot";

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

// validate method is available in res.locals
// validate(req).query() is equals to pathMap["/users"]["get"].query.safeParse(req.query)
const { data, error } = res.locals.validate(req).query();
if (data !== undefined) {
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
res.status(200).json({ userNames: [`page${data.page}#user1`] });
} else {
const r = await res.locals.validate(req).query();
if (r.issues) {
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["get"].res["400"]
res.status(400).json({ errorMessage: error.toString() });
return res.status(400).json({ errorMessage: r.issues.toString() });
}
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
return res.status(200).json({ userNames: [`page${r.value.page}#user1`] });
});
wApp.post("/users", (req, res) => {
// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
const { data, error } = res.locals.validate(req).body();

wApp.post("/users", async (req, res) => {
{
// Request header also can be validated
res.locals.validate(req).headers();
}
if (data !== undefined) {
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
res.status(200).json({ userId: data.userName + "#0" });
} else {

// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
const r = await res.locals.validate(req).body();
if (r.issues) {
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["post"].res["400"]
res.status(400).json({ errorMessage: error.toString() });
return res.status(400).json({ errorMessage: r.issues.toString() });
}
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
return res.status(200).json({ userId: r.value.userName + "#0" });
});

const getUserHandler: Handlers["/users/:userId"]["get"] = (req, res) => {
const { data: params, error } = res.locals.validate(req).params();

if (params !== undefined) {
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
res.status(200).json({ userName: "user#" + params.userId });
} else {
const getUserHandler: Handlers["/users/:userId"]["get"] = async (
req,
res,
) => {
const r = await res.locals.validate(req).params();
if (r.issues) {
// res.status(400).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["400"]
res.status(400).json({ errorMessage: error.toString() });
return res.status(400).json({ errorMessage: r.issues.toString() });
}
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
return res.status(200).json({ userName: "user#" + r.value.userId });
};
wApp.get("/users/:userId", getUserHandler);

Expand Down
8 changes: 5 additions & 3 deletions examples/misc/express/valibot/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import express from "express";
import * as v from "valibot";
import cors from "cors";
import { OpenAPIV3_1 } from "openapi-types";
import { ValibotOpenApiEndpoints } from "@notainc/typed-api-spec/valibot/openapi";
import { toOpenApiDoc } from "@notainc/typed-api-spec/valibot";
import {
OpenApiEndpointsSchema,
toOpenApiDoc,
} from "@notainc/typed-api-spec/core";

const openapiBaseDoc: Omit<OpenAPIV3_1.Document, "paths"> = {
openapi: "3.1.0",
Expand Down Expand Up @@ -45,7 +47,7 @@ const apiEndpoints = {
},
},
},
} satisfies ValibotOpenApiEndpoints;
} satisfies OpenApiEndpointsSchema;

const newApp = () => {
const app = express();
Expand Down
49 changes: 25 additions & 24 deletions examples/misc/express/zod/express.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from "express";
import { pathMap } from "../../spec/zod";
import { ToHandlers, typed } from "@notainc/typed-api-spec/express/zod";
import { ToHandlers, typed } from "@notainc/typed-api-spec/express";
import { asAsync } from "@notainc/typed-api-spec/express";

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

// validate method is available in res.locals
// validate(req).query() is equals to pathMap["/users"]["get"].query.safeParse(req.query)
const { data, error } = res.locals.validate(req).query();
if (data !== undefined) {
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
res.status(200).json({ userNames: [`page${data.page}#user1`] });
} else {
const r = await res.locals.validate(req).query();
if (r.issues) {
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["get"].res["400"]
res.status(400).json({ errorMessage: error.toString() });
return res.status(400).json({ errorMessage: r.issues.toString() });
}
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["get"].res["200"]
return res.status(200).json({ userNames: [`page${r.value.page}#user1`] });
});
wApp.post("/users", (req, res) => {
// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
const { data, error } = res.locals.validate(req).body();

wApp.post("/users", async (req, res) => {
{
// Request header also can be validated
res.locals.validate(req).headers();
}
if (data !== undefined) {
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
res.status(200).json({ userId: data.userName + "#0" });
} else {

// validate(req).body() is equals to pathMap["/users"]["post"].body.safeParse(req.body)
const r = await res.locals.validate(req).body();
if (r.issues) {
// res.status(400).json() accepts only the response schema defined in pathMap["/users"]["post"].res["400"]
res.status(400).json({ errorMessage: error.toString() });
return res.status(400).json({ errorMessage: r.issues.toString() });
}
// res.status(200).json() accepts only the response schema defined in pathMap["/users"]["post"].res["200"]
return res.status(200).json({ userId: r.value.userName + "#0" });
});

const getUserHandler: Handlers["/users/:userId"]["get"] = (req, res) => {
const { data: params, error } = res.locals.validate(req).params();

if (params !== undefined) {
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
res.status(200).json({ userName: "user#" + params.userId });
} else {
const getUserHandler: Handlers["/users/:userId"]["get"] = async (
req,
res,
) => {
const r = await res.locals.validate(req).params();
if (r.issues) {
// res.status(400).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["400"]
res.status(400).json({ errorMessage: error.toString() });
return res.status(400).json({ errorMessage: r.issues.toString() });
}
// res.status(200).json() accepts only the response schema defined in pathMap["/users/:userId"]["get"].res["200"]
return res.status(200).json({ userName: "user#" + r.value.userId });
};
wApp.get("/users/:userId", getUserHandler);

Expand Down
12 changes: 7 additions & 5 deletions examples/misc/express/zod/openapi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import cors from "cors";
import { OpenAPIV3_1 } from "openapi-types";
import "zod-openapi/extend";
import z from "zod";
import { toOpenApiDoc } from "@notainc/typed-api-spec/zod/openapi";
import { ZodOpenApiEndpoints } from "@notainc/typed-api-spec/zod/openapi";
import {
OpenApiEndpointsSchema,
toOpenApiDoc,
} from "@notainc/typed-api-spec/core";

const openapiBaseDoc: Omit<OpenAPIV3_1.Document, "paths"> = {
openapi: "3.1.0",
Expand Down Expand Up @@ -48,15 +50,15 @@ const apiEndpoints = {
},
},
},
} satisfies ZodOpenApiEndpoints;
} satisfies OpenApiEndpointsSchema;

const newApp = () => {
const app = express();
app.use(express.json());
app.use(cors());
// const wApp = asAsync(typed(apiEndpoints, app));
app.get("/openapi", (req, res) => {
const openapi = toOpenApiDoc(openapiBaseDoc, apiEndpoints);
app.get("/openapi", async (req, res) => {
const openapi = await toOpenApiDoc(openapiBaseDoc, apiEndpoints);
res.status(200).json(openapi);
});
return app;
Expand Down
14 changes: 9 additions & 5 deletions examples/misc/fastify/zod/fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
ZodTypeProvider,
} from "fastify-type-provider-zod";
import { pathMap } from "../../spec/zod";
import { toRoutes } from "@notainc/typed-api-spec/fastify/zod";
import { toRoutes } from "@notainc/typed-api-spec/fastify";
import { ZodError } from "zod";
const fastify = Fastify({ logger: true });

Expand All @@ -31,7 +31,8 @@ const routes = toRoutes(pathMap);
server.route({
...routes["/users"]["get"],
handler: async (request, reply) => {
const page = request.query.page;
type Query = { page: string };
const { page } = request.query as Query;
if (Number.isNaN(Number(page))) {
return reply.status(400).send({ errorMessage: "page is not a number" });
}
Expand All @@ -52,7 +53,8 @@ const _noExecution = () => {
// @ts-expect-error noexist is not defined in pathMap["/users"]["get"]
request.query.noexist;
}
const page = request.query.page;
type Query = { page: string };
const { page } = request.query as Query;
return { userNames: [`page${page}#user1`] };
},
);
Expand All @@ -61,7 +63,8 @@ const _noExecution = () => {
server.route({
...routes["/users"]["post"],
handler: async (request, reply) => {
const userName = request.body.userName;
type Body = { userName: string };
const { userName } = request.body as Body;
return reply
.header("Content-Type", "application/json")
.send({ userId: userName + "#0" });
Expand All @@ -71,7 +74,8 @@ server.route({
server.route({
...routes["/users/:userId"]["get"],
handler: async (request) => {
const userId = request.params.userId;
type Params = { userId: string };
const { userId } = request.params as Params;
return { userName: `user#${userId}` };
},
});
Expand Down
6 changes: 3 additions & 3 deletions examples/misc/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"scripts": {
"format": "prettier --write ",
"format": "prettier --write .",
"watch:type-check": "npx tsc --noEmit --watch",
"test": "run-p test:*",
"test:lint": "eslint ",
"test:format": "prettier --check ",
"test:lint": "eslint .",
"test:format": "prettier --check .",
"test:type-check": "tsc --noEmit",
"ex:express:zod:server": "tsx express/zod/express.ts",
"ex:express:zod:client": "tsx express/zod/fetch.ts",
Expand Down
14 changes: 6 additions & 8 deletions examples/misc/simple/withValidation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { newZodValidator, ZodApiEndpoints } from "@notainc/typed-api-spec/zod";
import { withValidation } from "@notainc/typed-api-spec/fetch";
import { z } from "zod";
import { SpecValidatorError } from "@notainc/typed-api-spec/fetch";
import { ApiEndpointsSchema } from "@notainc/typed-api-spec/core";

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

Expand All @@ -12,21 +12,20 @@ const spec = {
responses: { 200: { body: z.object({ names: z.string().array() }) } },
},
},
} satisfies ZodApiEndpoints;
} satisfies ApiEndpointsSchema;
const spec2 = {
"/repos/:owner/:repo/topics": {
get: {
// Invalid response schema
responses: { 200: { body: z.object({ noexist: z.string() }) } },
},
},
} satisfies ZodApiEndpoints;
} satisfies ApiEndpointsSchema;

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

{
// const fetchT = fetch as FetchT<typeof GITHUB_API_ORIGIN, Spec>;
const { req: reqValidator, res: resValidator } = newZodValidator(spec2);
const fetchWithV = withValidation(fetch, spec2, reqValidator, resValidator);
const fetchWithV = withValidation(fetch, spec2);
try {
await fetchWithV(
`${GITHUB_API_ORIGIN}/repos/nota/typed-api-spec/topics?page=1`,
{ headers: { Accept: "application/vnd.github+json" } },
);
} catch (e) {
if (e instanceof SpecValidatorError) {
console.log("error thrown", e.error);
console.log("error thrown", e);
} else {
throw e;
}
Expand Down
8 changes: 4 additions & 4 deletions examples/misc/spec/valibot.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as v from "valibot";
import {
ApiEndpointsSchema,
ToApiEndpoints,
ValibotApiEndpoints,
} from "@notainc/typed-api-spec/valibot";
} from "@notainc/typed-api-spec/core";
import * as v from "valibot";

const JsonHeader = v.object({
"Content-Type": v.literal("application/json"),
Expand Down Expand Up @@ -44,5 +44,5 @@ export const pathMap = {
},
},
},
} satisfies ValibotApiEndpoints;
} satisfies ApiEndpointsSchema;
export type PathMap = ToApiEndpoints<typeof pathMap>;
7 changes: 5 additions & 2 deletions examples/misc/spec/zod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {
ApiEndpointsSchema,
ToApiEndpoints,
} from "@notainc/typed-api-spec/core";
import { z } from "zod";
import { ToApiEndpoints, ZodApiEndpoints } from "@notainc/typed-api-spec/zod";

const JsonHeader = z.union([
z.object({ "content-type": z.literal("application/json") }),
Expand Down Expand Up @@ -42,5 +45,5 @@ export const pathMap = {
},
},
},
} satisfies ZodApiEndpoints;
} satisfies ApiEndpointsSchema;
export type PathMap = ToApiEndpoints<typeof pathMap>;
4 changes: 1 addition & 3 deletions examples/misc/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
{
"exclude": [
"../../pkgs/docs"
],
"exclude": ["../../pkgs/docs"],
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */

Expand Down
Loading