Skip to content
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
4 changes: 2 additions & 2 deletions examples/express.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import express from "express";
import { asAsync, typed } from "../src";
import { asAsync, typed } from "../src/express";
import { pathMap } from "./spec";

const newApp = () => {
Expand All @@ -9,7 +9,7 @@ const newApp = () => {
// ```
// // validatorMiddleware allows to use res.locals.validate method
// app.use(validatorMiddleware(pathMap));
// // wApp is same as app, but with additional type information
// // wApp is same as app, but with additional common information
// const wApp = app as TRouter<typeof pathMap>;
// ```
const wApp = asAsync(typed(pathMap, app));
Expand Down
12 changes: 6 additions & 6 deletions examples/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { PathMap } from "./spec";
import { TFetch } from "../src";
import { JSON$stringifyT } from "../src/json";
import { JSON$stringifyT } from "../src";
import { unreachable } from "../src/utils";
import FetchT from "../src/fetch";

const fetchT = fetch as TFetch<typeof origin, PathMap>;
const fetchT = fetch as FetchT<typeof origin, PathMap>;
const origin = "http://localhost:3000";
const headers = { "Content-Type": "application/json" };
// stringify is same as JSON.stringify but with type information
// stringify is same as JSON.stringify but with common information
const stringify = JSON.stringify as JSON$stringifyT;

const main = async () => {
Expand Down Expand Up @@ -34,7 +34,7 @@ const main = async () => {

{
// query parameter example
// TODO: Add type information for query parameter
// TODO: Add common information for query parameter
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Add common information for query parameter.

Would you like me to implement this enhancement or should I open a GitHub issue to track this task?

const path = `${origin}/users?page=1`;
const method = "get";
const res = await fetchT(path, { method });
Expand All @@ -56,7 +56,7 @@ const main = async () => {
method,
headers,
// body is the request schema defined in pathMap["/users"]["post"].body
// stringify is same as JSON.stringify but with type information
// stringify is same as JSON.stringify but with common information
body: stringify({ userName: "user1" }),
});
if (res.ok) {
Expand Down
26 changes: 23 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,30 @@
"import": "./dist/index.mjs",
"types": "./dist/index.d.ts"
},
"./common": {
"require": "./dist/common/index.js",
"import": "./dist/common/index.mjs",
"types": "./dist/common/index.d.ts"
},
"./express": {
"require": "./dist/express.js",
"import": "./dist/express.mjs",
"types": "./dist/express.d.ts"
"require": "./dist/express/index.js",
"import": "./dist/express/index.mjs",
"types": "./dist/express/index.d.ts"
},
"./fetch": {
"require": "./dist/fetch/index.js",
"import": "./dist/fetch/index.mjs",
"types": "./dist/fetch/index.d.ts"
},
"./json": {
"require": "./dist/json/index.js",
"import": "./dist/json/index.mjs",
"types": "./dist/json/index.d.ts"
},
"./zod": {
"require": "./dist/zod/index.js",
"import": "./dist/zod/index.mjs",
"types": "./dist/zod/index.d.ts"
}
},
"main": "./dist/index.js",
Expand Down
File renamed without changes.
5 changes: 5 additions & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./hono-types";
export * from "./query-string";
export * from "./spec";
export * from "./type";
export * from "./url";
File renamed without changes.
File renamed without changes.
12 changes: 11 additions & 1 deletion src/spec.ts → src/common/spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { StatusCode } from "./hono-types";
import { ClientResponse, StatusCode } from "./hono-types";
import { ParseUrlParams } from "./url";

export type ApiResponses = Partial<Record<StatusCode, z.ZodTypeAny>>;
Expand Down Expand Up @@ -68,3 +68,13 @@ export type Method = (typeof Method)[number];
export type ApiEndpoints = {
[K in string]: Partial<Record<Method, ApiSpec<ParseUrlParams<K>>>>;
};

type ApiClientResponses<AResponses extends ApiResponses> = {
[SC in keyof AResponses & StatusCode]: ClientResponse<
z.infer<ApiResSchema<AResponses, SC>>,
SC,
"json"
>;
};
export type MergeApiResponses<AR extends ApiResponses> =
ApiClientResponses<AR>[keyof ApiClientResponses<AR>];
File renamed without changes.
3 changes: 3 additions & 0 deletions src/common/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type FilterNever<T extends Record<string, unknown>> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
};
5 changes: 1 addition & 4 deletions src/url.t-test.ts → src/common/url.t-test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { Equal, Expect } from "./type-test";
import { OriginPattern, ParseOrigin, ParseURL } from "./url";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const o: OriginPattern = "https://example.com";
import { ParseOrigin, ParseURL } from "./url";

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type cases = [
Expand Down
File renamed without changes.
44 changes: 25 additions & 19 deletions src/express.ts → src/express/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { IRouter, RequestHandler, Router } from "express";
import { ApiEndpoints, ApiResponses, ApiResSchema, ApiSpec, Method } from "./";
import { Validator, Validators } from "./validator";
import {
ApiEndpoints,
ApiResponses,
ApiResSchema,
ApiSpec,
Method,
} from "../index";
import { ZodValidator, ZodValidators } from "../zod";
import {
NextFunction,
ParamsDictionary,
Request,
Response,
} from "express-serve-static-core";
import { StatusCode } from "./hono-types";
import { StatusCode } from "../common";
import { z } from "zod";
import { ParseUrlParams } from "./url";
import { ParseUrlParams } from "../common";

interface ParsedQs {
export interface ParsedQs {
[key: string]: undefined | string | string[] | ParsedQs | ParsedQs[];
}
type Handler<
export type Handler<
Spec extends ApiSpec | undefined,
SC extends keyof NonNullable<ApiSpec>["res"] & StatusCode = 200,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -26,7 +32,7 @@ type Handler<
next: NextFunction,
) => void;

type ExpressResponse<
export type ExpressResponse<
Responses extends ApiResponses,
SC extends keyof Responses & StatusCode,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -40,18 +46,18 @@ type ExpressResponse<
) => Response<z.infer<ApiResSchema<Responses, SC>>, LocalsObj, SC>;
};

type ValidateLocals<
export type ValidateLocals<
AS extends ApiSpec | undefined,
QueryKeys extends string,
> = AS extends ApiSpec
? {
validate: (
req: Request<ParamsDictionary, unknown, unknown, unknown>,
) => Validators<AS, QueryKeys>;
) => ZodValidators<AS, QueryKeys>;
}
: Record<string, never>;

type TRouter<
export type RouterT<
Endpoints extends ApiEndpoints,
SC extends StatusCode = StatusCode,
> = Omit<IRouter, Method> & {
Expand All @@ -64,7 +70,7 @@ type TRouter<
ValidateLocals<Endpoints[Path][M], ParseUrlParams<Path>>
>
>
) => TRouter<Endpoints, SC>;
) => RouterT<Endpoints, SC>;
};

const validatorMiddleware = (pathMap: ApiEndpoints) => {
Expand All @@ -78,7 +84,7 @@ const validatorMiddleware = (pathMap: ApiEndpoints) => {
export const typed = <const Endpoints extends ApiEndpoints>(
pathMap: Endpoints,
router: Router,
): TRouter<Endpoints> => {
): RouterT<Endpoints> => {
router.use(validatorMiddleware(pathMap));
return router;
};
Expand All @@ -92,21 +98,21 @@ export const newValidator = <E extends ApiEndpoints>(endpoints: E) => {
return {
params: () =>
spec?.params?.safeParse(req.params) as E[Path][M] extends ApiSpec
? Validator<E[Path][M]["params"]>
? ZodValidator<E[Path][M]["params"]>
: undefined,
body: () =>
spec?.body?.safeParse(req.body) as E[Path][M] extends ApiSpec
? Validator<E[Path][M]["body"]>
? ZodValidator<E[Path][M]["body"]>
: undefined,
query: () =>
spec?.query?.safeParse(req.query) as E[Path][M] extends ApiSpec
? Validator<E[Path][M]["query"]>
? ZodValidator<E[Path][M]["query"]>
: undefined,
};
};
};

type AsyncRequestHandler<Handler extends RequestHandler> = (
export type AsyncRequestHandler<Handler extends RequestHandler> = (
req: Parameters<NoInfer<Handler>>[0],
res: Parameters<NoInfer<Handler>>[1],
next: Parameters<NoInfer<Handler>>[2],
Expand All @@ -123,13 +129,13 @@ export const wrap = <Handler extends RequestHandler>(
const wrapHandlers = (handlers: never[]) =>
handlers.map((h) => wrap(h) as never);
export const asAsync = <T extends ApiEndpoints>(
router: TRouter<T>,
): TRouter<T> => {
router: RouterT<T>,
): RouterT<T> => {
return Method.reduce((acc, method) => {
return {
...acc,
[method]: (path: string, ...handlers: never[]) =>
router[method](path, ...wrapHandlers(handlers)),
};
}, {} as TRouter<T>);
}, {} as RouterT<T>);
};
29 changes: 9 additions & 20 deletions src/fetch.ts → src/fetch/index.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import {
ApiBodySchema,
ApiEndpoints,
ApiResponses,
ApiResSchema,
InferOrUndefined,
MergeApiResponses,
Method,
} from "./spec";
import { StatusCode, ClientResponse } from "./hono-types";
import { z } from "zod";
} from "../common";
import {
MatchedPatterns,
OriginPattern,
ParseURL,
ToUrlParamPattern,
} from "./url";
import { TypedString } from "./json";
} from "../common";
import { TypedString } from "../json";

interface TRequestInit<
export interface RequestInitT<
M extends Method,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Body extends Record<string, any> | undefined,
Expand All @@ -25,17 +22,7 @@ interface TRequestInit<
body?: TypedString<Body>;
}

type ApiClientResponses<AResponses extends ApiResponses> = {
[SC in keyof AResponses & StatusCode]: ClientResponse<
z.infer<ApiResSchema<AResponses, SC>>,
SC,
"json"
>;
};
export type MergeApiResponses<AR extends ApiResponses> =
ApiClientResponses<AR>[keyof ApiClientResponses<AR>];

export type TFetch<Origin extends OriginPattern, E extends ApiEndpoints> = <
type FetchT<Origin extends OriginPattern, E extends ApiEndpoints> = <
Input extends
| `${Origin}${ToUrlParamPattern<keyof E & string>}`
| `${Origin}${ToUrlParamPattern<keyof E & string>}?${string}`,
Expand All @@ -44,6 +31,8 @@ export type TFetch<Origin extends OriginPattern, E extends ApiEndpoints> = <
M extends Method = "get",
>(
input: Input,
init?: TRequestInit<M, InferOrUndefined<ApiBodySchema<E, CandidatePaths, M>>>,
init?: RequestInitT<M, InferOrUndefined<ApiBodySchema<E, CandidatePaths, M>>>,
// FIXME: NonNullable
) => Promise<MergeApiResponses<NonNullable<E[CandidatePaths][M]>["res"]>>;

export default FetchT;
24 changes: 21 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
export * from "./spec";
export * from "./fetch";
export * from "./express";
export * from "./common";
export {
asAsync as expressAsAsync,
wrap as expressWrap,
AsyncRequestHandler as ExpressAsyncRequestHandler,
newValidator as expressNewValidator,
typed as expressTyped,
ExpressResponse,
ValidateLocals as ExpressValidateLocals,
RouterT as ExpressRouterT,
Handler as ExpressHandler,
ParsedQs as ExpressParsedQs,
} from "./express";

import FetchT, { RequestInitT } from "./fetch";
export { FetchT, RequestInitT };

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

export * from "./zod";
6 changes: 6 additions & 0 deletions src/json.ts → src/json/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,9 @@ export type JSON$stringifyT = <T>(
replacer?: undefined,
space?: number | string | undefined,
) => TypedString<T>;

type JSONT = JSON & {
stringify: JSON$stringifyT;
};

export default JSONT;
15 changes: 6 additions & 9 deletions src/validator.ts → src/zod/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { z } from "zod";
import { ApiSpec } from "./spec";
import { ApiSpec } from "../common";
import { FilterNever } from "../common";

type SafeParse<Z extends z.ZodTypeAny> = ReturnType<Z["safeParse"]>;
export type Validator<V extends z.ZodTypeAny | undefined> =
export type ZodValidator<V extends z.ZodTypeAny | undefined> =
V extends z.ZodTypeAny ? () => ReturnType<V["safeParse"]> : never;
export type Validators<
export type ZodValidators<
AS extends ApiSpec,
QueryKeys extends string,
> = FilterNever<{
Expand All @@ -13,10 +14,6 @@ export type Validators<
: AS["params"] extends z.ZodTypeAny
? () => SafeParse<AS["params"]>
: () => SafeParse<z.ZodType<Record<QueryKeys, string>>>;
query: Validator<AS["query"]>;
body: Validator<AS["body"]>;
query: ZodValidator<AS["query"]>;
body: ZodValidator<AS["body"]>;
}>;

type FilterNever<T extends Record<string, unknown>> = {
[K in keyof T as T[K] extends never ? never : K]: T[K];
};