From 79e95ed146c5bcd89d2e0a5d4f6c0e6fabae7f2b Mon Sep 17 00:00:00 2001 From: Florian Date: Tue, 14 Nov 2023 19:13:13 +0100 Subject: [PATCH] CI: add ci --- .eslintignore | 9 + .eslintrc.json | 30 +- .github/workflows/ci.yml | 15 + .prettierignore | 11 + .vscode/settings.json | 37 + README.md | 16 +- package.json | 16 +- packages/core/README.md | 16 +- packages/core/jest.config.js | 6 +- packages/core/package.json | 1 + packages/core/src/RouteDefinition.ts | 13 + packages/core/src/client/client.ts | 170 ++ packages/core/src/clientFactory.test.ts | 131 -- packages/core/src/clientFactory.ts | 97 - packages/core/src/index.ts | 15 +- packages/core/src/{ => providers}/Provider.ts | 2 +- .../src/{ => providers}/TypeboxProvider.ts | 7 +- .../core/src/{ => providers}/ZodProvider.ts | 7 +- packages/core/src/proxyFactory.ts | 164 -- packages/core/src/server/server.ts | 51 + packages/core/src/tests/stack.test.ts | 117 ++ packages/core/src/types.ts | 34 +- packages/core/tsup.config.ts | 8 +- packages/react-query/README.md | 16 +- packages/react-query/jest.config.js | 6 +- packages/react-query/package.json | 1 + packages/react-query/src/ReactQuery.ts | 60 +- packages/react-query/src/index.ts | 4 +- packages/react-query/tsconfig.json | 2 +- packages/react-query/tsup.config.ts | 10 +- site/docs/getting-started.md | 40 +- site/docs/intro.mdx | 8 +- site/docs/recipes/client-usage.md | 14 +- site/docs/recipes/react-query.md | 22 +- site/docs/recipes/recommended-architecture.md | 12 +- site/docs/recipes/server-usage.md | 20 +- site/docs/recipes/type-inference.md | 12 +- site/docusaurus.config.js | 75 +- site/sidebars.js | 2 +- .../src/components/HomepageFeatures/index.tsx | 6 +- site/src/components/Video.tsx | 4 +- yarn.lock | 1848 +++++++++++++++++ 42 files changed, 2535 insertions(+), 600 deletions(-) create mode 100644 .eslintignore create mode 100644 .github/workflows/ci.yml create mode 100644 .prettierignore create mode 100644 .vscode/settings.json create mode 100644 packages/core/src/RouteDefinition.ts create mode 100644 packages/core/src/client/client.ts delete mode 100644 packages/core/src/clientFactory.test.ts delete mode 100644 packages/core/src/clientFactory.ts rename packages/core/src/{ => providers}/Provider.ts (92%) rename packages/core/src/{ => providers}/TypeboxProvider.ts (56%) rename packages/core/src/{ => providers}/ZodProvider.ts (60%) delete mode 100644 packages/core/src/proxyFactory.ts create mode 100644 packages/core/src/server/server.ts create mode 100644 packages/core/src/tests/stack.test.ts diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1c9144c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,9 @@ +node_modules +dist +.webpack +.data +build +infra +release.config.js +.next +routeTree.gen.ts diff --git a/.eslintrc.json b/.eslintrc.json index b7fdeda..9c58cc2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -16,7 +16,13 @@ "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["react", "@typescript-eslint", "simple-import-sort"], + "plugins": [ + "react", + "@typescript-eslint", + "simple-import-sort", + "import", + "unused-imports" + ], "rules": { "simple-import-sort/imports": "error", "react/react-in-jsx-scope": "off", @@ -24,9 +30,25 @@ "react/display-name": 0, "@typescript-eslint/ban-ts-comment": "off", "react/prop-types": "off", - "(@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": "warn", - "prefer-const": "error" + "@typescript-eslint/no-empty-interface": "off", + "react/no-unescaped-entities": "off", + "@typescript-eslint/no-var-requires": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "error", + { + "vars": "all", + "varsIgnorePattern": "^_", + "args": "after-used", + "argsIgnorePattern": "^_" + } + ], + "import/no-useless-path-segments": ["error"], + "import/no-duplicates": ["error", { "considerQueryString": true }], + "import/no-unassigned-import": ["off"], + "import/newline-after-import": ["error", { "count": 1 }] }, "settings": { "react": { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2cb09e2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,15 @@ +name: CI + +on: [pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Run yarn install + run: yarn install + + - name: Run prettier and eslint + run: yarn lint diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..58da353 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +dist +.yarn +node_modules +.next + +styled-system +styled-system/** +styled-system/**/* +routeTree.gen.ts +build +.docusaurus diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..8134efd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,37 @@ +{ + "editor.formatOnSave": true, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "files.insertFinalNewline": true, + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/*/lib": true, + "**/*/build": true, + "**/.webpack": true + }, + "files.watcherExclude": { + "**/*/lib": true, + "**/*/dist": true, + "**/*/build": true, + "**/.webpack": true, + "**/node_modules": false + }, + "search.exclude": { + "**/*/lib": true, + "**/*/dist": true, + "**/*/build": true, + "**/.webpack": true, + "**/.next": true, + "**/node_modules": true + }, + "javascript.preferences.importModuleSpecifier": "relative", + "typescript.preferences.importModuleSpecifier": "relative" +} diff --git a/README.md b/README.md index 3832421..6a9c5e8 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,8 @@ Let's first create a route on the server: ```typescript title="Route creation with Fastify and Zod" // server.ts -import { createRoute } from "@http-wizard/core"; -import { z } from "zod"; +import { createRoute } from '@http-wizard/core'; +import { z } from 'zod'; const User = z.object({ id: z.string(), @@ -60,8 +60,8 @@ const User = z.object({ }); export const getUsers = (fastify: FastifyInstance) => { - return createRoute("/users", { - method: "GET", + return createRoute('/users', { + method: 'GET', schema: { response: { 200: z.array(User), @@ -86,13 +86,13 @@ Now, let's use the Router type on the client: ```typescript title="Client instanciation with axios" // client.ts -import { createClient, ZodTypeProvider } from "@http-wizard/core"; -import axios from "axios"; +import { createClient, ZodTypeProvider } from '@http-wizard/core'; +import axios from 'axios'; -import type { Router } from "./server"; +import type { Router } from './server'; const apiClient = createClient(axios.instance()); -const users = await apiClient.ref("[GET]/users").query({}); +const users = await apiClient.ref('[GET]/users').query({}); // users array is safe: { id:string, name:string }[] ``` diff --git a/package.json b/package.json index d381fe9..cae4ade 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,18 @@ { - "devDependencies": {}, - "dependencies": { + "scripts": { + "lint": "yarn lint:eslint && yarn lint:prettier", + "lint:eslint": "eslint .", + "lint:prettier": "prettier --check ." + }, + "devDependencies": { + "typescript": "^5.0.4", + "@typescript-eslint/eslint-plugin": "^5.30.0", + "@typescript-eslint/parser": "^5.30.0", + "eslint": "^8.51.0", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-react": "^7.30.1", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-unused-imports": "^3.0.0", "prettier": "^3.0.3" } } diff --git a/packages/core/README.md b/packages/core/README.md index 98742b6..8fb6fe6 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -51,8 +51,8 @@ Let's first create a route on the server: ```typescript title="Route creation with Fastify and Zod" // server.ts -import { createRoute } from "@http-wizard/core"; -import { z } from "zod"; +import { createRoute } from '@http-wizard/core'; +import { z } from 'zod'; const User = z.object({ id: z.string(), @@ -60,8 +60,8 @@ const User = z.object({ }); export const getUsers = (fastify: FastifyInstance) => { - return createRoute("/users", { - method: "GET", + return createRoute('/users', { + method: 'GET', schema: { response: { 200: z.array(User), @@ -86,13 +86,13 @@ Now, let's use the Router type on the client: ```typescript title="Client instanciation with axios" // client.ts -import { createClient, ZodTypeProvider } from "@http-wizard/core"; -import axios from "axios"; +import { createClient, ZodTypeProvider } from '@http-wizard/core'; +import axios from 'axios'; -import type { Router } from "./server"; +import type { Router } from './server'; const apiClient = createClient(axios.instance()); -const users = await apiClient.ref("[GET]/users").query({}); +const users = await apiClient.ref('[GET]/users').query({}); // users array is safe: { id:string, name:string }[] ``` diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index 33eadb7..6e476d5 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -1,8 +1,8 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { transform: { - "^.+\\.tsx?$": "esbuild-jest", + '^.+\\.tsx?$': 'esbuild-jest', }, - collectCoverageFrom: ["src/**/*.{ts,tsx}"], - testEnvironment: "node", + collectCoverageFrom: ['src/**/*.{ts,tsx}'], + testEnvironment: 'node', }; diff --git a/packages/core/package.json b/packages/core/package.json index b9435a2..f54514e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,6 +10,7 @@ "version": "1.3.16", "scripts": { "dev": "tsup src/index.ts --watch", + "typecheck": "tsc", "build": "tsup src/index.ts --dts", "test": "jest" }, diff --git a/packages/core/src/RouteDefinition.ts b/packages/core/src/RouteDefinition.ts new file mode 100644 index 0000000..6cb4058 --- /dev/null +++ b/packages/core/src/RouteDefinition.ts @@ -0,0 +1,13 @@ +import { AxiosRequestConfig } from 'axios'; + +import { SchemaTypeBox } from './providers/TypeboxProvider'; +import { SchemaZod } from './providers/ZodProvider'; + +export type Schema = SchemaTypeBox | SchemaZod; + +export type RouteDefinition = { + method: AxiosRequestConfig['method']; + url: string | (({ params }: { params: { [s: string]: string } }) => string); + okCode?: number; + schema: Schema; +}; diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts new file mode 100644 index 0000000..fcc5b50 --- /dev/null +++ b/packages/core/src/client/client.ts @@ -0,0 +1,170 @@ +import { AxiosInstance, AxiosRequestConfig } from 'axios'; + +import { TypeProvider } from '../providers/Provider'; +import { RouteDefinition } from '../RouteDefinition'; +import { Args, OkResponse } from '../types'; + +const processUrl = (url: string, args: object) => + Object.entries(('params' in args ? args?.params : undefined) ?? {}).reduce( + (acc, [key, value]) => + acc.replace(new RegExp(`:${key}`, 'g'), value as string), + url + ); + +export const createRouteUri = < + D extends RouteDefinition, + TP extends TypeProvider, +>({ + method, + url, + instance, + args, + config, +}: { + method: AxiosRequestConfig['method']; + url: string; + instance: AxiosInstance; + args: Args; + config?: AxiosRequestConfig; +}): string => { + return instance.getUri({ + method, + url: processUrl(url, args), + params: args?.query, + data: args?.body, + ...config, + ...args, + }); +}; + +export const query = async < + D extends RouteDefinition, + TP extends TypeProvider, +>({ + method, + url, + instance, + args, + config, +}: { + method: AxiosRequestConfig['method']; + url: string; + instance: AxiosInstance; + args: Args; + config?: AxiosRequestConfig; +}): Promise> => { + const { data } = await instance.request({ + method, + url: processUrl(url, args), + ...config, + params: 'query' in args ? args.query : undefined, + data: 'body' in args ? args.body : undefined, + }); + + return data; +}; + +export type Client< + Definitions extends Record, + TP extends TypeProvider, +> = { + ref: ( + url: URL + ) => Ref; + infer: { + [URL in keyof Definitions & string]: OkResponse; + }; + inferArgs: { + [URL in keyof Definitions & string]: Args; + }; +}; + +export type Ref< + Definitions extends Record, + URL extends keyof Definitions & string, + TP extends TypeProvider, +> = { + url: ( + args: Args, + config?: AxiosRequestConfig + ) => string; + query: ( + args: Args, + config?: AxiosRequestConfig + ) => Promise>; +}; + +export const createClient = < + Definitions extends Record, + TP extends TypeProvider, +>({ + instance, +}: { + instance: AxiosInstance; +}) => { + return { + route: ( + url: URL, + args: Args, + config?: AxiosRequestConfig + ) => { + const method = url.split(']')[0].replace('[', ''); + const shortUrl = url.split(']').slice(1).join(']'); + return { + url: createRouteUri({ + url: shortUrl, + method, + instance, + args, + config, + }), + query: () => { + return query({ + url: shortUrl, + method, + instance, + args, + config, + }); + }, + }; + }, + ref: ( + url: URL + ): Ref => { + const method = url.split(']')[0].replace('[', ''); + const shortUrl = url.split(']').slice(1).join(']'); + return { + url: ( + args: Args, + config?: AxiosRequestConfig + ) => + createRouteUri({ + url: shortUrl, + method, + instance, + args, + config, + }), + query: ( + args: Args, + config?: AxiosRequestConfig + ) => { + return query({ + url: shortUrl, + method, + instance, + args, + config, + }); + }, + }; + }, + infer: undefined as unknown as { + [URL in keyof Definitions & string]: OkResponse; + }, + inferArgs: undefined as unknown as { + [URL in keyof Definitions & string]: Args; + }, + }; +}; diff --git a/packages/core/src/clientFactory.test.ts b/packages/core/src/clientFactory.test.ts deleted file mode 100644 index b5ed551..0000000 --- a/packages/core/src/clientFactory.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { Type } from "@sinclair/typebox"; -import { createClient, createRoute } from "./proxyFactory"; -import { TypeBoxTypeProvider } from "./TypeboxProvider"; - -const getEasy = createRoute("/easy", { - method: "GET", - schema: { - response: { - 200: Type.Array( - Type.Object({ - name: Type.String(), - age: Type.Number(), - }) - ), - }, - }, -}).handle(() => {}); - -const getUser = createRoute("/user/:id", { - method: "GET", - schema: { - params: Type.Object({ - id: Type.String(), - }), - response: { - 200: Type.Array( - Type.Object({ - name: Type.String(), - age: Type.Number(), - }) - ), - }, - }, -}).handle(() => {}); - -const getToken = createRoute("/token", { - method: "GET", - schema: { - querystring: Type.Object({ size: Type.String() }), - response: { - 200: Type.String(), - }, - }, -}).handle(() => {}); - -const createUser = createRoute("/user", { - method: "POST", - schema: { - body: Type.Object({ name: Type.String() }), - response: { - 200: Type.String(), - }, - }, -}).handle(() => {}); - -const routes = { ...getUser, ...getToken, ...createUser, ...getEasy }; - -type Router = typeof routes; - -const client = createClient({} as any); - -describe("Check requests parameters and response", () => { - it("it should correctly call axios.request for a GET query with query parameters", async () => { - const request = jest.fn((params) => { - return { data: { name: "John Doe" } }; - }); - const client = createClient({ - instance: { request, getUri: () => "/user/toto" }, - } as any); - - const user = await client - .ref("[GET]/user/:id") - .query({ params: { id: "toto" } }); - - const url = await client.route("[GET]/user/:id", { params: { id: "toto" } }) - .url; - - const urld = await client - .ref("[GET]/user/:id") - .url({ params: { id: "toto" } }); - - expect(request.mock.calls?.[0]?.[0]).toMatchObject({ - url: "/user/toto", - method: "GET", - }); - expect(url).toBe("/user/toto"); - expect(user).toMatchObject({ name: "John Doe" }); - }); - - it("it should correctly call axios.request with corrects parameters for a GET query without arguments", async () => { - const request = jest.fn((params) => { - return { data: "my-token" }; - }); - const client = createClient({ - instance: { request, getUri: () => "" }, - } as any); - - const token = await client - .route("[GET]/token", { query: { size: "20" } }) - .query(); - - expect(request.mock.calls?.[0]?.[0]).toMatchObject({ - url: "/token", - method: "GET", - params: { size: "20" }, - }); - expect(token).toBe("my-token"); - }); - - it("it should correctly call axios.request on a POST query with a body", async () => { - const request = jest.fn((params) => { - return { data: { name: "John Doe" } }; - }); - const client = createClient({ - instance: { request, getUri: () => "" }, - } as any); - - const user = await client - .route("[POST]/user", { - body: { name: "John Doe" }, - }) - .query(); - - expect(request.mock.calls?.[0]?.[0]).toMatchObject({ - url: "/user", - method: "POST", - data: { name: "John Doe" }, - }); - expect(user).toMatchObject({ name: "John Doe" }); - }); -}); diff --git a/packages/core/src/clientFactory.ts b/packages/core/src/clientFactory.ts deleted file mode 100644 index 78623e9..0000000 --- a/packages/core/src/clientFactory.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { AxiosInstance, AxiosRequestConfig } from "axios"; -import { RouteDefinition, Schema } from "./types"; -import { CallTypeProvider, TypeProvider } from "./Provider"; - -export type Args< - S extends Schema, - TP extends TypeProvider -> = (S["params"] extends object - ? { params: CallTypeProvider } - : { params?: undefined }) & - (S["querystring"] extends object - ? { query: CallTypeProvider } - : { query?: undefined }) & - (S["body"] extends object - ? { body: CallTypeProvider } - : { body?: undefined }); - -export type Response< - S extends Schema, - OK extends number, - TP extends TypeProvider -> = CallTypeProvider; - -type Empty = O[keyof O] extends undefined | never - ? true - : false; - -type NeverIfEmpty = Empty extends true ? {} : O; - -const processUrl = (url: string, args: object) => - Object.entries(("params" in args ? args?.params : undefined) ?? {}).reduce( - (acc, [key, value]) => - acc.replace(new RegExp(`:${key}`, "g"), value as string), - url - ); - -export const createRouteUri = < - D extends RouteDefinition, - TP extends TypeProvider ->({ - method, - url, - instance, - args, - config, -}: { - method: AxiosRequestConfig["method"]; - url: string; - instance: AxiosInstance; - args: Args; - config?: AxiosRequestConfig; -}): string => { - return instance.getUri({ - method, - url: processUrl(url, args), - params: args?.query, - data: args?.body, - ...config, - ...args, - }); -}; - -export const query = async < - D extends RouteDefinition, - TP extends TypeProvider ->({ - method, - url, - instance, - args, - config, -}: { - method: AxiosRequestConfig["method"]; - url: string; - instance: AxiosInstance; - args: Args; - config?: AxiosRequestConfig; -}): Promise> => { - const { data } = await instance.request({ - method, - url: processUrl(url, args), - ...config, - params: "query" in args ? args.query : undefined, - data: "body" in args ? args.body : undefined, - }); - - return data; -}; - -export type OkResponse< - D extends RouteDefinition, - TP extends TypeProvider -> = Response; - -export const createRouteDefinition = ( - routeDefinition: R -) => routeDefinition; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 0c08b56..53b2448 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,7 +1,8 @@ -export type { TypeProvider } from "./Provider"; -export type { TypeBoxTypeProvider } from "./TypeboxProvider"; -export type { ZodTypeProvider } from "./ZodProvider"; -export type { RouteDefinition } from "./types"; -export type { OkResponse, Args } from "./clientFactory"; -export { createClient, createRoute } from "./proxyFactory"; -export type { Client, Ref } from "./proxyFactory"; +export type { TypeProvider } from './providers/Provider'; +export type { TypeBoxTypeProvider } from './providers/TypeboxProvider'; +export type { ZodTypeProvider } from './providers/ZodProvider'; +export type { RouteDefinition } from './RouteDefinition'; +export type { OkResponse, Args } from './types'; +export { createClient } from './client/client'; +export { createRoute } from './server/server'; +export type { Client, Ref } from './client/client'; diff --git a/packages/core/src/Provider.ts b/packages/core/src/providers/Provider.ts similarity index 92% rename from packages/core/src/Provider.ts rename to packages/core/src/providers/Provider.ts index 25dcf24..24c2289 100644 --- a/packages/core/src/Provider.ts +++ b/packages/core/src/providers/Provider.ts @@ -5,4 +5,4 @@ export interface TypeProvider { export type CallTypeProvider = (F & { input: I; -})["output"]; +})['output']; diff --git a/packages/core/src/TypeboxProvider.ts b/packages/core/src/providers/TypeboxProvider.ts similarity index 56% rename from packages/core/src/TypeboxProvider.ts rename to packages/core/src/providers/TypeboxProvider.ts index e94fc00..b153d55 100644 --- a/packages/core/src/TypeboxProvider.ts +++ b/packages/core/src/providers/TypeboxProvider.ts @@ -1,5 +1,6 @@ -import { Static, TSchema } from "@sinclair/typebox"; -import { TypeProvider } from "./Provider"; +import { Static, TSchema } from '@sinclair/typebox'; + +import { TypeProvider } from './Provider'; export type SchemaTypeBox = { params?: TSchema; @@ -9,5 +10,5 @@ export type SchemaTypeBox = { }; export interface TypeBoxTypeProvider extends TypeProvider { - output: this["input"] extends TSchema ? Static : never; + output: this['input'] extends TSchema ? Static : never; } diff --git a/packages/core/src/ZodProvider.ts b/packages/core/src/providers/ZodProvider.ts similarity index 60% rename from packages/core/src/ZodProvider.ts rename to packages/core/src/providers/ZodProvider.ts index aaf3e09..5772c2d 100644 --- a/packages/core/src/ZodProvider.ts +++ b/packages/core/src/providers/ZodProvider.ts @@ -1,5 +1,6 @@ -import { ZodType, z } from "zod"; -import { TypeProvider } from "./Provider"; +import { z, ZodType } from 'zod'; + +import { TypeProvider } from './Provider'; ``; export type SchemaZod = { @@ -10,5 +11,5 @@ export type SchemaZod = { }; export interface ZodTypeProvider extends TypeProvider { - output: this["input"] extends ZodType ? z.infer : never; + output: this['input'] extends ZodType ? z.infer : never; } diff --git a/packages/core/src/proxyFactory.ts b/packages/core/src/proxyFactory.ts deleted file mode 100644 index 78fc104..0000000 --- a/packages/core/src/proxyFactory.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { AxiosInstance, AxiosRequestConfig } from "axios"; -import { - Args, - createRouteDefinition, - OkResponse, - createRouteUri, - query, -} from "./clientFactory"; -import { RouteDefinition, Schema } from "./types"; -import { TypeProvider } from "./Provider"; - -const methods = [ - "GET", - "POST", - "PUT", - "DELETE", - "HEAD", - "PATCH", - "OPTIONS", - "COPY", - "MOVE", - "SEARCH", -] as const; - -export type Client< - Definitions extends Record, - TP extends TypeProvider -> = { - ref: ( - url: URL - ) => Ref; - infer: { - [URL in keyof Definitions & string]: OkResponse; - }; - inferArgs: { - [URL in keyof Definitions & string]: Args; - }; -}; - -export type Ref< - Definitions extends Record, - URL extends keyof Definitions & string, - TP extends TypeProvider -> = { - url: ( - args: Args, - config?: AxiosRequestConfig - ) => string; - query: ( - args: Args, - config?: AxiosRequestConfig - ) => Promise>; -}; - -export const createClient = < - Definitions extends Record, - TP extends TypeProvider ->({ - instance, -}: { - instance: AxiosInstance; -}) => { - return { - route: ( - url: URL, - args: Args, - config?: AxiosRequestConfig - ) => { - const method = url.split("]")[0].replace("[", ""); - const shortUrl = url.split("]").slice(1).join("]"); - return { - url: createRouteUri({ - url: shortUrl, - method, - instance, - args, - config, - }), - query: () => { - return query({ - url: shortUrl, - method, - instance, - args, - config, - }); - }, - }; - }, - ref: ( - url: URL - ): Ref => { - const method = url.split("]")[0].replace("[", ""); - const shortUrl = url.split("]").slice(1).join("]"); - return { - url: ( - args: Args, - config?: AxiosRequestConfig - ) => - createRouteUri({ - url: shortUrl, - method, - instance, - args, - config, - }), - query: ( - args: Args, - config?: AxiosRequestConfig - ) => { - return query({ - url: shortUrl, - method, - instance, - args, - config, - }); - }, - }; - }, - infer: undefined as unknown as { - [URL in keyof Definitions & string]: OkResponse; - }, - inferArgs: undefined as unknown as { - [URL in keyof Definitions & string]: Args; - }, - }; -}; - -export const createRoute = < - const URL extends string, - const D extends { - schema: Schema; - okCode?: number; - method: (typeof methods)[number]; - } ->( - url: URL, - options: D -) => { - return { - handle: ( - callback: (args: { - method: (typeof methods)[number]; - url: URL; - schema: D["schema"]; - }) => void - ) => { - callback({ - url, - method: options.method, - schema: options.schema, - }); - const routeDef = createRouteDefinition({ - url, - ...options, - }); - const key = `${options.method}/${url}` as `${D["method"]}${URL}`; - return { [key]: routeDef } as { - [k in `[${D["method"]}]${URL}`]: typeof routeDef; - }; - }, - }; -}; diff --git a/packages/core/src/server/server.ts b/packages/core/src/server/server.ts new file mode 100644 index 0000000..4b43a45 --- /dev/null +++ b/packages/core/src/server/server.ts @@ -0,0 +1,51 @@ +import { RouteDefinition, Schema } from '../RouteDefinition'; + +const methods = [ + 'GET', + 'POST', + 'PUT', + 'DELETE', + 'HEAD', + 'PATCH', + 'OPTIONS', + 'COPY', + 'MOVE', + 'SEARCH', +] as const; + +export const createRouteDefinition = ( + routeDefinition: R +) => routeDefinition; + +export const createRoute = < + const URL extends string, + const D extends { + schema: Schema; + okCode?: number; + method: (typeof methods)[number]; + }, +>( + url: URL, + options: D +) => { + return { + handle: ( + callback: (args: { + method: (typeof methods)[number]; + url: URL; + schema: D['schema']; + }) => void + ) => { + callback({ + url, + method: options.method, + schema: options.schema, + }); + const routeDef = { url, ...options }; + const key = `${options.method}/${url}` as `${D['method']}${URL}`; + return { [key]: routeDef } as { + [k in `[${D['method']}]${URL}`]: typeof routeDef; + }; + }, + }; +}; diff --git a/packages/core/src/tests/stack.test.ts b/packages/core/src/tests/stack.test.ts new file mode 100644 index 0000000..2cca093 --- /dev/null +++ b/packages/core/src/tests/stack.test.ts @@ -0,0 +1,117 @@ +import { Type } from '@sinclair/typebox'; +import { AxiosInstance } from 'axios'; + +import { createClient } from '../client/client'; +import { TypeBoxTypeProvider } from '../providers/TypeboxProvider'; +import { createRoute } from '../server/server'; + +const getUser = createRoute('/user/:id', { + method: 'GET', + schema: { + params: Type.Object({ + id: Type.String(), + }), + response: { + 200: Type.Array( + Type.Object({ + name: Type.String(), + age: Type.Number(), + }) + ), + }, + }, +}).handle(() => {}); + +const getToken = createRoute('/token', { + method: 'GET', + schema: { + querystring: Type.Object({ size: Type.String() }), + response: { + 200: Type.String(), + }, + }, +}).handle(() => {}); + +const createUser = createRoute('/user', { + method: 'POST', + schema: { + body: Type.Object({ name: Type.String() }), + response: { + 200: Type.String(), + }, + }, +}).handle(() => {}); + +const routes = { ...getUser, ...getToken, ...createUser }; + +type Router = typeof routes; + +describe('Check requests parameters and response', () => { + it('it should correctly call axios.request for a GET query with query parameters', async () => { + const request = jest.fn((_params) => { + return { data: { name: 'John Doe' } }; + }); + const client = createClient({ + instance: { + request, + getUri: () => '/user/toto', + } as unknown as AxiosInstance, + }); + + const user = await client + .ref('[GET]/user/:id') + .query({ params: { id: 'toto' } }); + + const url = await client.route('[GET]/user/:id', { params: { id: 'toto' } }) + .url; + + expect(request.mock.calls?.[0]?.[0]).toMatchObject({ + url: '/user/toto', + method: 'GET', + }); + expect(url).toBe('/user/toto'); + expect(user).toMatchObject({ name: 'John Doe' }); + }); + + it('it should correctly call axios.request with corrects parameters for a GET query without arguments', async () => { + const request = jest.fn((_params) => { + return { data: 'my-token' }; + }); + const client = createClient({ + instance: { request, getUri: () => '' } as unknown as AxiosInstance, + }); + + const token = await client + .route('[GET]/token', { query: { size: '20' } }) + .query(); + + expect(request.mock.calls?.[0]?.[0]).toMatchObject({ + url: '/token', + method: 'GET', + params: { size: '20' }, + }); + expect(token).toBe('my-token'); + }); + + it('it should correctly call axios.request on a POST query with a body', async () => { + const request = jest.fn((_params) => { + return { data: { name: 'John Doe' } }; + }); + const client = createClient({ + instance: { request, getUri: () => '' } as unknown as AxiosInstance, + }); + + const user = await client + .route('[POST]/user', { + body: { name: 'John Doe' }, + }) + .query(); + + expect(request.mock.calls?.[0]?.[0]).toMatchObject({ + url: '/user', + method: 'POST', + data: { name: 'John Doe' }, + }); + expect(user).toMatchObject({ name: 'John Doe' }); + }); +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 8ef2041..caa355d 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,12 +1,26 @@ -import { AxiosRequestConfig } from "axios"; -import { SchemaTypeBox } from "./TypeboxProvider"; -import { SchemaZod } from "./ZodProvider"; +import { CallTypeProvider, TypeProvider } from './providers/Provider'; +import { RouteDefinition, Schema } from './RouteDefinition'; -export type Schema = SchemaTypeBox | SchemaZod; +export type Args< + S extends Schema, + TP extends TypeProvider, +> = (S['params'] extends object + ? { params: CallTypeProvider } + : { params?: undefined }) & + (S['querystring'] extends object + ? { query: CallTypeProvider } + : { query?: undefined }) & + (S['body'] extends object + ? { body: CallTypeProvider } + : { body?: undefined }); -export type RouteDefinition = { - method: AxiosRequestConfig["method"]; - url: string | (({ params }: { params: { [s: string]: string } }) => string); - okCode?: number; - schema: Schema; -}; +export type Response< + S extends Schema, + OK extends number, + TP extends TypeProvider, +> = CallTypeProvider; + +export type OkResponse< + D extends RouteDefinition, + TP extends TypeProvider, +> = Response; diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index d4e360d..403597d 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -1,9 +1,9 @@ -import { defineConfig } from "tsup"; +import { defineConfig } from 'tsup'; export default defineConfig({ - target: "es2015", - platform: "browser", - format: ["cjs", "esm"], + target: 'es2015', + platform: 'browser', + format: ['cjs', 'esm'], splitting: false, shims: false, minify: false, diff --git a/packages/react-query/README.md b/packages/react-query/README.md index 98742b6..8fb6fe6 100644 --- a/packages/react-query/README.md +++ b/packages/react-query/README.md @@ -51,8 +51,8 @@ Let's first create a route on the server: ```typescript title="Route creation with Fastify and Zod" // server.ts -import { createRoute } from "@http-wizard/core"; -import { z } from "zod"; +import { createRoute } from '@http-wizard/core'; +import { z } from 'zod'; const User = z.object({ id: z.string(), @@ -60,8 +60,8 @@ const User = z.object({ }); export const getUsers = (fastify: FastifyInstance) => { - return createRoute("/users", { - method: "GET", + return createRoute('/users', { + method: 'GET', schema: { response: { 200: z.array(User), @@ -86,13 +86,13 @@ Now, let's use the Router type on the client: ```typescript title="Client instanciation with axios" // client.ts -import { createClient, ZodTypeProvider } from "@http-wizard/core"; -import axios from "axios"; +import { createClient, ZodTypeProvider } from '@http-wizard/core'; +import axios from 'axios'; -import type { Router } from "./server"; +import type { Router } from './server'; const apiClient = createClient(axios.instance()); -const users = await apiClient.ref("[GET]/users").query({}); +const users = await apiClient.ref('[GET]/users').query({}); // users array is safe: { id:string, name:string }[] ``` diff --git a/packages/react-query/jest.config.js b/packages/react-query/jest.config.js index 33eadb7..6e476d5 100644 --- a/packages/react-query/jest.config.js +++ b/packages/react-query/jest.config.js @@ -1,8 +1,8 @@ /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ module.exports = { transform: { - "^.+\\.tsx?$": "esbuild-jest", + '^.+\\.tsx?$': 'esbuild-jest', }, - collectCoverageFrom: ["src/**/*.{ts,tsx}"], - testEnvironment: "node", + collectCoverageFrom: ['src/**/*.{ts,tsx}'], + testEnvironment: 'node', }; diff --git a/packages/react-query/package.json b/packages/react-query/package.json index 59b202d..a8783f9 100644 --- a/packages/react-query/package.json +++ b/packages/react-query/package.json @@ -10,6 +10,7 @@ "version": "1.3.16", "scripts": { "dev": "tsup src/index.ts --watch", + "typecheck": "tsc", "build": "tsup src/index.ts --dts", "test": "jest" }, diff --git a/packages/react-query/src/ReactQuery.ts b/packages/react-query/src/ReactQuery.ts index 86bcf54..f221e17 100644 --- a/packages/react-query/src/ReactQuery.ts +++ b/packages/react-query/src/ReactQuery.ts @@ -1,27 +1,27 @@ import { Args, Client, + createClient, OkResponse, Ref, RouteDefinition, TypeProvider, - createClient, -} from "@http-wizard/core"; +} from '@http-wizard/core'; import { FetchQueryOptions, QueryClient, + useInfiniteQuery, UseInfiniteQueryOptions, UseInfiniteQueryResult, + useMutation, UseMutationOptions, UseMutationResult, - UseQueryOptions, - UseQueryResult, - useInfiniteQuery, - useMutation, useQuery, useQueryClient, -} from "@tanstack/react-query"; -import axios, { AxiosRequestConfig } from "axios"; + UseQueryOptions, + UseQueryResult, +} from '@tanstack/react-query'; +import axios, { AxiosRequestConfig } from 'axios'; export const createQueryClient = < Definitions extends Record, @@ -31,23 +31,23 @@ export const createQueryClient = < ...options }: Parameters[0] & { queryClient?: QueryClient; -}): Omit, "ref"> & { +}): Omit, 'ref'> & { ref: ( url: URL ) => Ref & { useQuery: ( - args: Args, + args: Args, options?: Omit< UseQueryOptions>, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, config?: AxiosRequestConfig ) => UseQueryResult>; useInfiniteQuery: ( - args: Args, + args: Args, options: Omit< UseInfiniteQueryOptions>, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, config?: AxiosRequestConfig ) => UseInfiniteQueryResult>; @@ -55,21 +55,21 @@ export const createQueryClient = < options?: UseMutationOptions< OkResponse, Error, - Args + Args >, - config?: Parameters["query"]>[1] + config?: Parameters['query']>[1] ) => UseMutationResult< OkResponse, Error, - Args + Args >; prefetchQuery: ( - args: Args, + args: Args, options?: Omit< FetchQueryOptions>, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, - config?: Parameters["query"]>[1] + config?: Parameters['query']>[1] ) => Promise; }; } => { @@ -82,12 +82,12 @@ export const createQueryClient = < const routeRef = client.ref(url); return { useQuery: ( - args: Args, + args: Args, options?: Omit< UseQueryOptions>, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, - config?: Parameters["query"]>[1] + config?: Parameters['query']>[1] ) => useQuery( { @@ -98,12 +98,12 @@ export const createQueryClient = < optionQueryClient ), useInfiniteQuery: ( - args: Args, + args: Args, options: Omit< UseInfiniteQueryOptions>, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, - config?: Parameters["query"]>[1] + config?: Parameters['query']>[1] ) => useInfiniteQuery( { @@ -117,9 +117,9 @@ export const createQueryClient = < options?: UseMutationOptions< OkResponse, Error, - Args + Args >, - config?: Parameters["query"]>[1] + config?: Parameters['query']>[1] ) => useMutation( { @@ -130,12 +130,12 @@ export const createQueryClient = < optionQueryClient ), prefetchQuery: ( - args: Args, + args: Args, options?: Omit< FetchQueryOptions>, - "queryKey" | "queryFn" + 'queryKey' | 'queryFn' >, - config?: Parameters["query"]>[1] + config?: Parameters['query']>[1] ) => { const queryClient = optionQueryClient ?? useQueryClient(); return queryClient.prefetchQuery({ diff --git a/packages/react-query/src/index.ts b/packages/react-query/src/index.ts index 490f13f..15c57b2 100644 --- a/packages/react-query/src/index.ts +++ b/packages/react-query/src/index.ts @@ -1,2 +1,2 @@ -export type { Client, RouteDefinition, OkResponse } from "@http-wizard/core"; -export { createQueryClient } from "./ReactQuery"; +export type { Client, RouteDefinition, OkResponse } from '@http-wizard/core'; +export { createQueryClient } from './ReactQuery'; diff --git a/packages/react-query/tsconfig.json b/packages/react-query/tsconfig.json index 4b69b98..53e8bdf 100644 --- a/packages/react-query/tsconfig.json +++ b/packages/react-query/tsconfig.json @@ -15,7 +15,7 @@ "outDir": "./dist" /* Specify an output folder for all emitted files. */, "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, "strict": true /* Enable all strict type-checking options. */, - "skipLibCheck": false, + "skipLibCheck": true, "declaration": true, "emitDeclarationOnly": true, "forceConsistentCasingInFileNames": true, diff --git a/packages/react-query/tsup.config.ts b/packages/react-query/tsup.config.ts index 7deb7d9..c748b9c 100644 --- a/packages/react-query/tsup.config.ts +++ b/packages/react-query/tsup.config.ts @@ -1,13 +1,13 @@ -import { defineConfig } from "tsup"; +import { defineConfig } from 'tsup'; export default defineConfig({ - target: "es2015", - platform: "browser", - format: ["cjs", "esm"], + target: 'es2015', + platform: 'browser', + format: ['cjs', 'esm'], splitting: false, shims: false, minify: false, sourcemap: true, clean: true, - external: ["http-wizard"], + external: ['http-wizard'], }); diff --git a/site/docs/getting-started.md b/site/docs/getting-started.md index f4ce65d..97957e6 100644 --- a/site/docs/getting-started.md +++ b/site/docs/getting-started.md @@ -21,14 +21,14 @@ Here is an example with Zod. ```typescript title="Route creation with Fastify and Zod" // server.ts -import { createRoute } from "@http-wizard/core"; -import fastify from "fastify"; +import { createRoute } from '@http-wizard/core'; +import fastify from 'fastify'; import { serializerCompiler, validatorCompiler, ZodTypeProvider, -} from "fastify-type-provider-zod"; -import { z } from "zod"; +} from 'fastify-type-provider-zod'; +import { z } from 'zod'; const User = z.object({ id: z.string(), @@ -36,8 +36,8 @@ const User = z.object({ }); export const getUsersRoute = (fastify: FastifyInstance) => { - return createRoute("/users", { - method: "GET", + return createRoute('/users', { + method: 'GET', schema: { response: { 200: z.array(User), @@ -67,13 +67,13 @@ Now, let's use the Router type on the client: ```typescript title="Client instancation with axios" // client.ts -import { createClient, ZodTypeProvider } from "@http-wizard/core"; -import axios from "axios"; +import { createClient, ZodTypeProvider } from '@http-wizard/core'; +import axios from 'axios'; -import type { Router } from "./server"; +import type { Router } from './server'; const apiClient = createClient(axios.instance()); -const users = await apiClient.ref("[GET]/users").query({}); +const users = await apiClient.ref('[GET]/users').query({}); // users array is safe: { id:string, name:string }[] ``` @@ -81,14 +81,14 @@ Let's first create a route on the server: ```typescript title="Route creation with Fastify and Zod" // server.ts -import { createRoute } from "@http-wizard/core"; -import fastify from "fastify"; +import { createRoute } from '@http-wizard/core'; +import fastify from 'fastify'; import { serializerCompiler, validatorCompiler, ZodTypeProvider, -} from "fastify-type-provider-zod"; -import { z } from "zod"; +} from 'fastify-type-provider-zod'; +import { z } from 'zod'; const User = z.object({ id: z.string(), @@ -96,8 +96,8 @@ const User = z.object({ }); export const getUsersRoute = (fastify: FastifyInstance) => { - return createRoute("/users", { - method: "GET", + return createRoute('/users', { + method: 'GET', schema: { response: { 200: z.array(User), @@ -127,13 +127,13 @@ Now, let's use the Router type on the client: ```typescript title="Client instancation with axios" // client.ts -import { createClient, ZodTypeProvider } from "@http-wizard/core"; -import axios from "axios"; +import { createClient, ZodTypeProvider } from '@http-wizard/core'; +import axios from 'axios'; -import type { Router } from "./server"; +import type { Router } from './server'; const apiClient = createClient(axios.instance()); -const users = await apiClient.ref("[GET]/users").query({}); +const users = await apiClient.ref('[GET]/users').query({}); // users array is safe: { id:string, name:string }[] ``` diff --git a/site/docs/intro.mdx b/site/docs/intro.mdx index 190a875..b969fe0 100644 --- a/site/docs/intro.mdx +++ b/site/docs/intro.mdx @@ -3,11 +3,11 @@ sidebar_position: 1 slug: / --- -import { Video } from "./../src/components/Video"; +import { Video } from './../src/components/Video'; # Introduction -
+