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
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Simplify
mpppk committed Mar 11, 2025
commit 0f393918d53feeded195e6adce3bca52b263946b
15 changes: 0 additions & 15 deletions pkgs/typed-api-spec/package.json
Original file line number Diff line number Diff line change
@@ -107,21 +107,6 @@
"types": "./dist/json/index.d.ts",
"require": "./dist/json/index.js",
"import": "./dist/json/index.mjs"
},
"./zod": {
"types": "./dist/zod/index.d.ts",
"require": "./dist/zod/index.js",
"import": "./dist/zod/index.mjs"
},
"./valibot": {
"types": "./dist/valibot/index.d.ts",
"require": "./dist/valibot/index.js",
"import": "./dist/valibot/index.mjs"
},
"./ss": {
"types": "./dist/ss/index.d.ts",
"require": "./dist/ss/index.js",
"import": "./dist/ss/index.mjs"
}
},
"main": "./dist/index.js",
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import * as v from "valibot";
import { OpenAPIV3_1 } from "openapi-types";
import { SSOpenApiEndpoints, toOpenApiDoc } from "../core";
import { SSOpenApiEndpoints, toOpenApiDoc } from "..";

describe("openapi", () => {
const endpoints = {
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import { OpenAPIV3_1 } from "openapi-types";
import "zod-openapi/extend";
import z from "zod";
import { SSOpenApiEndpoints, toOpenApiDoc } from "../core";
import { SSOpenApiEndpoints, toOpenApiDoc } from "..";
describe("openapi", () => {
const endpoints = {
"/pets": {
102 changes: 86 additions & 16 deletions pkgs/typed-api-spec/src/express/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { IRouter, RequestHandler } from "express";
import { IRouter, RequestHandler, Router } from "express";
import {
Method,
AnyApiResponses,
ApiResBody,
ApiSpec,
AnyApiSpec,
AnyApiEndpoints,
} from "../index";
import {
@@ -15,9 +14,14 @@ import {
} from "express-serve-static-core";
import { StatusCode } from "../core";
import { ParsedQs } from "qs";
import { AnySpecValidator, SpecValidatorMap } from "../core/validator/request";
import { AnySpecValidator } from "../core/validator/request";
import { StandardSchemaV1 } from "@standard-schema/spec";
import { newSSValidator, SSApiEndpoints } from "../core/ss";
import {
newSSValidator,
SSApiEndpoints,
ToApiEndpoints,
ToSSValidators,
} from "../core/ss";

/**
* Express Request Handler, but with more strict type information.
@@ -37,23 +41,64 @@ export type Handler<
) => void;

export type ToHandler<
Spec extends AnyApiSpec | undefined,
Validators extends AnySpecValidator | undefined,
E extends SSApiEndpoints,
Path extends keyof E & string,
M extends Method,
> = Handler<
Spec,
ToApiEndpoints<E>[Path][M],
ValidateLocals<
Validators extends AnySpecValidator ? Validators : Record<string, never>
ToSSValidators<E, Path, M> extends AnySpecValidator
? ToSSValidators<E, Path, M>
: Record<string, never>
>
>;
// export type ToHandler<
// Spec extends AnyApiSpec | undefined,
// Validators extends AnySpecValidator | undefined,
// > = Handler<
// Spec,
// ValidateLocals<
// Validators extends AnySpecValidator ? Validators : Record<string, never>
// >
// >;

export type ToHandlers<
E extends AnyApiEndpoints,
V extends SpecValidatorMap,
> = {
/**
* Convert ZodApiSpec to Express Request Handler type.
*/
// export type ToHandler<
// ZodE extends SSApiEndpoints,
// Path extends keyof ZodE & string,
// M extends Method,
// > = ToPureHandler<ToApiEndpoints<ZodE>[Path][M], ToSSValidators<ZodE, Path, M>>;

export type ToHandlers<E extends SSApiEndpoints> = {
[Path in keyof E & string]: {
[M in Method]: ToHandler<E[Path][M], V[Path][M]>;
[M in Method]: ToHandler<E, Path, M>;
};
};
// export type ToHandlers<
// E extends AnyApiEndpoints,
// V extends SpecValidatorMap,
// > = {
// [Path in keyof E & string]: {
// [M in Method]: ToHandler<E[Path][M], V[Path][M]>;
// };
// };

/**
* Convert SSApiEndpoints to Express Request Handler type map.
*/
// export type ToHandlers<
// ZodE extends SSApiEndpoints,
// E extends ToApiEndpoints<ZodE> = ToApiEndpoints<ZodE>,
// V extends ToValidatorsMap<ZodE> = ToValidatorsMap<ZodE>,
// > = ToPureHandlers<E, V>;

// export type ToValidatorsMap<ESchema extends SSApiEndpoints> = {
// [Path in keyof ESchema & string]: {
// [M in Method]: ToSSValidators<ESchema, Path, M>;
// };
// };

/**
* Express Response, but with more strict type information.
@@ -80,7 +125,6 @@ export type ValidateLocals<
*/
export type RouterT<
E extends AnyApiEndpoints,
V extends SpecValidatorMap,
SC extends StatusCode = StatusCode,
> = Omit<IRouter, Method> & {
[M in Method]: <Path extends string & keyof E>(
@@ -89,9 +133,9 @@ export type RouterT<
// Middlewareは複数のエンドポイントで実装を使い回されることがあるので、型チェックはゆるくする
...Array<RequestHandler>,
// Handlerは厳密に型チェックする
ToHandler<E[Path][M], V[Path][M]>,
ToHandler<E, Path, M>,
]
) => RouterT<E, V, SC>;
) => RouterT<E, SC>;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -212,3 +256,29 @@ export const asAsync = <Router extends IRouter | RouterT<any, any>>(
},
});
};

/**
* Set validator and add more strict type information to router.
*
* @param pathMap API endpoints
* @param router Express Router
*
* @example
* ```
* const router = typed(pathMap, express.Router())
* router.get('/path', (req, res) => {
* const {data, error} = res.locals.validate(req).query()
* if (error) {
* return res.status(400).json({ message: 'Invalid query' })
* }
* return res.status(200).json({ message: 'success', value: r.data.value })
* })
* ```
*/
export const typed = <const Endpoints extends SSApiEndpoints>(
pathMap: Endpoints,
router: Router,
): RouterT<Endpoints> => {
router.use(validatorMiddleware(pathMap));
return router;
};
116 changes: 58 additions & 58 deletions pkgs/typed-api-spec/src/express/ss.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,62 @@
import { Method } from "../core";
import { SSApiEndpoints, ToApiEndpoints, ToSSValidators } from "../core/ss";
import {
RouterT,
ToHandler as ToPureHandler,
ToHandlers as ToPureHandlers,
validatorMiddleware,
} from "./index";
import { Router } from "express";
// import { Method } from "../core";
// import { SSApiEndpoints, ToApiEndpoints, ToSSValidators } from "../core/ss";
// import {
// RouterT,
// ToHandler as ToPureHandler,
// ToHandlers as ToPureHandlers,
// validatorMiddleware,
// } from "./index";
// import { Router } from "express";

/**
* Convert ZodApiSpec to Express Request Handler type.
*/
export type ToHandler<
ZodE extends SSApiEndpoints,
Path extends keyof ZodE & string,
M extends Method,
> = ToPureHandler<ToApiEndpoints<ZodE>[Path][M], ToSSValidators<ZodE, Path, M>>;
// /**
// * Convert ZodApiSpec to Express Request Handler type.
// */
// export type ToHandler<
// ZodE extends SSApiEndpoints,
// Path extends keyof ZodE & string,
// M extends Method,
// > = ToPureHandler<ToApiEndpoints<ZodE>[Path][M], ToSSValidators<ZodE, Path, M>>;

/**
* Convert SSApiEndpoints to Express Request Handler type map.
*/
export type ToHandlers<
ZodE extends SSApiEndpoints,
E extends ToApiEndpoints<ZodE> = ToApiEndpoints<ZodE>,
V extends ToValidatorsMap<ZodE> = ToValidatorsMap<ZodE>,
> = ToPureHandlers<E, V>;
// /**
// * Convert SSApiEndpoints to Express Request Handler type map.
// */
// export type ToHandlers<
// ZodE extends SSApiEndpoints,
// E extends ToApiEndpoints<ZodE> = ToApiEndpoints<ZodE>,
// V extends ToValidatorsMap<ZodE> = ToValidatorsMap<ZodE>,
// > = ToPureHandlers<E, V>;

export type ToValidatorsMap<ZodE extends SSApiEndpoints> = {
[Path in keyof ZodE & string]: {
[M in Method]: ToSSValidators<ZodE, Path, M>;
};
};
// export type ToValidatorsMap<ZodE extends SSApiEndpoints> = {
// [Path in keyof ZodE & string]: {
// [M in Method]: ToSSValidators<ZodE, Path, M>;
// };
// };

/**
* Set validator and add more strict type information to router.
*
* @param pathMap API endpoints
* @param router Express Router
*
* @example
* ```
* const router = typed(pathMap, express.Router())
* router.get('/path', (req, res) => {
* const {data, error} = res.locals.validate(req).query()
* if (error) {
* return res.status(400).json({ message: 'Invalid query' })
* }
* return res.status(200).json({ message: 'success', value: r.data.value })
* })
* ```
*/
export const typed = <const Endpoints extends SSApiEndpoints>(
pathMap: Endpoints,
router: Router,
): RouterT<ToApiEndpoints<Endpoints>, ToValidatorsMap<Endpoints>> => {
// const { req: reqValidator } = newSSValidator(pathMap);
// router.use(validatorMiddleware(reqValidator));
// const { req: reqValidator } = newSSValidator(pathMap);
router.use(validatorMiddleware(pathMap));
return router;
};
// /**
// * Set validator and add more strict type information to router.
// *
// * @param pathMap API endpoints
// * @param router Express Router
// *
// * @example
// * ```
// * const router = typed(pathMap, express.Router())
// * router.get('/path', (req, res) => {
// * const {data, error} = res.locals.validate(req).query()
// * if (error) {
// * return res.status(400).json({ message: 'Invalid query' })
// * }
// * return res.status(200).json({ message: 'success', value: r.data.value })
// * })
// * ```
// */
// export const typed = <const Endpoints extends SSApiEndpoints>(
// pathMap: Endpoints,
// router: Router,
// ): RouterT<ToApiEndpoints<Endpoints>, ToValidatorsMap<Endpoints>> => {
// // const { req: reqValidator } = newSSValidator(pathMap);
// // router.use(validatorMiddleware(reqValidator));
// // const { req: reqValidator } = newSSValidator(pathMap);
// router.use(validatorMiddleware(pathMap));
// return router;
// };
9 changes: 7 additions & 2 deletions pkgs/typed-api-spec/src/express/valibot.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { describe, it, expect, vi, assert } from "vitest";
import request from "supertest";
import express from "express";
import { asAsync, ValidateLocals, validatorMiddleware } from "./index";
import {
asAsync,
ToHandlers,
typed,
ValidateLocals,
validatorMiddleware,
} from "./index";
import * as v from "valibot";
import { Request } from "express";
import { ParseUrlParams } from "../core";
@@ -10,7 +16,6 @@ import {
newValidatorPathNotFoundError,
} from "../core/validator/validate";
import { SSApiEndpoints, SSApiSpec, SSValidators } from "../core/ss";
import { ToHandlers, typed } from "./ss";

type SSValidateLocals<
AS extends SSApiSpec,
32 changes: 28 additions & 4 deletions pkgs/typed-api-spec/src/express/zod.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { describe, it, expect, vi, assert } from "vitest";
import request from "supertest";
import express from "express";
import { asAsync, ValidateLocals, validatorMiddleware } from "./index";
import {
asAsync,
RouterT,
ToHandlers,
typed,
ValidateLocals,
validatorMiddleware,
} from "./index";
import { z } from "zod";
import { Request } from "express";
import { ParseUrlParams } from "../core";
@@ -10,7 +17,6 @@
newValidatorPathNotFoundError,
} from "../core/validator/validate";
import { SSApiEndpoints, SSApiSpec, SSValidators } from "../core/ss";
import { ToHandlers, typed } from "./ss";

type SSValidateLocals<
AS extends SSApiSpec,
@@ -50,8 +56,7 @@
},
},
} satisfies SSApiEndpoints;
// const { req: reqValidator } = newSSValidator(pathMap);
// const middleware = validatorMiddleware(reqValidator);

const middleware = validatorMiddleware(pathMap);
const next = vi.fn();

@@ -278,6 +283,25 @@
},
} satisfies SSApiEndpoints;

type R = RouterT<typeof pathMap, 200>;
const r = {} as R;
r.get("/users", (req, res) => {

Check failure on line 288 in pkgs/typed-api-spec/src/express/zod.test.ts

GitHub Actions / test

src/express/zod.test.ts

TypeError: r.get is not a function ❯ src/express/zod.test.ts:288:5
res.json([{ id: "1", name: "alice" }]);
});
// type A = ToHandlers<typeof pathMap>["/users"]["get"];
// const a: A = (req, res) => {
// res.json([{ id: "1", name: "alice" }]);
// };
// type A = ToHandler<typeof pathMap, "/users", "get">;
// type B = ToApiEndpoints<
// typeof pathMap
// >["/users"]["get"]["responses"][200]["body"];
// type ERes = ExpressResponse<
// ToApiEndpoints<typeof pathMap>["/users"]["get"]["responses"],
// 200
// >;
// const eres = {} as ERes;
// eres.json([{ id: "1", name: "alice" }]);
it("ok", async () => {
const app = newApp();
const wApp = typed(pathMap, app);
20 changes: 20 additions & 0 deletions pkgs/typed-api-spec/src/fetch/new-fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type FetchT from "./index.js";
import { SSApiEndpoints, ToApiEndpoints } from "../core/ss.js";
import { withValidation } from "./index.js";

export const newFetch = <E extends SSApiEndpoints = SSApiEndpoints>(
specLoader: () => Promise<E>,
validation: boolean,
ft = fetch,
) => {
return async <
Origin extends string = "",
E2 extends ToApiEndpoints<E> = ToApiEndpoints<E>,
Fetch = FetchT<Origin, E2>,
>(): Promise<Fetch> => {
if (validation) {
return (await withValidation(ft, await specLoader())) as Fetch;
}
return ft as Fetch;
};
};
2 changes: 0 additions & 2 deletions pkgs/typed-api-spec/src/index.ts
Original file line number Diff line number Diff line change
@@ -13,5 +13,3 @@ export { FetchT, RequestInitT };

import JSONT, { JSON$stringifyT } from "./json";
export { JSONT, JSON$stringifyT };

export * from "./zod";
1 change: 0 additions & 1 deletion pkgs/typed-api-spec/src/valibot/index.ts

This file was deleted.

26 changes: 0 additions & 26 deletions pkgs/typed-api-spec/src/valibot/util.ts

This file was deleted.

4 changes: 0 additions & 4 deletions pkgs/typed-api-spec/src/zod/index.ts

This file was deleted.

27 changes: 0 additions & 27 deletions pkgs/typed-api-spec/src/zod/validation.ts

This file was deleted.