diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index d51776b..8e6749f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -25,5 +25,4 @@ jobs: - name: Install deps run: pnpm install - - - run: pnpm test:panel + diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 6e72053..1553d34 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -5,30 +5,8 @@ on: types: [published] jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v4 - - - name: Install node.js - uses: actions/setup-node@v4 - with: - node-version-file: '.node-version' - registry-url: https://registry.npmjs.org/ - cache: 'pnpm' - - - name: Install deps - run: pnpm install - - - run: pnpm test:panel - publish-npm: - needs: test runs-on: ubuntu-latest concurrency: "run" steps: diff --git a/.gitignore b/.gitignore index d5280a5..876eb6d 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,5 @@ Thumbs.db stats.html # yarn -.yarn \ No newline at end of file +.yarn +.claude \ No newline at end of file diff --git a/package.json b/package.json index 6dd99e3..c4707af 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "license": "MIT", "packageManager": "pnpm@9.15.1", "scripts": { - "test:panel": "pnpm -F trpc-ui test", "build:panel": "pnpm -F trpc-ui build", "build:test-app": "pnpm -F test-app build", "dev:dev-app": "pnpm -F dev-app dev", diff --git a/packages/dev-app/package.json b/packages/dev-app/package.json index fa520de..e23fa60 100644 --- a/packages/dev-app/package.json +++ b/packages/dev-app/package.json @@ -4,18 +4,30 @@ "private": true, "scripts": { "build": "next build", - "dev": "next dev", + "dev": "if [ -f .env ]; then export $(grep -v '^#' .env | xargs); fi && NEXT_PUBLIC_PORT=${NEXT_PUBLIC_PORT:-3000} && echo \"Starting dev server on port $NEXT_PUBLIC_PORT\" && next dev --port=$NEXT_PUBLIC_PORT", "lint": "next lint", "start": "next start" }, "dependencies": { + "@chakra-ui/core": "1.0.0-rc.8", + "@jsonforms/material-renderers": "^3.5.1", + "@jsonforms/react": "^3.5.1", + "@rjsf/chakra-ui": "^5.24.8", + "@rjsf/core": "^5.24.8", + "@rjsf/material-ui": "^5.24.8", + "@rjsf/mui": "^5.24.8", + "@rjsf/utils": "^5.24.8", + "@rjsf/validator-ajv8": "^5.24.8", "@tanstack/react-query": "^5.12.2", - "@trpc/client": "^11.0.0-next-beta.264", - "@trpc/next": "^11.0.0-next-beta.264", - "@trpc/react-query": "^11.0.0-next-beta.264", - "@trpc/server": "^11.0.0-next-beta.264", + "@trpc/client": "^11.4.3", + "@trpc/next": "^11.4.3", + "@trpc/react-query": "^11.4.3", + "@trpc/server": "^11.4.3", + "@types/json-schema": "^7.0.15", + "@valibot/to-json-schema": "1.0.0-rc.0", + "arktype": "^2.1.9", "autoprefixer": "^10.4.14", - "next": "^13.2.4", + "next": "^15.4.4", "next-transpile-modules": "^10.0.0", "postcss": "^8.4.21", "react": "18.2.0", @@ -23,7 +35,9 @@ "superjson": "1.12.2", "tailwindcss": "^3.3.1", "ts-loader": "^9.4.2", - "zod": "^3.24.2" + "valibot": "1.1.0", + "zod": "^3.25.0", + "zod-to-json-schema": "^3.24.4" }, "devDependencies": { "@types/node": "^18.15.5", diff --git a/packages/dev-app/src/env.mjs b/packages/dev-app/src/env.mjs index c34de06..a336f29 100644 --- a/packages/dev-app/src/env.mjs +++ b/packages/dev-app/src/env.mjs @@ -5,8 +5,9 @@ import { z } from "zod"; * built with invalid env vars. */ const server = z.object({ - NODE_ENV: z.enum(["development", "test", "production"]), - NEXT_PUBLIC_SUPERJSON: z.enum(["true", "false"]), + NODE_ENV: z.enum(["development", "test", "production"]).optional(), + NEXT_PUBLIC_SUPERJSON: z.enum(["true", "false"]).optional(), + NEXT_PUBLIC_PORT: z.coerce.number().default(3000), }); /** @@ -14,7 +15,8 @@ const server = z.object({ * built with invalid env vars. To expose them to the client, prefix them with `NEXT_PUBLIC_`. */ const client = z.object({ - NEXT_PUBLIC_SUPERJSON: z.enum(["true", "false"]), + NEXT_PUBLIC_SUPERJSON: z.enum(["true", "false"]).optional(), + NEXT_PUBLIC_PORT: z.coerce.number().default(3000), }); /** @@ -26,6 +28,7 @@ const client = z.object({ const processEnv = { NODE_ENV: process.env.NODE_ENV, NEXT_PUBLIC_SUPERJSON: process.env.NEXT_PUBLIC_SUPERJSON, + NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT, }; // Don't touch the part below diff --git a/packages/dev-app/src/pages/api/trpc-superjson/[trpc].ts b/packages/dev-app/src/pages/api/trpc-superjson/[trpc].ts new file mode 100644 index 0000000..c1c220a --- /dev/null +++ b/packages/dev-app/src/pages/api/trpc-superjson/[trpc].ts @@ -0,0 +1,58 @@ +import { initTRPC } from "@trpc/server"; +import { createNextApiHandler } from "@trpc/server/adapters/next"; +import superjson from "superjson"; +import type { TRPCPanelMeta } from "trpc-ui"; +import { ZodError } from "zod"; + +import { env } from "~/env.mjs"; +import { appRouterSuperjson } from "~/router-superjson"; +import { createTRPCContext } from "~/server/api/trpc"; + +// Create a separate tRPC instance with superjson transformer +const tSuperjson = initTRPC + .context() + .meta() + .create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, + allowOutsideOfServer: true, + }); + +// Create middleware with logging +const loggingMiddleware = tSuperjson.middleware( + async ({ next, path, type, getRawInput }) => { + const rawInput = await getRawInput(); + console.log(`= + [SUPERJSON ${type.toUpperCase()}] ${path}`); + console.log("Raw Input:", rawInput); + console.log("Input type:", typeof rawInput); + console.log("Input JSON:", JSON.stringify(rawInput, null, 2)); + return next(); + }, +); + +// Create procedure with superjson-enabled tRPC instance +const procedureSuperjson = tSuperjson.procedure.use(loggingMiddleware); + +// Export API handler +export default createNextApiHandler({ + router: appRouterSuperjson, + createContext: createTRPCContext, + onError: + env.NODE_ENV === "development" + ? ({ path, error }) => { + console.error( + `L tRPC (superjson) failed on ${path ?? ""}: ${error.message}`, + ); + } + : undefined, +}); diff --git a/packages/dev-app/src/pages/index.tsx b/packages/dev-app/src/pages/index.tsx index bf58620..ce177f3 100644 --- a/packages/dev-app/src/pages/index.tsx +++ b/packages/dev-app/src/pages/index.tsx @@ -1,30 +1,31 @@ import dynamic from "next/dynamic"; -import { parseRouterWithOptions } from "trpc-ui/parse/parseRouter"; +// import { parseRouterWithOptions } from "trpc-ui/parse/parseRouter"; +import { parseTRPCRouter } from "trpc-ui/parseV2/parse"; import { RootComponent } from "trpc-ui/react-app/Root"; import { trpc } from "trpc-ui/react-app/trpc"; import { env } from "~/env.mjs"; import { appRouter } from "~/router"; console.log(`Using superjson: ${env.NEXT_PUBLIC_SUPERJSON}`); -const parse = parseRouterWithOptions(appRouter, { - transformer: env.NEXT_PUBLIC_SUPERJSON === "false" ? undefined : "superjson", -}); +// const parse = parseRouterWithOptions(appRouter, { +// transformer: env.NEXT_PUBLIC_SUPERJSON === "false" ? undefined : "superjson", +// }); + +const parseV2 = parseTRPCRouter(appRouter); const App = dynamic( Promise.resolve(() => ( )), { ssr: false }, diff --git a/packages/dev-app/src/pages/superjson.tsx b/packages/dev-app/src/pages/superjson.tsx new file mode 100644 index 0000000..2515296 --- /dev/null +++ b/packages/dev-app/src/pages/superjson.tsx @@ -0,0 +1,32 @@ +import dynamic from "next/dynamic"; +import { parseTRPCRouter } from "trpc-ui/parseV2/parse"; +import { RootComponent } from "trpc-ui/react-app/Root"; +import { env } from "~/env.mjs"; +import { appRouterSuperjson } from "~/router-superjson"; + +console.log("Using superjson: true"); + +const parseV2 = parseTRPCRouter(appRouterSuperjson); + +const App = dynamic( + Promise.resolve(() => ( + + )), + { ssr: false }, +); + +const Component = () => { + return ; +}; + +export default Component; diff --git a/packages/dev-app/src/router-superjson.ts b/packages/dev-app/src/router-superjson.ts new file mode 100644 index 0000000..874e5ce --- /dev/null +++ b/packages/dev-app/src/router-superjson.ts @@ -0,0 +1,283 @@ +import { TRPCError } from "@trpc/server"; +import { initTRPC } from "@trpc/server"; +import { type } from "arktype"; +import superjson from "superjson"; +import type { TRPCPanelMeta } from "trpc-ui"; +import * as v from "valibot"; +import { ZodError } from "zod"; +import * as z from "zod/v3"; +import * as z4 from "zod/v4"; +import { createTRPCContext } from "~/server/api/trpc"; + +// Create a separate tRPC instance with superjson transformer +const tSuperjson = initTRPC + .context() + .meta() + .create({ + transformer: superjson, + errorFormatter({ shape, error }) { + return { + ...shape, + data: { + ...shape.data, + zodError: + error.cause instanceof ZodError ? error.cause.flatten() : null, + }, + }; + }, + allowOutsideOfServer: true, + }); + +const loggingMiddleware = tSuperjson.middleware( + async ({ next, path, type, getRawInput }) => { + const rawInput = await getRawInput(); + console.log(`🔍 [SUPERJSON ${type.toUpperCase()}] ${path}`); + console.log("Raw Input:", rawInput); + console.log("Input type:", typeof rawInput); + console.log("Input JSON:", JSON.stringify(rawInput, null, 2)); + return next(); + }, +); + +export const createTRPCRouterSuperjson = tSuperjson.router; +export const procedureSuperjson = tSuperjson.procedure.use(loggingMiddleware); + +const secondValidator = procedureSuperjson + .input( + z.object({ + needString: z.string(), + }), + ) + .use(({ ctx, next }) => { + return next({ ctx }); + }); + +const arktypeVal = procedureSuperjson + .input( + type({ + name: "string", + }), + ) + .use(({ ctx, next }) => { + return next({ ctx }); + }); + +const deepRouterSuperjson = createTRPCRouterSuperjson({ + coolQueryWithDate: procedureSuperjson + .input( + z.object({ + needString: z.string(), + createdAt: z.date(), + }), + ) + .query(({ input }) => ({ + data: "thing with date", + date: input.createdAt, + })), +}); + +// {"0":{"json":{"memo":"dd","transactionId":"txn_01K2B49GD0HE7FZFY33TBEZC3E"}}} + +// valid query +// http://localhost:3000/trpc/transaction.budgetOptions?batch=1&input=%7B%220%22%3A%7B%22json%22%3A%7B%22transactionId%22%3A%22txn_01K23XPNG0J376K8N7FYE0PNHT%22%7D%7D%7D + +const validResult = { + result: { + data: { + json: { + addressLine1: "223 West Wilkinson Street", + addressLine2: null, + city: "Kill Devil Hills", + county: null, + id: "add_01K0W56VT9EMQV4BAWTJ6E2KZJ", + isBilling: true, + isDefault: false, + name: "Billing Address", + organizationId: "org_01K0W548C5G5ANPVYH9A0FFQ91", + state: "NC", + zipcode: "27948", + }, + }, + }, +}; + +const postsRouterSuperjson = createTRPCRouterSuperjson({ + nothing: procedureSuperjson.input(z.any()).query(({ input }) => { + return { + testSet: new Set(["asd"]), + test: Math.random(), + }; + }), + superMutation: procedureSuperjson + .input( + type({ + test: "string", + value: "string", + }), + ) + .mutation(({ input }) => { + return { + ...input, + }; + }), + superQuery: procedureSuperjson + .input( + type({ + test: "string", + value: "string", + }), + ) + .query(({ input }) => { + return { + theTest: input.test, + theValue: input.value, + }; + }), + complexSuperJson: procedureSuperjson + .input( + z.object({ + id: z.bigint(), + name: z.string(), + createdAt: z.date(), + tags: z.set(z.string()), + metadata: z.map(z.string(), z.string()), + }), + ) + .query(({ input }) => { + return { + message: "You used superjson!", + input: input, + processedAt: new Date(), + }; + }), + + dateTest: procedureSuperjson + .input( + z.object({ + date: z.date(), + nested: z.object({ + text: z.string(), + }), + }), + ) + .mutation(({ input }) => { + console.log(input); + return { + id: "aoisdjfoasidjfasodf", + time: input.date.getTime(), + originalDate: input.date, + }; + }), + + createPostZodThreeSuperjson: secondValidator + .meta({ + description: + "Zod v3 procedure with superjson types and merged validators", + }) + .input( + z.object({ + text: z.string().min(1).describe("hi there").optional(), + createdAt: z.date().describe("Creation date"), + tags: z.set(z.string()).describe("Post tags"), + metadata: z.map(z.string(), z.string()).describe("Additional metadata"), + }), + ) + .mutation(({ input }) => { + return { + ...input, + processedAt: new Date(), + }; + }), + + createPostZodFourSuperjson: procedureSuperjson + .input( + z4.object({ + title: z4.string().min(1).describe("Post title"), + content: z4.string().describe("Post content"), + publishedAt: z4.coerce.date().describe("Publication date"), + categories: z4.set(z4.string()).describe("Post categories"), + }), + ) + .mutation(({ input }) => { + return { + id: "generated-id", + ...input, + createdAt: new Date(), + }; + }), + + // createPostArkTypeSuperjson: arktypeVal + // .input( + // type({ + // name: "string", + // age: "string", + // birthDate: "Date", + // }), + // ) + // .query(({ input }) => { + // return { + // message: "ArkType validation with Date successful", + // data: input, + // processedAt: new Date(), + // }; + // }), + + // createPostValibotSuperjson: procedureSuperjson + // .input( + // v.object({ + // email: v.pipe(v.string(), v.email()), + // username: v.pipe(v.string(), v.minLength(3)), + // registeredAt: v.date(), + // preferences: v.map(v.string(), v.string()), + // }), + // ) + // .mutation(({ input }) => { + // return { + // success: true, + // user: input, + // processedAt: new Date(), + // }; + // }), + + // mergedZod3SuperjsonProcedure: secondValidator + // .input( + // z.object({ + // additionalField: z + // .string() + // .describe("Additional field for merged validation"), + // timestamps: z.set(z.date()).describe("Set of timestamps"), + // }), + // ) + // .query(({ input }) => { + // return { + // message: "Merged Zod v3 validation with superjson", + // data: input, + // processedAt: new Date(), + // }; + // }), + + // mergedArktypeSuperjsonProcedure: arktypeVal + // .input( + // type({ + // category: "string", + // priority: "'low' | 'medium' | 'high'", + // dueDate: "Date", + // // labels: "Set", + // }), + // ) + // .mutation(({ input }) => { + // return { + // message: "Merged ArkType validation with superjson", + // result: input, + // processedAt: new Date(), + // }; + // }), + + // deep: deepRouterSuperjson, +}); + +export const appRouterSuperjson = createTRPCRouterSuperjson({ + postsRouterSuperjson, +}); + +export type AppRouterSuperjson = typeof appRouterSuperjson; diff --git a/packages/dev-app/src/router.ts b/packages/dev-app/src/router.ts index eacf675..d778377 100644 --- a/packages/dev-app/src/router.ts +++ b/packages/dev-app/src/router.ts @@ -1,29 +1,71 @@ import { TRPCError } from "@trpc/server"; -import { z } from "zod"; +import { type } from "arktype"; +import * as v from "valibot"; +import * as z3 from "zod/v3"; +import * as z4 from "zod/v4"; import { createTRPCRouter, procedure } from "~/server/api/trpc"; -const postsRouter = createTRPCRouter({ - complexSuperJson: procedure +const zod3Middleware = procedure + .input( + z3.object({ + needString: z3.string(), + }), + ) + .use(({ ctx, next }) => { + return next({ ctx }); + }); + +const zod4Middleware = procedure + .input( + z4.object({ + needString: z4.string(), + }), + ) + .use(({ ctx, next }) => { + return next({ ctx }); + }); + +const arktypeVal = procedure + .input( + type({ + name: "string", + }), + ) + .use(({ ctx, next }) => { + return next({ ctx }); + }); + +const valibotMiddleware = procedure + .input(v.object({ middlewareProp: v.string() })) + .use(({ ctx, next }) => { + return next({ ctx }); + }); + +const deepRouter = createTRPCRouter({ + coolQuery: procedure .input( - z.object({ - id: z.bigint(), - name: z.string(), - createdAt: z.date(), - tags: z.set(z.string()), - metadata: z.map(z.string(), z.string()), + z3.object({ + needString: z3.string(), }), ) - .query(({ input }) => { - return { - message: "You used superjson!", - input: input, - }; - }), - meta: procedure - .meta({ - description: "This is a router that contains posts", - }) - .query(() => null), + .query(({ input }) => ({ + data: "thing", + })), +}); + +const anotherRouter = createTRPCRouter({ + coolQuery2: procedure + .input( + z3.object({ + hi: z3.number(), + }), + ) + .query(() => ({ + data: "thing", + })), +}); + +const postsRouter = createTRPCRouter({ getAllPosts: procedure .meta({ description: "Simple procedure that returns a list of posts", @@ -35,22 +77,31 @@ const postsRouter = createTRPCRouter({ text: "Post Id", }, { - id: "asodifjaosdf", - text: "Post Id", + id: "asodifjaosdf2", + text: "Post Id 2", }, { - id: "asodifjaosdf", - text: "Post Id", + id: "asodifjaosdf3", + text: "Post Id 3", }, ]; }), - createPost: procedure + createPostZodThree: zod3Middleware + .meta({ + description: "Zod v3 procedure with merged input validators", + }) .input( - z.object({ - text: z.string().min(1), - nested: z.object({ - nestedText: z.string(), - }), + z3.object({ + text: z3.string().min(1).describe("hi there").optional(), + nested: z3 + .object({ + nestedText: z3.string().describe("what's happening").optional(), + nestedAgain: z3.object({ + nest: z3.boolean().describe("cool bool"), + }), + }) + .describe("object descriptions"), + optionalProp: z3.string().optional(), }), ) .mutation(({ input }) => { @@ -58,237 +109,106 @@ const postsRouter = createTRPCRouter({ ...input, }; }), - dateTest: procedure + createPostZodFour: procedure .input( - z.object({ - date: z.date(), - nested: z.object({ - text: z.string(), - }), + z4.object({ + title: z4.string().min(1).describe("Post title"), + content: z4.string().describe("Post content"), }), ) .mutation(({ input }) => { - console.log(input); return { - id: "aoisdjfoasidjfasodf", - time: input.date.getTime(), + id: "generated-id", + ...input, + createdAt: new Date().toISOString(), }; }), - createNestedPost: procedure - .input( - z.object({ - text: z.string(), - }), - ) + mergedZodFour: zod4Middleware .input( - z.object({ - title: z.string(), + z4.object({ + testNum: z4.number(), }), ) - .mutation(({ input }) => { + .query(({ input }) => { return { - id: "aoisdjfoasidjfasodf", - text: input.text, + called: new Date().toString(), + ...input, }; }), -}); -const discriminatedFieldEnum = z.enum(["One", "Two"]); - -export const appRouter = createTRPCRouter({ - postsRouter, - inputShowcaseRouter: createTRPCRouter({ - textInput: procedure - .input(z.object({ aTextInput: z.string() })) - .query(() => { - return "It's an input"; - }), - numberInput: procedure - .input(z.object({ aNumberInput: z.number() })) - .query(() => { - return "It's an input"; - }), - enumInput: procedure - .input(z.object({ aEnumInput: z.enum(["One", "Two"]) })) - .query(() => { - return "It's an input"; - }), - nativeEnumInput: procedure - .input(z.object({ aEnumInput: z.nativeEnum({ ONE: "one", TWO: "two" }) })) - .query(() => { - return "It's an input"; - }), - stringArrayInput: procedure - .input(z.object({ aStringArray: z.string().array() })) - .query(() => { - return "It's an input"; - }), - objectInput: procedure - .input( - z.object({ - anObject: z.object({ - numberArray: z.number().array(), - }), - }), - ) - .query(() => { - return "It's an input"; - }), - discriminatedUnionInput: procedure - .input( - z.object({ - aDiscriminatedUnion: z.discriminatedUnion("discriminatedField", [ - z.object({ - discriminatedField: z.literal("One"), - aFieldThatOnlyShowsWhenValueIsOne: z.string(), - }), - z.object({ - discriminatedField: z.literal("Two"), - aFieldThatOnlyShowsWhenValueIsTwo: z.object({ - someTextFieldInAnObject: z.string(), - }), - }), - z.object({ - discriminatedField: z.literal("Three"), - }), - ]), - }), - ) - .query(() => { - return "It's an input"; - }), - unionInput: procedure - .input( - z.object({ - aUnion: z.union([z.literal("one"), z.literal(2)]), - }), - ) - .query(({ input }) => { - return input; - }), - emailTextInput: procedure - .input( - z.object({ - email: z.string().email("That's an invalid email (custom message)"), - }), - ) - .query(() => { - return "It's good"; - }), - voidInput: procedure.input(z.void()).query(() => { - return "yep"; - }), - }), - postSomething: procedure + basicArktype: procedure .input( - z.object({ - title: z.string(), - content: z.string(), + type({ + test: "string", }), ) - .mutation(({ input: { title, content } }) => { - return { - title, - content, - }; + .query(({ input }) => { + return input; }), - discriminatedUnionInput: procedure + createPostArkType: arktypeVal .input( - z.object({ - aDiscriminatedUnion: z.discriminatedUnion("discriminatedField", [ - z.object({ - discriminatedField: discriminatedFieldEnum.extract(["One"]), // <-- this doesn't work - aFieldThatOnlyShowsWhenValueIsOne: z.string(), - }), - z.object({ - discriminatedField: z.literal("Two"), - aFieldThatOnlyShowsWhenValueIsTwo: z.object({ - someTextFieldInAnObject: z.string(), - }), - }), - ]), + type({ + test: "string", + test3: "number > 5", }), ) .query(({ input }) => { - return input; + return { + message: "ArkType validation successful", + data: input, + }; }), - procedureWithDescription: procedure - .meta({ - description: - "# This is a description\n\nIt's a **good** one.\nIt may be overkill in certain situations, but procedures descriptions can render markdown thanks to [react-markdown](https://github.com/remarkjs/react-markdown) and [tailwindcss-typography](https://github.com/tailwindlabs/tailwindcss-typography)\n1. Lists\n2. Are\n3. Supported\n but I *personally* think that [links](https://github.com/aidansunbury/trpc-ui) and images ![Image example](https://private-user-images.githubusercontent.com/64103161/384591987-7dc0e751-d493-4337-ac8d-a1f16924bf48.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzExNDM3OTMsIm5iZiI6MTczMTE0MzQ5MywicGF0aCI6Ii82NDEwMzE2MS8zODQ1OTE5ODctN2RjMGU3NTEtZDQ5My00MzM3LWFjOGQtYTFmMTY5MjRiZjQ4LnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDExMDklMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQxMTA5VDA5MTEzM1omWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTE4YmM4OTlkZmYyNmJjOWI5YzgwZDUxOTVlYTBjODlkMTVkMzNlNmJjZDhkZDJiNTRhNzFmNDZhMzllNDc2ZGYmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.FsvDvXo6S7n4uOsi3LMUUOeEhjXq6LF88MlU60gzZ2k)\n are the most useful for documentation purposes", - }) + createPostValibot: procedure + .input(v.object({ name: v.string() })) + .mutation(({ input }) => { + return { + success: true, + user: input, + }; + }), + mergedZod3Procedure: zod3Middleware .input( - z.object({ - id: z.string().describe("The id of the thing."), - searchTerm: z + z3.object({ + additionalField: z3 .string() - .optional() - .describe( - "Even term descriptions *can* render basic markdown, but don't get too fancy", - ), - searchTerm2: z - .string() - .optional() - .describe( - "The name of the thing to search for. Really really long long long boi long boi long Really really long long long boi long boi long Really really long long long boi long boi long Really really long long long boi long boi long", - ), + .describe("Additional field for merged validation"), }), ) - .query(() => { - return "Was that described well enough?"; + .query(({ input }) => { + return { + message: "Merged Zod v3 validation", + data: input, + }; }), - nonObjectInput: procedure.input(z.string()).query(({ input }) => { - return `Hello ${input}`; - }), - slowProcedure: procedure + + //! This breaks, but I think it is an issue with json schema features maybe not being supported? + mergedArktypeProcedure: arktypeVal .input( - z.object({ - name: z.string(), + type({ + category: "string", + priority: "'low' | 'medium' | 'high'", }), ) - .query(async ({ input }) => { - // two second delay - await new Promise((resolve) => setTimeout(resolve, 2000)); - return `Hello ${input.name}`; + .mutation(({ input }) => { + return { + message: "Merged ArkType validation", + result: input, + }; }), - anErrorThrowingRoute: procedure - .input( - z.object({ - ok: z.string(), - }), - ) - .query(() => { - throw new TRPCError({ - message: "It broke.", - code: "FORBIDDEN", - }); + mergedValibot: valibotMiddleware + .input(v.object({ name: v.string() })) + .mutation(({ input }) => { + return { + success: true, + user: input, + }; }), - allInputs: procedure - .input( - z.object({ - obj: z.object({ - string: z.string().optional(), - }), - stringMin5: z.string().min(5), - numberMin10: z.number().min(10), - stringOptional: z.string().optional(), - enum: z.enum(["One", "Two"]), - optionalEnum: z.enum(["Three", "Four"]).optional(), - stringArray: z.string().array(), - boolean: z.boolean(), - discriminatedUnion: z.discriminatedUnion("disc", [ - z.object({ - disc: z.literal("one"), - oneProp: z.string(), - }), - z.object({ - disc: z.literal("two"), - twoProp: z.enum(["one", "two"]), - }), - ]), - union: z.union([z.literal("one"), z.literal(2)]), - }), - ) - .query(() => ({ goodJob: "yougotthedata" })), + + deep: deepRouter, +}); + +export const appRouter = createTRPCRouter({ + postsRouter, + anotherRouter, }); // export type definition of API diff --git a/packages/dev-app/src/server/api/trpc.ts b/packages/dev-app/src/server/api/trpc.ts index 32a462b..e4fd091 100644 --- a/packages/dev-app/src/server/api/trpc.ts +++ b/packages/dev-app/src/server/api/trpc.ts @@ -1,12 +1,3 @@ -/** - * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: - * 1. You want to modify request context (see Part 1). - * 2. You want to create a new middleware or type of procedure (see Part 3). - * - * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will - * need to use are documented accordingly near the end. - */ - /** * 1. CONTEXT * @@ -60,7 +51,7 @@ const t = initTRPC .context() .meta() .create({ - transformer: env.NEXT_PUBLIC_SUPERJSON === "false" ? undefined : superjson, + transformer: undefined, errorFormatter({ shape, error }) { return { ...shape, @@ -95,4 +86,17 @@ export const createTRPCRouter = t.router; * guarantee that a user querying is authorized, but you can still access user session data if they * are logged in. */ -export const procedure = t.procedure; + +// In your tRPC setup file +const loggingMiddleware = t.middleware( + async ({ next, path, type, getRawInput }) => { + const rawInput = await getRawInput(); + console.log(`🔍 [${type.toUpperCase()}] ${path}`); + console.log("Raw Input:", rawInput); + console.log("Input type:", typeof rawInput); + console.log("Input JSON:", JSON.stringify(rawInput, null, 2)); + return next(); + }, +); + +export const procedure = t.procedure.use(loggingMiddleware); diff --git a/packages/dev-app/src/test.ts b/packages/dev-app/src/test.ts new file mode 100644 index 0000000..a64ce5c --- /dev/null +++ b/packages/dev-app/src/test.ts @@ -0,0 +1,342 @@ +import { toJsonSchema } from "@valibot/to-json-schema"; +import { type } from "arktype"; +import type { JSONSchema7Type } from "json-schema"; +import * as v from "valibot"; +import { z } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import { appRouter } from "./router"; + +const index = 0; + +appRouter.postsRouter.createPost; + +function parseNode(node: any) { + return node._def; +} + +const base = type({}); + +const array = [ + type({ + text1: "string >=1", + }), + type({ + text2: "string >=1", + }), +]; + +array.reduce((acc, curr) => { + return acc.and(curr); +}); + +const ark = type({ + text: "string >=1", +}).and( + type({ + text1: "string", + }), +); + +const vali = v.object({ + text: v.pipe(v.string(), v.minLength(1)), + nested: v.object({ + nestedText: v.string(), + }), +}); + +const schemaOne = z.object({ + prop1: z.string(), +}); + +const schemaTwo = z.object({ + prop2: z.number(), +}); + +const combined = schemaOne.merge(schemaTwo); + +const res = zodToJsonSchema(combined); + +const expected = z.object({ + text: z.string().min(1), + nested: z.object({ + nestedText: z.string(), + }), +}); + +expected["~standard"].vendor; + +// const jsonSchema = zodToJsonSchema( +// appRouter.postsRouter.createPost._def.inputs[0] +// ); + +// console.dir(appRouter.postsRouter.createPost._def.inputs.length); + +// console.dir(jsonSchema, { +// depth: null, +// }); + +// console.dir(ark.toJsonSchema(), { +// depth: null, +// }); + +// console.dir(zodToJsonSchema(expected), { +// depth: null, +// }); + +// console.dir(toJsonSchema(vali)); + +// console.dir(appRouter._def, { +// depth: null, +// }); + +// console.dir(appRouter.postsRouter.createPost._def, { +// depth: null, +// }); + +// console.dir(appRouter.postsRouter.deep.coolQuery._def, { +// depth: null, +// }); + +function detectValidatorType( + validator: any, +): "zod" | "valibot" | "arktype" | "unknown" { + // Handle null or undefined + if (validator == null) { + return "unknown"; + } + + // First attempt: Check for standard schema vendor property + try { + if (validator["~standard"]?.vendor) { + const vendor = validator["~standard"].vendor.toLowerCase(); + if (vendor.includes("zod")) return "zod"; + if (vendor.includes("valibot")) return "valibot"; + if (vendor.includes("arktype")) return "arktype"; + } + } catch (e) { + // Ignore errors when accessing properties + } + console.log("unable to determine based on standard schema"); + + // Second attempt: Use heuristics based on library-specific properties + + // Check for Zod + // Zod schemas have specific properties like _def, safeParse, parse, and ZodType + + if ( + validator._def !== undefined && + typeof validator.safeParse === "function" && + typeof validator.parse === "function" && + validator instanceof Object.getPrototypeOf(validator).constructor && + (Object.getPrototypeOf(validator).constructor.name.includes("Zod") || + validator.constructor.name.includes("Zod")) + ) { + return "zod"; + } + + // Check for Valibot + // Valibot validators have specific structure with _type, _schema, and _parse properties + if ( + validator._type !== undefined && + (validator._schema !== undefined || validator._expected !== undefined) && + typeof validator._parse === "function" + ) { + return "valibot"; + } + + // Check for Arktype + // Arktype types have specific properties like infer, type, as, and schema + if ( + typeof validator.infer === "function" && + validator.type !== undefined && + typeof validator.as === "function" && + validator.schema !== undefined + ) { + return "arktype"; + } + + // Unknown validator type + return "unknown"; +} + +/** + * Type representing the validator types supported by the parser + */ +export type ValidatorType = "zod" | "valibot" | "arktype" | "unknown" | "mixed"; + +/** + * Base type for common properties shared by routers and procedures + */ +export type BaseNodeType = { + path: string[]; +}; + +/** + * Type representing metadata that can be attached to a procedure + */ +export type ProcedureMeta = Record; + +/** + * Type representing any procedure (query or mutation) + */ +export type Procedure = BaseNodeType & { + type: "mutation" | "query"; + meta: ProcedureMeta; + validator: ValidatorType; + schema?: JSONSchema7Type; +}; + +/** + * Type for a tRPC router + */ +export type Router = BaseNodeType & { + type: "router"; + children: RouterStructure; +}; + +/** + * Type representing the entire router structure + * A dictionary where keys are router/procedure names and values are the corresponding structures + */ +export type RouterStructure = Record; + +/** + * Type representing the output of the parseTRPCRouter function + */ +export type ParsedTRPCRouter = RouterStructure; + +/** + * Recursively parses a tRPC router structure and its sub-routers + * + * @param router - The router or procedure to parse + * @param currentPath - The current path in the router hierarchy + * @param detectValidatorFn - Function to detect the type of validator + * @param zodToJsonSchemaFn - Function to convert Zod schema to JSON Schema (optional) + * @returns A structured representation of the router hierarchy + */ +function parseTRPCRouter( + router: any, + currentPath: string[] = [], + detectValidatorFn: ( + validator: any, + ) => "zod" | "valibot" | "arktype" | "unknown" | "mixed" = () => "unknown", +): ParsedTRPCRouter { + // The result object we'll build up + const result: Record = {}; + + // Iterate over each key in the router + for (const key in router) { + const item = router[key]; + + // Skip all internal properties (starting with _) + if (key.startsWith("_")) { + continue; + } + + // Create the path for this node + const nodePath = [...currentPath, key]; + + // Check if it's a procedure (query or mutation) + if (item?._def?.type) { + const meta = item._def.meta || {}; + + // Determine validator type + let validatorType: "zod" | "valibot" | "arktype" | "unknown" | "mixed" = + "unknown"; + let jsonSchema: any = undefined; + + // Check if inputs array exists and has elements + if ( + item._def.inputs && + Array.isArray(item._def.inputs) && + item._def.inputs.length > 0 + ) { + // Get validator type of first input + const firstType = detectValidatorFn(item._def.inputs[0]); + + // Check if all inputs are of the same type + const allSameType = item._def.inputs.every( + (input) => detectValidatorFn(input) === firstType, + ); + + validatorType = allSameType ? firstType : "mixed"; + + // Generate JSON Schema for Zod validators + if (validatorType === "zod") { + try { + // Merge all Zod schemas + let mergedSchema = item._def.inputs[0]; + + for (let i = 1; i < item._def.inputs.length; i++) { + if (typeof mergedSchema.merge === "function") { + mergedSchema = mergedSchema.merge(item._def.inputs[i]); + } + } + + // Convert merged schema to JSON Schema + jsonSchema = zodToJsonSchema(mergedSchema); + } catch (error) { + // If merging or conversion fails, leave jsonSchema as undefined + console.error("Error generating JSON Schema:", error); + } + } else if (validatorType === "valibot") { + try { + const merged = v.intersect(item._def.inputs); + jsonSchema = toJsonSchema(merged); + } catch (error) { + // If merging or conversion fails, leave jsonSchema as undefined + console.error("Error generating JSON Schema:", error); + } + } else if (validatorType === "arktype") { + const merged = item._def.inputs.reduce((merge, curr) => { + merge.and(curr); + }); + jsonSchema = merged.toJsonSchema(); + } + } + + if (item._def.type === "query") { + result[key] = { + type: "query", + path: nodePath, + meta, + validator: validatorType, + schema: jsonSchema, + }; + } else if (item._def.type === "mutation") { + result[key] = { + type: "mutation", + path: nodePath, + meta, + validator: validatorType, + schema: jsonSchema, + }; + } + } + // Check if it's a router (contains other procedures or routers) + else if (item && typeof item === "object" && !Array.isArray(item)) { + // Recursively parse potential router + const children = parseTRPCRouter(item, nodePath, detectValidatorFn); + + // Only add it as a router if it has children + if (Object.keys(children).length > 0) { + result[key] = { + type: "router", + path: nodePath, + children, + }; + } + } + } + + return result; +} + +console.dir( + JSON.stringify(parseTRPCRouter(appRouter, [], detectValidatorType)), + { + depth: null, + }, +); + +// console.log(detectValidatorType(ark)); diff --git a/packages/dev-app/src/utils/api.ts b/packages/dev-app/src/utils/api.ts index 27cea06..c4b1caa 100644 --- a/packages/dev-app/src/utils/api.ts +++ b/packages/dev-app/src/utils/api.ts @@ -4,7 +4,7 @@ * * We also create a few inference helpers for input and output types. */ -import { httpBatchLink, loggerLink } from "@trpc/client"; +import { httpBatchLink, httpLink, loggerLink } from "@trpc/client"; import { createTRPCNext } from "@trpc/next"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import superjson from "superjson"; @@ -24,7 +24,7 @@ export const api = createTRPCNext({ * * @see https://trpc.io/docs/data-transformers */ - transformer: superjson, + // transformer: superjson, config() { return { /** @@ -38,15 +38,19 @@ export const api = createTRPCNext({ process.env.NODE_ENV === "development" || (opts.direction === "down" && opts.result instanceof Error), }), - httpBatchLink({ + httpLink({ url: `${getBaseUrl()}/api/trpc`, - /** - * Transformer used for data de-serialization from the server. - * - * @see https://trpc.io/docs/data-transformers - */ - transformer: superjson, + // transformer, }), + // httpBatchLink({ + // url: `${getBaseUrl()}/api/trpc`, + // /** + // * Transformer used for data de-serialization from the server. + // * + // * @see https://trpc.io/docs/data-transformers + // */ + // // transformer: superjson, + // }), ], }; }, diff --git a/packages/trpc-ui/package.json b/packages/trpc-ui/package.json index 506c11d..098b22b 100644 --- a/packages/trpc-ui/package.json +++ b/packages/trpc-ui/package.json @@ -33,16 +33,20 @@ }, "peerDependencies": { "@trpc/server": "^11.0.0-next-beta.264", - "zod": "^3.19.1" + "@valibot/to-json-schema": "^1.0.0", + "arktype": "^2.0.0", + "valibot": "^1.0.0", + "zod": "^3.25.0", + "zod-to-json-schema": "^3.20.0" }, "devDependencies": { "@babel/core": "^7.20.2", "@babel/preset-react": "^7.18.6", - "@emotion/react": "^11.10.5", - "@emotion/styled": "^11.10.5", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", "@hookform/resolvers": "^2.9.10", "@mui/icons-material": "^5.10.16", - "@mui/material": "^5.10.16", + "@mui/material": "^6.4.8", "@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-json": "^5.0.1", @@ -93,19 +97,27 @@ "zustand": "^4.1.5" }, "dependencies": { + "@jsonforms/material-renderers": "^3.5.1", + "@jsonforms/react": "^3.5.1", "@monaco-editor/react": "^4.7.0", + "@rjsf/core": "^5.24.8", + "@rjsf/material-ui": "^5.24.8", + "@rjsf/utils": "^5.24.8", + "@rjsf/validator-ajv8": "^5.24.8", "@stoplight/json-schema-sampler": "^0.3.0", - "@textea/json-viewer": "^3.0.0", + "@textea/json-viewer": "^4.0.0", + "@types/json-schema": "^7.0.15", + "add": "^2.0.6", "clsx": "^2.1.1", "fuzzysort": "^2.0.4", "nuqs": "^2.2.1", "path": "^0.12.7", + "pnpm": "^10.6.4", "pretty-bytes": "^6.1.0", "pretty-ms": "^8.0.0", "react-markdown": "^9.0.1", "string-byte-length": "^1.6.0", "tailwind-merge": "^2.5.5", - "url": "^0.11.0", - "zod-to-json-schema": "^3.20.0" + "url": "^0.11.0" } } diff --git a/packages/trpc-ui/rollup.config.js b/packages/trpc-ui/rollup.config.js index 53b336a..ed5428f 100644 --- a/packages/trpc-ui/rollup.config.js +++ b/packages/trpc-ui/rollup.config.js @@ -16,12 +16,10 @@ export default [ plugins: [ typescript({ tsconfig: "tsconfig.buildPanel.json" }), json(), - // resolve(), - // babel({ - // exclude: "node_modules/**", - // presets: ["@babel/env", "@babel/preset-react"], - // }), - // commonjs(), + nodeResolve({ + extensions: [".js", ".ts", ".tsx", "ts"], + }), + commonjs(), ], output: [ { file: "lib/index.js", format: "cjs", inlineDynamicImports: true }, @@ -41,6 +39,7 @@ export default [ postcss({ extract: path.resolve("lib/react-app/index.css"), }), + json(), nodeResolve({ extensions: [".js", ".ts", ".tsx", "ts"], }), diff --git a/packages/trpc-ui/src/index.ts b/packages/trpc-ui/src/index.ts index dc9c052..c0bbc6d 100644 --- a/packages/trpc-ui/src/index.ts +++ b/packages/trpc-ui/src/index.ts @@ -1,3 +1,3 @@ export { renderTrpcPanel } from "./render"; export type { TRPCPanelMeta } from "./meta"; -export { parseRouterWithOptions } from "./parse/parseRouter"; +export { parseTRPCRouter } from "./parseV2/parse"; diff --git a/packages/trpc-ui/src/parse/__tests__/parseProcedure.test.ts b/packages/trpc-ui/src/parse/__tests__/parseProcedure.test.ts deleted file mode 100644 index 529b2bf..0000000 --- a/packages/trpc-ui/src/parse/__tests__/parseProcedure.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { - parseTestRouter, - parseTestRouterInputSchema, - testMutationExpectedParseResult, - testQueryExpectedParseResult, - testTrpcInstance, -} from "@src/parse/__tests__/utils/router"; -import { testSchemas } from "@src/parse/__tests__/utils/schemas"; -import { - type ParsedProcedure, - parseProcedure, -} from "@src/parse/parseProcedure"; -import type { Procedure } from "@src/parse/routerType"; -import { z } from "zod"; -import zodToJsonSchema from "zod-to-json-schema"; - -describe("Parse TRPC Procedure", () => { - it("should parse the test query", () => { - // IDK how to type this - const testQuery = parseTestRouter.testQuery as unknown as Procedure; - const parsedProcedure = parseProcedure(testQuery, ["testQuery"], {}); - expect(testQueryExpectedParseResult).toStrictEqual(parsedProcedure); - }); - it("should parse the test mutation", () => { - // IDK how to type this - const testQuery = parseTestRouter.testMutation as unknown as Procedure; - const parsedProcedure = parseProcedure(testQuery, ["testMutation"], {}); - expect(testMutationExpectedParseResult).toStrictEqual(parsedProcedure); - }); - it("should parse the meta description if it exists", () => { - const description = "It's a good description"; - const { testQuery } = testTrpcInstance.router({ - testQuery: testTrpcInstance.procedure - .meta({ description }) - .input(parseTestRouterInputSchema) - .query(() => "nope"), - }); - const expected = { - ...testQueryExpectedParseResult, - extraData: { - ...testQueryExpectedParseResult.extraData, - description, - }, - }; - - const parsed = parseProcedure( - testQuery as unknown as Procedure, - ["testQuery"], - {}, - ); - expect(parsed).toStrictEqual(expected); - }); - it("should parse input descriptions if they exist for common types", () => { - // good luck understanding this - const description = "A description"; - const testSchemasWithDescriptions = testSchemas.map((e, i) => ({ - ...e, - schema: e.schema.describe(description + i), - })); - const inputSchema = z.object({ - ...Object.fromEntries( - testSchemasWithDescriptions.map((e, i) => [i, e.schema]), - ), - }); - const expected: ParsedProcedure = { - nodeType: "procedure", - inputSchema: zodToJsonSchema(inputSchema), - pathFromRootRouter: ["testQuery"], - procedureType: "query", - extraData: { - parameterDescriptions: { - ...Object.fromEntries( - testSchemasWithDescriptions.map((_, i) => [i, description + i]), - ), - }, - }, - node: { - type: "object", - path: [], - children: { - ...Object.fromEntries( - testSchemasWithDescriptions.map((e, i) => [i, e.parsed]), - ), - }, - }, - }; - const testRouter = testTrpcInstance.router({ - testQuery: testTrpcInstance.procedure - .input(inputSchema) - .query(() => "nothing"), - }); - const parsed = parseProcedure( - testRouter.testQuery as unknown as Procedure, - ["testQuery"], - {}, - ); - expect(parsed).toStrictEqual(expected); - }); - it("should parse descriptions from nested objects with the appropriate path", () => {}); -}); diff --git a/packages/trpc-ui/src/parse/__tests__/parseRouter.test.ts b/packages/trpc-ui/src/parse/__tests__/parseRouter.test.ts deleted file mode 100644 index 50bb896..0000000 --- a/packages/trpc-ui/src/parse/__tests__/parseRouter.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - expectedTestRouterInputParsedNode, - parseTestRouter, - parseTestRouterInputSchema, - testTrpcInstance, -} from "@src/parse/__tests__/utils/router"; -import { - type ParsedRouter, - parseRouterWithOptions, -} from "@src/parse/parseRouter"; -import { zodToJsonSchema } from "zod-to-json-schema"; - -describe("Parse TRPC Router", () => { - it("should parse the test router", () => { - const expected: ParsedRouter = { - path: [], - nodeType: "router", - children: { - testQuery: { - nodeType: "procedure", - node: expectedTestRouterInputParsedNode, - inputSchema: zodToJsonSchema(parseTestRouterInputSchema), - procedureType: "query", - pathFromRootRouter: ["testQuery"], - extraData: { - parameterDescriptions: {}, - }, - }, - testMutation: { - nodeType: "procedure", - node: expectedTestRouterInputParsedNode, - inputSchema: zodToJsonSchema(parseTestRouterInputSchema), - procedureType: "mutation", - pathFromRootRouter: ["testMutation"], - extraData: { - parameterDescriptions: {}, - }, - }, - }, - }; - const parsed = parseRouterWithOptions(parseTestRouter, {}); - expect(parsed).toStrictEqual(expected); - }); - - it("should parse a nested test router", () => { - const expected: ParsedRouter = { - path: [], - nodeType: "router", - children: { - nestedRouter: { - nodeType: "router", - path: ["nestedRouter"], - children: { - testQuery: { - nodeType: "procedure", - node: expectedTestRouterInputParsedNode, - inputSchema: zodToJsonSchema(parseTestRouterInputSchema), - procedureType: "query", - pathFromRootRouter: ["nestedRouter", "testQuery"], - extraData: { - parameterDescriptions: {}, - }, - }, - testMutation: { - nodeType: "procedure", - node: expectedTestRouterInputParsedNode, - inputSchema: zodToJsonSchema(parseTestRouterInputSchema), - procedureType: "mutation", - pathFromRootRouter: ["nestedRouter", "testMutation"], - extraData: { - parameterDescriptions: {}, - }, - }, - }, - }, - }, - }; - const parseTestNestedRouter = testTrpcInstance.router({ - nestedRouter: parseTestRouter, - }); - const parsed = parseRouterWithOptions(parseTestNestedRouter, {}); - - expect(expected).toStrictEqual(parsed); - }); -}); diff --git a/packages/trpc-ui/src/parse/__tests__/utils/router.ts b/packages/trpc-ui/src/parse/__tests__/utils/router.ts deleted file mode 100644 index 955d221..0000000 --- a/packages/trpc-ui/src/parse/__tests__/utils/router.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { TRPCPanelMeta } from "@src/meta"; -import type { ObjectNode } from "@src/parse/parseNodeTypes"; -import type { ParsedProcedure } from "@src/parse/parseProcedure"; -import { initTRPC } from "@trpc/server"; -import { z } from "zod"; -import { zodToJsonSchema } from "zod-to-json-schema"; - -export const testTrpcInstance = initTRPC.meta().create({}); - -export const parseTestRouterInputSchema = z.object({ - id: z.string(), - age: z.number(), - expectedAgeOfDeath: z.number().optional(), - object: z.object({ - nestedId: z.string(), - }), - du: z.discriminatedUnion("d", [ - z.object({ - d: z.literal("one"), - oneProps: z.string(), - }), - z.object({ - d: z.literal("two"), - }), - ]), -}); - -export const expectedTestRouterInputParsedNode: ObjectNode = { - type: "object", - path: [], - children: { - id: { - type: "string", - path: ["id"], - }, - age: { - type: "number", - path: ["age"], - }, - expectedAgeOfDeath: { - type: "number", - optional: true, - path: ["expectedAgeOfDeath"], - }, - object: { - type: "object", - path: ["object"], - children: { - nestedId: { - type: "string", - path: ["object", "nestedId"], - }, - }, - }, - du: { - type: "discriminated-union", - path: ["du"], - discriminatorName: "d", - discriminatedUnionValues: ["one", "two"], - discriminatedUnionChildrenMap: { - one: { - type: "object", - path: ["du"], - children: { - d: { - type: "literal", - value: "one", - path: ["du", "d"], - }, - oneProps: { - type: "string", - path: ["du", "oneProps"], - }, - }, - }, - two: { - type: "object", - path: ["du"], - children: { - d: { - type: "literal", - value: "two", - path: ["du", "d"], - }, - }, - }, - }, - }, - }, -}; - -export const testQueryExpectedParseResult: ParsedProcedure = { - nodeType: "procedure", - node: expectedTestRouterInputParsedNode, - inputSchema: zodToJsonSchema(parseTestRouterInputSchema), - procedureType: "query", - pathFromRootRouter: ["testQuery"], - extraData: { - parameterDescriptions: {}, - }, -}; - -export const testMutationExpectedParseResult: ParsedProcedure = { - nodeType: "procedure", - node: expectedTestRouterInputParsedNode, - inputSchema: zodToJsonSchema(parseTestRouterInputSchema), - procedureType: "mutation", - pathFromRootRouter: ["testMutation"], - extraData: { - parameterDescriptions: {}, - }, -}; - -export const testQuery = testTrpcInstance.procedure - .input(parseTestRouterInputSchema) - .query(() => "Nada"); - -export const testMutation = testTrpcInstance.procedure - .input(parseTestRouterInputSchema) - .mutation(() => "Nope"); - -export const parseTestRouter = testTrpcInstance.router({ - testQuery, - testMutation, -}); diff --git a/packages/trpc-ui/src/parse/__tests__/utils/schemas.ts b/packages/trpc-ui/src/parse/__tests__/utils/schemas.ts deleted file mode 100644 index 1d685ab..0000000 --- a/packages/trpc-ui/src/parse/__tests__/utils/schemas.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { ParsedInputNode } from "@src/parse/parseNodeTypes"; -import { type ZodSchema, z } from "zod"; - -export const testEnumValues = ["a", "b", "c"] as const; -export const testSchemas: { schema: ZodSchema; parsed: ParsedInputNode }[] = [ - { schema: z.string(), parsed: { type: "string", path: ["0"] } }, - { schema: z.number(), parsed: { type: "number", path: ["1"] } }, - { schema: z.boolean(), parsed: { type: "boolean", path: ["2"] } }, - { - schema: z.undefined(), - parsed: { type: "literal", value: undefined, path: ["3"] }, - }, - { schema: z.null(), parsed: { type: "literal", value: null, path: ["4"] } }, - { schema: z.bigint(), parsed: { type: "number", path: ["5"] } }, - { - schema: z.enum(testEnumValues), - parsed: { - type: "enum", - enumValues: testEnumValues as unknown as string[], - path: ["6"], - }, - }, - { - schema: z.string().array(), - parsed: { - type: "array", - path: ["7"], - childType: { type: "string", path: [] }, - }, - }, - { - schema: z.string().optional(), - parsed: { type: "string", path: ["8"], optional: true }, - }, -]; diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/array.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/array.test.ts deleted file mode 100644 index 2ac9ce0..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/array.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { z } from "zod"; -import type { ArrayNode, ObjectNode } from "../../../parseNodeTypes"; -import { defaultReferences } from "../../defaultReferences"; -import { parseZodArrayDef } from "../../zod/parsers/parseZodArrayDef"; -import { parseZodObjectDef } from "../../zod/parsers/parseZodObjectDef"; - -describe("Parse Zod Array", () => { - it("should parse a string array schema", () => { - const expected: ArrayNode = { - type: "array", - childType: { - type: "string", - path: [], - }, - path: [], - }; - const schema = z.string().array(); - const parsed = parseZodArrayDef(schema._def, defaultReferences()); - expect(parsed).toStrictEqual(expected); - }); - - it("should pass an empty array as the path for object-nested array childType", () => { - const expected: ObjectNode = { - type: "object", - children: { - childArray: { - type: "array", - path: ["childArray"], - childType: { - type: "string", - path: [], - }, - }, - }, - path: [], - }; - const schema = z.object({ - childArray: z.string().array(), - }); - const parsed = parseZodObjectDef(schema._def, defaultReferences()); - expect(parsed).toStrictEqual(expected); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/bigint.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/bigint.test.ts deleted file mode 100644 index fcfa649..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/bigint.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; -import type { NumberNode } from "../../../parseNodeTypes"; -import { defaultReferences } from "../../defaultReferences"; -import { parseZodBigIntDef } from "../../zod/parsers/parseZodBigIntDef"; - -describe("Zod BigInt", () => { - it("should parse a big end as a number node", () => { - const expected: NumberNode = { - type: "number", - path: [], - }; - const schema = z.bigint(); - const parsed = parseZodBigIntDef(schema._def, defaultReferences()); - expect(parsed).toStrictEqual(expected); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/boolean.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/boolean.test.ts deleted file mode 100644 index 8548e4d..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/boolean.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { z } from "zod"; -import type { BooleanNode } from "../../../parseNodeTypes"; -import { defaultReferences } from "../../defaultReferences"; -import { parseZodBooleanFieldDef } from "../../zod/parsers/parseZodBooleanFieldDef"; - -describe("Parse Zod Boolean", () => { - it("should parse a zod boolean as a boolean node", () => { - const expected: BooleanNode = { - type: "boolean", - path: [], - }; - const schema = z.boolean(); - const parsed = parseZodBooleanFieldDef(schema._def, defaultReferences()); - expect(parsed).toStrictEqual(expected); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/branded.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/branded.test.ts deleted file mode 100644 index af3fbf6..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/branded.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { type ZodBrandedDef, type ZodType, z } from "zod"; -import type { ParsedInputNode } from "../../../parseNodeTypes"; -import { defaultReferences } from "../../defaultReferences"; -import { parseZodBrandedDef } from "../../zod/parsers/parseZodBrandedDef"; - -describe("Parsed ZodBranded", () => { - it("should parse branded nodes as their base zod type", () => { - const testCases: { - node: ParsedInputNode; - zodType: ZodType; - }[] = [ - { - node: { - type: "number", - path: [], - }, - zodType: z.number().brand("number"), - }, - { - node: { - type: "string", - path: [], - }, - zodType: z.string().brand("string"), - }, - ]; - for (const testCase of testCases) { - const parsed = parseZodBrandedDef( - testCase.zodType._def as unknown as ZodBrandedDef, - defaultReferences(), - ); - expect(parsed).toStrictEqual(testCase.node); - } - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/default.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/default.test.ts deleted file mode 100644 index 4c394f4..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/default.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodDefaultDef } from "@src/parse/input-mappers/zod/parsers/parseZodDefaultDef"; -import type { ParsedInputNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodDefault", () => { - it("should parse zod number with default as number", () => { - const expected: ParsedInputNode = { - type: "number", - path: [], - }; - const zodSchema = z.number().default(5); - const parsed = parseZodDefaultDef(zodSchema._def, defaultReferences()); - expect(parsed).toStrictEqual(expected); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/discriminatedUnion.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/discriminatedUnion.test.ts deleted file mode 100644 index f39b6b5..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/discriminatedUnion.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { - type ZodDiscriminatedUnionDefUnversioned, - parseZodDiscriminatedUnionDef, -} from "@src/parse/input-mappers/zod/parsers/parseZodDiscriminatedUnionDef"; -import type { DiscriminatedUnionNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse Zod Discriminated Union", () => { - //write test - it("should parse a discriminated union node", () => { - const expected: DiscriminatedUnionNode = { - type: "discriminated-union", - path: [], - discriminatorName: "disc", - discriminatedUnionValues: ["one", "two"], - discriminatedUnionChildrenMap: { - one: { - type: "object", - children: { - numberPropertyOne: { - type: "number", - path: ["numberPropertyOne"], - }, - disc: { - type: "literal", - path: ["disc"], - value: "one", - }, - }, - path: [], - }, - two: { - type: "object", - children: { - stringPropertyTwo: { - type: "string", - path: ["stringPropertyTwo"], - }, - disc: { - type: "literal", - path: ["disc"], - value: "two", - }, - }, - path: [], - }, - }, - }; - const zodSchema = z.discriminatedUnion("disc", [ - z.object({ - disc: z.literal("one"), - numberPropertyOne: z.number(), - }), - z.object({ - disc: z.literal("two"), - stringPropertyTwo: z.string(), - }), - ]); - const parsedZod = parseZodDiscriminatedUnionDef( - zodSchema._def as unknown as ZodDiscriminatedUnionDefUnversioned, - defaultReferences(), - ); - expect(parsedZod).toStrictEqual(expected); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/effects.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/effects.test.ts deleted file mode 100644 index 213308a..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/effects.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodEffectsDef } from "@src/parse/input-mappers/zod/parsers/parseZodEffectsDef"; -import type { StringNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodEffects", () => { - it("should parse a zod effects string as an string", () => { - const expected: StringNode = { - type: "string", - path: [], - }; - const schema = z.preprocess((val) => String(val), z.string()); - expect(parseZodEffectsDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/enum.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/enum.test.ts deleted file mode 100644 index cc1e10a..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/enum.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodEnumDef } from "@src/parse/input-mappers/zod/parsers/parseZodEnumDef"; -import type { EnumNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodEnum", () => { - it("should parse a zod enum", () => { - const expected: EnumNode = { - type: "enum", - enumValues: ["one", "two", "three"], - path: [], - }; - const parsed = parseZodEnumDef( - z.enum(["one", "two", "three"])._def, - defaultReferences(), - ); - expect(expected).toStrictEqual(parsed); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/literal.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/literal.test.ts deleted file mode 100644 index a2af996..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/literal.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodLiteralDef } from "@src/parse/input-mappers/zod/parsers/parseZodLiteralDef"; -import type { LiteralNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodLiteral", () => { - it("should parse a zod literal for each possible type", () => { - const testCases: { - value: LiteralNode["value"]; - expectedNode: LiteralNode; - }[] = [ - { - value: "string", - expectedNode: { - type: "literal", - value: "string", - path: [], - }, - }, - { - value: 5, - expectedNode: { - type: "literal", - value: 5, - path: [], - }, - }, - { - value: undefined, - expectedNode: { - type: "literal", - value: undefined, - path: [], - }, - }, - { - value: null, - expectedNode: { - value: null, - type: "literal", - path: [], - }, - }, - { - value: BigInt(5), - expectedNode: { - value: BigInt(5), - type: "literal", - path: [], - }, - }, - ]; - for (const testCase of testCases) { - expect( - parseZodLiteralDef(z.literal(testCase.value)._def, defaultReferences()), - ).toStrictEqual(testCase.expectedNode); - } - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/nativeEnum.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/nativeEnum.test.ts deleted file mode 100644 index 9e05910..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/nativeEnum.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodNativeEnumDef } from "@src/parse/input-mappers/zod/parsers/parseZodNativeEnumDef"; -import type { EnumNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodNativeEnum", () => { - it("should parse a zod native enum", () => { - const expected: EnumNode = { - type: "enum", - enumValues: ["one", "two", "three"], - path: [], - }; - - enum ExampleEnum { - ONE = "one", - TWO = "two", - THREE = "three", - } - - const parsed = parseZodNativeEnumDef( - z.nativeEnum(ExampleEnum)._def, - defaultReferences(), - ); - expect(expected).toStrictEqual(parsed); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/null.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/null.test.ts deleted file mode 100644 index f298030..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/null.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodNullDef } from "@src/parse/input-mappers/zod/parsers/parseZodNullDef"; -import type { LiteralNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodNull", () => { - it("should parse a zod nullable as a literal with value null", () => { - const expected: LiteralNode = { - type: "literal", - value: null, - path: [], - }; - const schema = z.null(); - expect(parseZodNullDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/nullable.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/nullable.test.ts deleted file mode 100644 index c5c2f3c..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/nullable.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodNullableDef } from "@src/parse/input-mappers/zod/parsers/parseZodNullableDef"; -import type { NumberNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodNullable", () => { - it("should parse a nullable as it's underlying type", () => { - const expected: NumberNode = { - type: "number", - path: [], - }; - const schema = z.number().nullable(); - expect(parseZodNullableDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/number.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/number.test.ts deleted file mode 100644 index 51f1bfb..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/number.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodNumberDef } from "@src/parse/input-mappers/zod/parsers/parseZodNumberDef"; -import type { NumberNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodNumber", () => { - it("should parse a number node", () => { - const expected: NumberNode = { - type: "number", - path: [], - }; - const schema = z.number(); - expect(parseZodNumberDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/object.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/object.test.ts deleted file mode 100644 index 2764536..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/object.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodObjectDef } from "@src/parse/input-mappers/zod/parsers/parseZodObjectDef"; -import type { ObjectNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodObject", () => { - it("should parse an empty zod object", () => { - const expected: ObjectNode = { - type: "object", - children: {}, - path: [], - }; - const schema = z.object({}); - expect(parseZodObjectDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); - it("should parse an object with different property types", () => { - const expected: ObjectNode = { - type: "object", - children: { - number: { - type: "number", - path: ["number"], - }, - string: { - type: "string", - path: ["string"], - }, - }, - path: [], - }; - const schema = z.object({ - number: z.number(), - string: z.string(), - }); - expect(parseZodObjectDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); - it("should correctly create nested object paths", () => { - const expected: ObjectNode = { - type: "object", - path: [], - children: { - obj: { - type: "object", - path: ["obj"], - children: { - obj2: { - type: "object", - path: ["obj", "obj2"], - children: { - str: { - type: "string", - path: ["obj", "obj2", "str"], - }, - }, - }, - }, - }, - }, - }; - const schema = z.object({ - obj: z.object({ - obj2: z.object({ - str: z.string(), - }), - }), - }); - expect(parseZodObjectDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/optional.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/optional.test.ts deleted file mode 100644 index 157cd87..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/optional.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodOptionalDef } from "@src/parse/input-mappers/zod/parsers/parseZodOptionalDef"; -import type { NumberNode, ObjectNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodOptional", () => { - it("should return it's parsed inner type with optional true", () => { - const expected: NumberNode = { - type: "number", - optional: true, - path: [], - }; - const schema = z.number().optional(); - expect(parseZodOptionalDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); - it("should not apply optional: true to nodes that are not direct children", () => { - const expected: ObjectNode = { - optional: true, - type: "object", - path: [], - children: { - number: { - type: "number", - path: ["number"], - }, - }, - }; - const schema = z - .object({ - number: z.number(), - }) - .optional(); - expect(parseZodOptionalDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/promise.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/promise.test.ts deleted file mode 100644 index 03fc159..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/promise.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodPromiseDef } from "@src/parse/input-mappers/zod/parsers/parseZodPromiseDef"; -import type { NumberNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodPromise", () => { - it("should parse a zod promise as it's underlying node type", () => { - const expected: NumberNode = { - type: "number", - path: [], - }; - const schema = z.number().promise(); - expect(parseZodPromiseDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/string.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/string.test.ts deleted file mode 100644 index 5f8797c..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/string.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodStringDef } from "@src/parse/input-mappers/zod/parsers/parseZodStringDef"; -import type { StringNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodString", () => { - it("should parse a string schema as a string node", () => { - const expected: StringNode = { - type: "string", - path: [], - }; - const schema = z.string(); - expect(parseZodStringDef(schema._def, defaultReferences())).toStrictEqual( - expected, - ); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/undefined.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/undefined.test.ts deleted file mode 100644 index 96e8882..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/undefined.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodUndefinedDef } from "@src/parse/input-mappers/zod/parsers/parseZodUndefinedDef"; -import type { LiteralNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse ZodUndefined", () => { - it("should parse zod undefined as a literal with value undefined", () => { - const unexpected: LiteralNode = { - type: "literal", - value: undefined, - path: [], - }; - const schema = z.undefined(); - expect( - parseZodUndefinedDef(schema._def, defaultReferences()), - ).toStrictEqual(unexpected); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/union.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/union.test.ts deleted file mode 100644 index d219e68..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/union.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { defaultReferences } from "@src/parse/input-mappers/defaultReferences"; -import { parseZodUnionDef } from "@src/parse/input-mappers/zod/parsers/parseZodUnionDef"; -import type { UnionNode } from "@src/parse/parseNodeTypes"; -import { z } from "zod"; - -describe("Parse Zod Union", () => { - it("should parse a union node", () => { - const expected: UnionNode = { - type: "union", - path: [], - values: [ - { - type: "literal", - value: "one", - path: [], - }, - { - type: "literal", - value: 2, - path: [], - }, - ], - }; - const zodSchema = z.union([z.literal("one"), z.literal(2)]); - const parsedZod = parseZodUnionDef(zodSchema._def, defaultReferences()); - expect(parsedZod).toStrictEqual(expected); - }); -}); diff --git a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/void.test.ts b/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/void.test.ts deleted file mode 100644 index d48c9ee..0000000 --- a/packages/trpc-ui/src/parse/input-mappers/__tests__/zod/void.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { z } from "zod"; -import type { LiteralNode } from "../../../parseNodeTypes"; -import { defaultReferences } from "../../defaultReferences"; -import { parseZodVoidDef } from "../../zod/parsers/parseZodVoidDef"; -import { zodSelectorFunction } from "../../zod/selector"; - -describe("Parse ZodVoid", () => { - it("should parse a void def as a literal node with undefined value", () => { - const expected: LiteralNode = { - type: "literal", - path: [], - value: undefined, - }; - const zodSchema = z.void(); - const parsed = parseZodVoidDef(zodSchema._def, defaultReferences()); - expect(parsed).toStrictEqual(expected); - }); - - it("should be mapped correctly via the selector and parsed as a literal node", () => { - const expected: LiteralNode = { - type: "literal", - path: [], - value: undefined, - }; - const zodSchema = z.void(); - const parsed = zodSelectorFunction(zodSchema._def, defaultReferences()); - expect(parsed).toStrictEqual(expected); - }); -}); diff --git a/packages/trpc-ui/src/parse/parseProcedure.ts b/packages/trpc-ui/src/parse/parseProcedure.ts index 2f0c388..82c0108 100644 --- a/packages/trpc-ui/src/parse/parseProcedure.ts +++ b/packages/trpc-ui/src/parse/parseProcedure.ts @@ -115,7 +115,7 @@ function nodeAndInputSchemaFromInputs( }; } -export function parseProcedure( +function parseProcedure( procedure: Procedure, path: string[], options: TrpcPanelExtraOptions, diff --git a/packages/trpc-ui/src/parse/parseRouter.ts b/packages/trpc-ui/src/parse/parseRouter.ts index 54b7fbd..db21947 100644 --- a/packages/trpc-ui/src/parse/parseRouter.ts +++ b/packages/trpc-ui/src/parse/parseRouter.ts @@ -3,7 +3,7 @@ import { type Router, isProcedure, isRouter } from "./routerType"; import type { AnyTRPCRouter } from "@trpc/server"; import type { zodToJsonSchema } from "zod-to-json-schema"; import { logParseError } from "./parseErrorLogs"; -import { type ParsedProcedure, parseProcedure } from "./parseProcedure"; +import { type ParsedProcedure } from "./parseProcedure"; // TODO this should be more specific, as it hurts the type safety lower down export type JSON7SchemaType = ReturnType; @@ -42,9 +42,9 @@ function parseRouter( if (isRouter(child)) { return parseRouter(child, newPath, options); } - if (isProcedure(child)) { - return parseProcedure(child, newPath, options); - } + // if (isProcedure(child)) { + // return parseProcedure(child, newPath, options); + // } return null; })(); if (!parsedNode) { @@ -67,7 +67,7 @@ export type TrpcPanelExtraOptions = { transformer?: "superjson"; }; -export function parseRouterWithOptions( +function parseRouterWithOptions( router: AnyTRPCRouter, parseRouterOptions: TrpcPanelExtraOptions, ) { diff --git a/packages/trpc-ui/src/parseV2/detectValidator.ts b/packages/trpc-ui/src/parseV2/detectValidator.ts new file mode 100644 index 0000000..2e1ac34 --- /dev/null +++ b/packages/trpc-ui/src/parseV2/detectValidator.ts @@ -0,0 +1,59 @@ +import type { ValidatorType } from "./types"; + +export function detectValidatorType(validator: any): ValidatorType { + // Handle null or undefined + if (validator == null) { + return "unknown"; + } + + // First attempt: Check for standard schema vendor property + try { + if (validator["~standard"]?.vendor) { + const vendor = validator["~standard"].vendor.toLowerCase(); + if (vendor.includes("zod")) return "zod"; + if (vendor.includes("valibot")) return "valibot"; + if (vendor.includes("arktype")) return "arktype"; + } + } catch (e) { + // Ignore errors when accessing properties + } + + // Second attempt: Use heuristics based on library-specific properties + + // Check for Zod + // Zod schemas have specific properties like _def, safeParse, parse, and ZodType + if ( + validator._def !== undefined && + typeof validator.safeParse === "function" && + typeof validator.parse === "function" && + validator instanceof Object.getPrototypeOf(validator).constructor && + (Object.getPrototypeOf(validator).constructor.name.includes("Zod") || + validator.constructor.name.includes("Zod")) + ) { + return "zod"; + } + + // Check for Valibot + // Valibot validators have specific structure with _type, _schema, and _parse properties + if ( + validator._type !== undefined && + (validator._schema !== undefined || validator._expected !== undefined) && + typeof validator._parse === "function" + ) { + return "valibot"; + } + + // Check for Arktype + // Arktype types have specific properties like infer, type, as, and schema + if ( + typeof validator.infer === "function" && + validator.type !== undefined && + typeof validator.as === "function" && + validator.schema !== undefined + ) { + return "arktype"; + } + + // Unknown validator type + return "unknown"; +} diff --git a/packages/trpc-ui/src/parseV2/fetcher.ts b/packages/trpc-ui/src/parseV2/fetcher.ts new file mode 100644 index 0000000..1e8020f --- /dev/null +++ b/packages/trpc-ui/src/parseV2/fetcher.ts @@ -0,0 +1,104 @@ +import type { Procedure } from "./types"; + +interface DataTransformer { + serialize(object: any): any; + deserialize(object: any): any; +} + +interface FetchWrapperOptions { + baseUrl: string; + headers?: Record; + fetch?: typeof fetch; + transformer?: DataTransformer; +} + +interface ProcedureCallOptions { + input?: any; + signal?: AbortSignal; +} + +export function createProcedureFetcher(options: FetchWrapperOptions) { + const { + baseUrl, + headers = {}, + fetch: customFetch = globalThis.fetch, + transformer, + } = options; + + return async function callProcedure( + procedure: Procedure, + callOptions: ProcedureCallOptions = {}, + ) { + const { input, signal } = callOptions; + + // Build the procedure path from the path array + const dotPath = procedure.path.join("."); + const baseUrlClean = baseUrl.replace(/\/$/, ""); + + // Determine HTTP method based on procedure type + const method = procedure.type === "query" ? "GET" : "POST"; + + // Prepare request options + const requestOptions: RequestInit = { + method, + headers: { + "Content-Type": "application/json", + ...headers, + }, + signal, + }; + + let url = `${baseUrlClean}/${dotPath}`; + const queryParts: string[] = []; + + // Handle input serialization based on procedure type + if (input !== undefined) { + if (procedure.type === "query") { + // For queries, add input as URL parameter (no batch for single requests) + queryParts.push(`input=${encodeURIComponent(JSON.stringify(input))}`); + } else { + // For mutations, add input as request body + requestOptions.body = JSON.stringify(input); + } + } + + // Construct final URL with query parameters + if (queryParts.length > 0) { + url += `?${queryParts.join("&")}`; + } + + console.log("🔍 Request Debug:"); + console.log("URL:", url); + console.log("Method:", method); + console.log("Body:", requestOptions.body); + console.log("Input:", input); + console.log( + "Serialized Input:", + transformer ? transformer.serialize(input) : input, + ); + + // Make the request + const response = await customFetch(url, requestOptions); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Response Error:", errorText); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const json = await response.json(); + + // Handle tRPC response format + if ("error" in json) { + throw new Error(`tRPC Error: ${json.error.message}`); + } + + // Apply transformer to deserialize response if available + const resultData = json.result?.data; + if (transformer && resultData !== undefined) { + return transformer.deserialize(resultData); + } + + return resultData; + }; +} diff --git a/packages/trpc-ui/src/parseV2/parse.ts b/packages/trpc-ui/src/parseV2/parse.ts new file mode 100644 index 0000000..598de27 --- /dev/null +++ b/packages/trpc-ui/src/parseV2/parse.ts @@ -0,0 +1,166 @@ +import { toJsonSchema } from "@valibot/to-json-schema"; +import type { Type as ArkTypeValidator } from "arktype"; +import type { JSONSchema7Object } from "json-schema"; +import * as v from "valibot"; +import { zodToJsonSchema } from "zod-to-json-schema"; +import * as z3 from "zod/v3"; +import * as z4 from "zod/v4"; +import { detectValidatorType } from "./detectValidator"; +import type { ParsedTRPCRouter, Router } from "./types"; + +export function parseRootRouter(router: any): Router { + return parseTRPCRouter(router, []) as unknown as Router; +} + +/** + * Recursively parses a tRPC router structure and its sub-routers + * + * @param router - The router or procedure to parse + * @param currentPath - The current path in the router hierarchy + * @param detectValidatorFn - Function to detect the type of validator + * @param zodToJsonSchemaFn - Function to convert Zod schema to JSON Schema (optional) + * @returns A structured representation of the router hierarchy + */ +export function parseTRPCRouter( + router: any, + currentPath: string[] = [], +): ParsedTRPCRouter { + // The result object we'll build up + const result: Record = {}; + + // Iterate over each key in the router + for (const key in router) { + const item = router[key]; + + // Skip all internal properties (starting with _) + if (key.startsWith("_")) { + continue; + } + + // Create the path for this node + const nodePath = [...currentPath, key]; + + // Check if it's a procedure (query or mutation) + if (item?._def?.type) { + const meta = item._def.meta || {}; + + // Determine validator type + let validatorType: "zod" | "valibot" | "arktype" | "unknown" | "mixed" = + "unknown"; + let jsonSchema: any = undefined; + + // Check if inputs array exists and has elements + if ( + item._def.inputs && + Array.isArray(item._def.inputs) && + item._def.inputs.length > 0 + ) { + // Get validator type of first input + const firstType = detectValidatorType(item._def.inputs[0]); + + // Check if all inputs are of the same type + const allSameType = item._def.inputs.every( + (input: any) => detectValidatorType(input) === firstType, + ); + + validatorType = allSameType ? firstType : "mixed"; + + // Generate JSON Schema for Zod validators + if (validatorType === "zod") { + try { + // Merge all Zod schemas + let mergedSchema = item._def.inputs[0]; + + for (let i = 1; i < item._def.inputs.length; i++) { + if (typeof mergedSchema.extend === "function") { + mergedSchema = mergedSchema.extend(item._def.inputs[i].shape); + } + } + + //* This works, but the merging is not working + // jsonSchema = zodToJsonSchema(mergedSchema); + + if ("_zod" in mergedSchema) { + jsonSchema = z4.toJSONSchema(mergedSchema, { + target: "draft-7", + unrepresentable: "any", + }); + } else { + jsonSchema = zodToJsonSchema(mergedSchema); + } + } catch (error) { + // If merging or conversion fails, leave jsonSchema as undefined + console.error("Error generating JSON Schema:", error); + } + } else if (validatorType === "valibot") { + try { + const merged = v.intersect(item._def.inputs); + jsonSchema = toJsonSchema(merged); + } catch (error) { + // If merging or conversion fails, leave jsonSchema as undefined + console.error("Error generating JSON Schema:", error); + } + } else if (validatorType === "arktype") { + console.log("arktype"); + console.log(item._def.inputs); + jsonSchema = arkToJson(item._def.inputs); + } + } + + if (item._def.type === "query") { + result[key] = { + type: "query", + path: nodePath, + meta, + validator: validatorType, + schema: jsonSchema, + }; + } else if (item._def.type === "mutation") { + result[key] = { + type: "mutation", + path: nodePath, + meta, + validator: validatorType, + schema: jsonSchema, + }; + } + } + // Check if it's a router (contains other procedures or routers) + else if (item && typeof item === "object" && !Array.isArray(item)) { + // Recursively parse potential router + const children = parseTRPCRouter(item, nodePath); + + // Only add it as a router if it has children + if (Object.keys(children).length > 0) { + result[key] = { + type: "router", + path: nodePath, + children, + }; + } + } + } + + return result; +} + +function arkToJson(inputs: ArkTypeValidator[]): JSONSchema7Object { + if (inputs.length === 1) { + return inputs[0]?.toJsonSchema(); + } + if (inputs.length > 1) { + const [first, ...rest] = inputs; + return arkRecursive(first, rest); + } + return {}; +} +function arkRecursive( + base: ArkTypeValidator, + rest: ArkTypeValidator[], +): JSONSchema7Object { + if (rest.length === 0) { + return base.toJsonSchema(); + } + const [first, ...left] = rest; + return arkRecursive(base.and(first), left); +} diff --git a/packages/trpc-ui/src/parseV2/types.ts b/packages/trpc-ui/src/parseV2/types.ts new file mode 100644 index 0000000..237d7b1 --- /dev/null +++ b/packages/trpc-ui/src/parseV2/types.ts @@ -0,0 +1,44 @@ +import type { JSONSchema7Object } from "json-schema"; + +/** + * Type representing the validator types supported by the parser + */ +export type ValidatorType = "zod" | "valibot" | "arktype" | "unknown" | "mixed"; + +/** + * Base type for common properties shared by routers and procedures + */ +export type BaseNodeType = { + path: string[]; +}; + +/** + * Type representing metadata that can be attached to a procedure + */ +export type ProcedureMeta = Record; + +/** + * Type representing any procedure (query or mutation) + */ +export type Procedure = BaseNodeType & { + type: "mutation" | "query"; + meta: ProcedureMeta; + validator: ValidatorType; + schema?: JSONSchema7Object; +}; + +/** + * Type for a tRPC router + */ +export type Router = BaseNodeType & { + type: "router"; + children: ParsedTRPCRouter; +}; + +export type RouterOrProcedure = Router | Procedure; + +/** + * Type representing the output of the parseTRPCRouter function + */ +// TODO rename this to children +export type ParsedTRPCRouter = Record; diff --git a/packages/trpc-ui/src/react-app/Root.tsx b/packages/trpc-ui/src/react-app/Root.tsx index f4aadb3..0cbc1e8 100644 --- a/packages/trpc-ui/src/react-app/Root.tsx +++ b/packages/trpc-ui/src/react-app/Root.tsx @@ -1,13 +1,20 @@ +import type { ParsedTRPCRouter } from "@src/parseV2/types"; import { HeadersPopup } from "@src/react-app/components/HeadersPopup"; import { SearchOverlay } from "@src/react-app/components/SearchInputOverlay"; -import { AllPathsContextProvider } from "@src/react-app/components/contexts/AllPathsContext"; +import { + AllPathsContextProvider, + useAllPaths, +} from "@src/react-app/components/contexts/AllPathsContext"; import { HeadersContextProvider, useHeaders, } from "@src/react-app/components/contexts/HeadersContext"; import { HotKeysContextProvider } from "@src/react-app/components/contexts/HotKeysContext"; -import { SiteNavigationContextProvider } from "@src/react-app/components/contexts/SiteNavigationContext"; -import { useSiteNavigationContext } from "@src/react-app/components/contexts/SiteNavigationContext"; +import { + SiteNavigationContextProvider, + useSiteNavigationContext, +} from "@src/react-app/components/contexts/SiteNavigationContext"; + import { useLocalStorage } from "@src/react-app/components/hooks/useLocalStorage"; import type { RenderOptions } from "@src/render"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -24,33 +31,33 @@ import { MetaHeader } from "./components/MetaHeader"; import { RouterContainer } from "./components/RouterContainer"; import { SideNav } from "./components/SideNav"; import { TopBar } from "./components/TopBar"; -import { RenderOptionsProvider } from "./components/contexts/OptionsContext"; +import { + RenderOptionsProvider, + useRenderOptions, +} from "./components/contexts/OptionsContext"; +import { Container } from "./v2/Container"; export function RootComponent({ - rootRouter, + parsedRouter, options, - trpc, }: { - rootRouter: ParsedRouter; + parsedRouter: ParsedTRPCRouter; //* The new one options: RenderOptions; - trpc: ReturnType; }) { return ( - + - - - - -
- -
-
-
-
-
+ + + +
+ +
+
+
+
@@ -58,49 +65,15 @@ export function RootComponent({ ); } -function ClientProviders({ - trpc, - children, - options, -}: { - trpc: ReturnType; - children: ReactNode; - options: RenderOptions; -}) { - const headers = useHeaders(); - const [trpcClient] = useState(() => - trpc.createClient({ - links: [ - httpBatchLink({ - url: options.url, - headers: headers.getHeaders, - }), - ], - transformer: (() => { - if (options.transformer === "superjson") return superjson; - return undefined; - })(), - }), - ); - const [queryClient] = useState(() => new QueryClient()); - - return ( - - - {children} - {/* */} - - - ); -} - function AppInnards({ - rootRouter, options, + parsedRouter, }: { - rootRouter: ParsedRouter; + parsedRouter: ParsedTRPCRouter; options: RenderOptions; }) { + const { router } = useRenderOptions(); + const [sidebarOpen, setSidebarOpen] = useLocalStorage( "trpc-panel.show-minimap", true, @@ -112,15 +85,16 @@ function AppInnards({ useEffect(() => { openAndNavigateTo(path ?? [], true); }, []); + const allPaths = useAllPaths(); return (
- +
{JSON.stringify(allPaths, null, 2)}
+ {/* */} + {/*
{JSON.stringify(router, null, 2)}
*/} + {Object.entries(router).map(([key, routerOrProcedure]) => { + return ; + })}
diff --git a/packages/trpc-ui/src/react-app/components/CollapsableSection.tsx b/packages/trpc-ui/src/react-app/components/CollapsableSection.tsx index 020ffd8..0d98839 100644 --- a/packages/trpc-ui/src/react-app/components/CollapsableSection.tsx +++ b/packages/trpc-ui/src/react-app/components/CollapsableSection.tsx @@ -2,8 +2,8 @@ import { Chevron } from "@src/react-app/components/Chevron"; import { collapsables, useCollapsableIsShowing, - useSiteNavigationContext, } from "@src/react-app/components/contexts/SiteNavigationContext"; +import { useSiteNavigationContext } from "@src/react-app/components/contexts/SiteNavigationContext"; import { backgroundColor, solidColorBg, @@ -40,7 +40,7 @@ export function CollapsableSection({ }) { const { scrollToPathIfMatches } = useSiteNavigationContext(); const shown = useCollapsableIsShowing(fullPath); - const [_path, setPath] = useQueryState("path"); + const [path, setPath] = useQueryState("path"); const containerRef = useRef(null); useEffect(() => { diff --git a/packages/trpc-ui/src/react-app/components/SideNav.tsx b/packages/trpc-ui/src/react-app/components/SideNav.tsx index e839387..060050a 100644 --- a/packages/trpc-ui/src/react-app/components/SideNav.tsx +++ b/packages/trpc-ui/src/react-app/components/SideNav.tsx @@ -1,21 +1,27 @@ import type { ParsedProcedure } from "@src/parse/parseProcedure"; +import type { + ParsedTRPCRouter, + Router, + RouterOrProcedure, +} from "@src/parseV2/types"; import { Chevron } from "@src/react-app/components/Chevron"; import { ItemTypeIcon } from "@src/react-app/components/ItemTypeIcon"; import { collapsables, - useCollapsableIsShowing, useSiteNavigationContext, } from "@src/react-app/components/contexts/SiteNavigationContext"; +import { useCollapsableIsShowing } from "@src/react-app/components/contexts/SiteNavigationContext"; import { colorSchemeForNode } from "@src/react-app/components/style-utils"; import React, { useCallback } from "react"; import type { ParsedRouter } from "../../parse/parseRouter"; export function SideNav({ - rootRouter, + // rootRouter, open, -}: // setOpen, -{ + parsedRoouter, +}: { open: boolean; - rootRouter: ParsedRouter; + // rootRouter: ParsedRouter; + parsedRoouter: ParsedTRPCRouter; setOpen: (value: boolean) => void; }) { return open ? ( @@ -23,29 +29,32 @@ export function SideNav({ style={{ maxHeight: "calc(100vh - 4rem)" }} className="flex min-w-[16rem] flex-col items-start space-y-2 overflow-scroll border-r-2 border-r-panelBorder bg-actuallyWhite p-2 pr-4 shadow-sm" > - + {Object.entries(parsedRoouter).map(([key, routerOrProcedure]) => { + return ; + })}
) : null; } +//* Fix function SideNavItem({ node, - path, + // path, }: { - node: ParsedRouter | ParsedProcedure; - path: string[]; + node: RouterOrProcedure; + // path: string[]; }) { const { markForScrollTo } = useSiteNavigationContext(); - const shown = useCollapsableIsShowing(path) || path.length === 0; + const shown = useCollapsableIsShowing(node.path) || node.path.length === 0; const onClick = useCallback(function onClick() { - collapsables.toggle(path); - markForScrollTo(path); + collapsables.toggle(node.path); + markForScrollTo(node.path); }, []); return ( <> - {path.length > 0 && ( + {node.path.length > 0 && (