diff --git a/packages/test-app/package.json b/packages/test-app/package.json index 1e36749..e66ade7 100644 --- a/packages/test-app/package.json +++ b/packages/test-app/package.json @@ -26,10 +26,10 @@ "trpc-panel": "^1.0.0", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.0", - "zod": "^3.19.1" + "zod": "3.20.2" }, "scripts": { - "dev": "SERVER_URL=\"http://localhost\" TRPC_PATH=\"trpc\" DEV_PORT=\"4000\" SIMULATE_DELAY=\"true\" nodemon ./src/server.ts", + "dev": "LIVE_RELOAD=\"true\" SERVER_URL=\"http://localhost\" TRPC_PATH=\"trpc\" DEV_PORT=\"4000\" SIMULATE_DELAY=\"true\" nodemon ./src/server.ts", "build": "npx tsc --project tsconfig.json", "start": "node ./lib/server" }, diff --git a/packages/trpc-panel/package.json b/packages/trpc-panel/package.json index b09a20a..3644a3d 100644 --- a/packages/trpc-panel/package.json +++ b/packages/trpc-panel/package.json @@ -28,7 +28,6 @@ }, "peerDependencies": { "@trpc/server": "^10.0.0", - "react": "^18.2.0", "zod": "^3.19.1" }, "devDependencies": { diff --git a/packages/trpc-panel/rollup.config.js b/packages/trpc-panel/rollup.config.js index b6d124c..97752f0 100644 --- a/packages/trpc-panel/rollup.config.js +++ b/packages/trpc-panel/rollup.config.js @@ -48,9 +48,17 @@ export default [ typescript(), replace({ "process.env.NODE_ENV": JSON.stringify("production"), + preventAssignment: false, }), babel({ - presets: ["@babel/preset-react"], + presets: [ + [ + "@babel/preset-react", + { + development: isWatching, + }, + ], + ], }), commonjs(), copy({ @@ -62,7 +70,6 @@ export default [ ], }), !isWatching && terser(), - visualizer(), ], }, ]; diff --git a/packages/trpc-panel/src/index.ts b/packages/trpc-panel/src/index.ts index 966412c..b194001 100644 --- a/packages/trpc-panel/src/index.ts +++ b/packages/trpc-panel/src/index.ts @@ -1,3 +1 @@ -export { parseNode as parseRouter } from "./parse/parse-router"; -export { mapZodObjectToNode as mapZodObject } from "./parse/input-mappers/zod"; export { renderTrpcPanel as renderTrpcPanel } from "./render"; diff --git a/packages/trpc-panel/src/parse/input-mappers/zod.ts b/packages/trpc-panel/src/parse/input-mappers/zod.ts deleted file mode 100644 index 7412127..0000000 --- a/packages/trpc-panel/src/parse/input-mappers/zod.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { ParsedInputNode } from "../parse-router"; -import { z } from "zod"; -// TODO - Use zods enum instead? -enum ZodTypeNames { - string = "ZodString", - number = "ZodNumber", - boolean = "ZodBoolean", - object = "ZodObject", - enum = "ZodEnum", - array = "ZodArray", - descriminatedUnion = "ZodDiscriminatedUnion", - optional = "ZodOptional", - literal = "ZodLiteral", - void = "ZodVoid", -} - -function checkZodType(typeName: string, zodObject: z.AnyZodObject) { - return zodObject._def.typeName === typeName; -} - -function isZodOptional( - zodObject: z.AnyZodObject | z.ZodOptional -): zodObject is z.ZodOptional { - return ( - zodObject._def.typeName === - (ZodTypeNames.optional as unknown as z.ZodFirstPartyTypeKind) - ); -} - -function mapNode( - path: string[], - zodAny: z.AnyZodObject | z.ZodOptional -): ParsedInputNode | null { - const optional = isZodOptional(zodAny); - const unwrappedOptional = optional ? zodAny._def.innerType : zodAny; - if (checkZodType(ZodTypeNames.string, unwrappedOptional)) { - return { type: "string", path, optional }; - } - if (checkZodType(ZodTypeNames.number, unwrappedOptional)) { - return { type: "number", path, optional }; - } - if (checkZodType(ZodTypeNames.boolean, unwrappedOptional)) { - return { type: "boolean", path, optional }; - } - if (checkZodType(ZodTypeNames.descriminatedUnion, unwrappedOptional)) { - const union = unwrappedOptional as unknown as z.ZodDiscriminatedUnion< - string, - string, - any - >; - const entries = Array.from(union._def.options.entries()); - const nodeEntries = entries.map(([discriminatorValue, zodObj]) => [ - discriminatorValue, - mapNode(path, zodObj), - ]); - if (nodeEntries.some((e) => e[1] === null)) return null; - const nodesMap = Object.fromEntries(nodeEntries); - - return { - type: "discriminated-union", - path, - optional, - discriminatedUnionValues: entries.map(([n]) => n), - discriminatedUnionChildrenMap: nodesMap, - discriminatorName: union._def.discriminator, - }; - } - if (checkZodType(ZodTypeNames.enum, unwrappedOptional)) { - const enum_ = unwrappedOptional as z.ZodEnum; - const values = enum_._def.values as string[]; - return { type: "enum", path, optional, enumValues: values }; - } - if (checkZodType(ZodTypeNames.object, unwrappedOptional)) { - const obj = unwrappedOptional as z.ZodObject; - const shape = obj.shape; - const children: { [propertyName: string]: ParsedInputNode } = {}; - for (var propertyName of Object.keys(shape)) { - const node = mapNode( - path.concat([propertyName]), - shape[propertyName]! - ); - if (node === null) return null; - children[propertyName] = node; - } - return { - type: "object", - children, - path, - optional, - }; - } - if (checkZodType(ZodTypeNames.array, unwrappedOptional)) { - const { type } = (unwrappedOptional as z.ZodArray)._def; - // Pass empty path because it will be calculated dynamically on the front end - // (dynamic path gets passed to components) - const childType = mapNode([], type); - if (childType === null) return null; - return { - type: "array", - - childType, - path, - optional, - }; - } - if (checkZodType(ZodTypeNames.literal, unwrappedOptional)) { - const { value } = unwrappedOptional as z.ZodLiteral< - bigint | string | number | boolean - >; - return { - value, - type: "literal", - path, - optional, - }; - } - return null; -} - -export function mapZodObjectToNode( - object: z.AnyZodObject -): ParsedInputNode | null { - const parsed = mapNode([], object); - return parsed; -} diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodArrayDef.tsx b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodArrayDef.tsx new file mode 100644 index 0000000..0ce4a21 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodArrayDef.tsx @@ -0,0 +1,13 @@ +import { ZodArrayDef } from "zod"; +import { ArrayNode, ParseFunction } from "../../../parsed-node-types"; +import { zodSelectorFunction } from "../selector"; + +export const parseZodArrayDef: ParseFunction = (def, refs) => { + const {type} = def + const childType = zodSelectorFunction(type._def, refs) + return { + type: "array", + childType, + ...refs + }; +} \ No newline at end of file diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodBooleanFieldDef.ts b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodBooleanFieldDef.ts new file mode 100644 index 0000000..73ee9d0 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodBooleanFieldDef.ts @@ -0,0 +1,6 @@ +import { ZodBooleanDef } from "zod"; +import { BooleanNode, ParseFunction } from "../../../parsed-node-types"; + +export const parseZodBooleanFieldDef: ParseFunction = (_, ref)=>{ + return { type: "boolean", ...ref }; +} \ No newline at end of file diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodDiscriminatedUnionDef.ts b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodDiscriminatedUnionDef.ts new file mode 100644 index 0000000..3697068 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodDiscriminatedUnionDef.ts @@ -0,0 +1,67 @@ +import { AnyZodObject, ZodFirstPartyTypeKind } from "zod"; +import { + DiscriminatedUnionNode, + ParseFunction, +} from "../../../parsed-node-types"; +import { zodSelectorFunction } from "../selector"; + +type OptionsMap = Map; + +type ZodDiscriminatedUnionThreePointTwenty = { + optionsMap: OptionsMap; + discriminator: string; +}; + +type ZodDiscriminatedUnionPreThreePointTwenty = { + options: OptionsMap; + discriminator: string; +}; + +export type ZodDiscriminatedUnionDefUnversioned = + | ZodDiscriminatedUnionPreThreePointTwenty + | ZodDiscriminatedUnionThreePointTwenty; + +function isZodThreePointTwenty( + def: ZodDiscriminatedUnionDefUnversioned +): def is ZodDiscriminatedUnionThreePointTwenty { + return "optionsMap" in def; +} + +function makeDefConsistent(def: ZodDiscriminatedUnionDefUnversioned): { + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion; + discriminator: string; + options: Map; +} { + const optionsMap = isZodThreePointTwenty(def) + ? def.optionsMap + : def.options; + return { + typeName: ZodFirstPartyTypeKind.ZodDiscriminatedUnion, + discriminator: def.discriminator, + options: optionsMap, + }; +} + +export const parseZodDiscriminatedUnionDef: ParseFunction< + ZodDiscriminatedUnionDefUnversioned, + DiscriminatedUnionNode +> = (def, refs) => { + const defConsistent = makeDefConsistent(def); + const entries = Array.from(defConsistent.options.entries()); + const nodeEntries = entries.map(([discriminatorValue, zodObj]) => [ + discriminatorValue, + zodSelectorFunction(zodObj._def, refs), + ]); + // Not sure why this is here but seems important + // if (nodeEntries.some((e) => e[1] === null)) return null; + + const nodesMap = Object.fromEntries(nodeEntries); + + return { + type: "discriminated-union", + discriminatedUnionValues: entries.map(([n]) => n), + discriminatedUnionChildrenMap: nodesMap, + discriminatorName: def.discriminator, + ...refs, + }; +}; diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodEnumDef.ts b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodEnumDef.ts new file mode 100644 index 0000000..d3ec260 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodEnumDef.ts @@ -0,0 +1,10 @@ +import { ZodEnumDef } from "zod"; +import { EnumNode, ParseFunction } from "../../../parsed-node-types"; + +export const parseZodEnumDef: ParseFunction = ( + def, + refs +) => { + const values = def.values as string[]; + return { type: "enum", enumValues: values, ...refs }; +}; diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodLiteralDef.tsx b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodLiteralDef.tsx new file mode 100644 index 0000000..e5e5552 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodLiteralDef.tsx @@ -0,0 +1,10 @@ +import { ZodLiteralDef } from "zod"; +import { LiteralNode, ParseFunction } from "../../../parsed-node-types"; + +export const parseZodLiteralDef: ParseFunction = (def, refs)=>{ + return { + type :'literal', + value: def.value, + ...refs, + } +} \ No newline at end of file diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodNumberDef.ts b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodNumberDef.ts new file mode 100644 index 0000000..feea2ea --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodNumberDef.ts @@ -0,0 +1,13 @@ +import { NumberNode, ParseFunction } from "../../../parsed-node-types"; +import { ZodNumberDef } from "zod"; + +export const parseZodNumberDef: ParseFunction = ( + _, + references +) => { + return { + type: "number", + path: references.path, + optional: references.optional, + }; +}; diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodObjectDef.ts b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodObjectDef.ts new file mode 100644 index 0000000..06242a4 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodObjectDef.ts @@ -0,0 +1,29 @@ +import { ZodObjectDef } from "zod"; +import { + ObjectNode, + ParsedInputNode, + ParseFunction, + UnsupportedNode, +} from "../../../parsed-node-types"; +import { zodSelectorFunction } from "../selector"; + +export const parseZodObjectDef: ParseFunction< + ZodObjectDef, + ObjectNode | UnsupportedNode +> = (def, refs) => { + const shape = def.shape(); + const children: { [propertyName: string]: ParsedInputNode } = {}; + for (var propertyName of Object.keys(shape)) { + const node = zodSelectorFunction(shape[propertyName]!._def, { + ...refs, + path: refs.path.concat([propertyName]), + }); + children[propertyName] = node; + } + return { + type: "object", + children, + path: refs.path, + optional: refs.optional, + }; +}; diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodOptionalDef.tsx b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodOptionalDef.tsx new file mode 100644 index 0000000..7fa62d2 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodOptionalDef.tsx @@ -0,0 +1,12 @@ +import { ZodOptionalDef } from "zod"; +import { ParsedInputNode, ParseFunction } from "../../../parsed-node-types"; +import { zodSelectorFunction } from "../selector"; + + +export const parseZodOptionalDef: ParseFunction = (def, refs)=>{ + const parsedInner = zodSelectorFunction(def.innerType._def, refs) + return { + ...parsedInner, + optional: true, + } +} \ No newline at end of file diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodStringDef.ts b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodStringDef.ts new file mode 100644 index 0000000..e900731 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/parsers/parseZodStringDef.ts @@ -0,0 +1,10 @@ +import { ParseFunction, StringNode } from "../../../parsed-node-types"; +import { ZodStringDef } from "zod"; + +export const parseZodStringDef: ParseFunction = (_, refs)=>{ + return { + type: 'string', + path: refs.path, + optional: refs.optional + } +} \ No newline at end of file diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/selector.ts b/packages/trpc-panel/src/parse/input-mappers/zod/selector.ts new file mode 100644 index 0000000..a28b31c --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/selector.ts @@ -0,0 +1,73 @@ +import { ParsedInputNode } from "../../parsed-node-types"; +import { + z, + ZodArrayDef, + ZodBooleanDef, + ZodEnumDef, + ZodFirstPartyTypeKind, + ZodLiteralDef, + ZodNumberDef, + ZodObjectDef, + ZodOptionalDef, + ZodStringDef, +} from "zod"; +import { parseZodStringDef } from "./parsers/parseZodStringDef"; +import { ParserSelectorFunction } from "../../parsed-node-types"; +import { ZodDefWithType } from "./zod-types"; +import { parseZodArrayDef } from "./parsers/parseZodArrayDef"; +import { parseZodBooleanFieldDef } from "./parsers/parseZodBooleanFieldDef"; +import { + parseZodDiscriminatedUnionDef, + ZodDiscriminatedUnionDefUnversioned, +} from "./parsers/parseZodDiscriminatedUnionDef"; +import { parseZodEnumDef } from "./parsers/parseZodEnumDef"; +import { parseZodLiteralDef } from "./parsers/parseZodLiteralDef"; +import { parseZodNumberDef } from "./parsers/parseZodNumberDef"; +import { parseZodObjectDef } from "./parsers/parseZodObjectDef"; +import { parseZodOptionalDef } from "src/parse/input-mappers/zod/parsers/parseZodOptionalDef"; + +export const zodSelectorFunction: ParserSelectorFunction = ( + def, + references +) => { + // const optional = isZodOptional(zodAny); + // const unwrappedOptional = optional ? zodAny._def.innerType : zodAny; + // Please keep these in alphabetical order + switch (def.typeName) { + case ZodFirstPartyTypeKind.ZodArray: + return parseZodArrayDef(def as ZodArrayDef, references); + case ZodFirstPartyTypeKind.ZodBoolean: + return parseZodBooleanFieldDef(def as ZodBooleanDef, references); + case ZodFirstPartyTypeKind.ZodDiscriminatedUnion: + return parseZodDiscriminatedUnionDef( + // Zod had some type changes between 3.19 -> 3.20 and we want to support both, not sure there's a way + // to avoid this. + def as unknown as ZodDiscriminatedUnionDefUnversioned, + references + ); + case ZodFirstPartyTypeKind.ZodEnum: + return parseZodEnumDef(def as ZodEnumDef, references); + case ZodFirstPartyTypeKind.ZodLiteral: + return parseZodLiteralDef(def as ZodLiteralDef, references); + case ZodFirstPartyTypeKind.ZodNumber: + return parseZodNumberDef(def as ZodNumberDef, references); + case ZodFirstPartyTypeKind.ZodObject: + return parseZodObjectDef(def as ZodObjectDef, references); + case ZodFirstPartyTypeKind.ZodOptional: + return parseZodOptionalDef(def as ZodOptionalDef, references); + case ZodFirstPartyTypeKind.ZodString: + return parseZodStringDef(def as ZodStringDef, references); + } + return { type: "unsupported", path: references.path, optional: false }; +}; + +export function mapZodObjectToNode( + object: z.AnyZodObject +): ParsedInputNode | null { + const parsed = zodSelectorFunction(object._def, { + path: [], + optional: false, + options: {}, + }); + return parsed; +} diff --git a/packages/trpc-panel/src/parse/input-mappers/zod/zod-types.ts b/packages/trpc-panel/src/parse/input-mappers/zod/zod-types.ts new file mode 100644 index 0000000..d6288f5 --- /dev/null +++ b/packages/trpc-panel/src/parse/input-mappers/zod/zod-types.ts @@ -0,0 +1,3 @@ +import { ZodFirstPartyTypeKind, ZodTypeDef } from "zod"; + +export type ZodDefWithType = ZodTypeDef & {typeName: ZodFirstPartyTypeKind} \ No newline at end of file diff --git a/packages/trpc-panel/src/parse/parse-router.ts b/packages/trpc-panel/src/parse/parse-router.ts index 5f5c30d..ff67850 100644 --- a/packages/trpc-panel/src/parse/parse-router.ts +++ b/packages/trpc-panel/src/parse/parse-router.ts @@ -1,53 +1,20 @@ import { - RouterOrProcedure, Router, - ProcedureDef, - QueryDef, + Procedure, + isRouter, + isProcedure, + isQueryDef, + isMutationDef, } from "./router-type"; -import { z } from "zod"; -import { mapZodObjectToNode } from "./input-mappers/zod"; +import { AnyZodObject, z } from "zod"; +import { zodSelectorFunction } from "./input-mappers/zod/selector"; import { Router as TRPCRouter } from "@trpc/server"; import { zodToJsonSchema } from "zod-to-json-schema"; -import { logParseError } from "src/parse/parse-error-log"; +import { logParseError } from "./parse-error-log"; +import { ParsedInputNode, ParseReferences } from "./parsed-node-types"; export type JSON7SchemaType = ReturnType; -export type CommonInputType = "string" | "number" | "boolean"; - -type SharedInputNodeProperties = { - path: (string | number)[]; - optional: boolean; -}; - -export type ParsedInputNode = - | ( - | { - type: CommonInputType; - } - | { - type: "array"; - childType: ParsedInputNode; - } - | { - type: "object"; - children: { [name: string]: ParsedInputNode }; - } - | { - type: "enum"; - enumValues: string[]; - } - | { - type: "discriminated-union"; - discriminatedUnionValues: string[]; - discriminatedUnionChildrenMap: { - [value: string]: ParsedInputNode; - }; - discriminatorName: string; - } - | { type: "literal"; value: string | boolean | number | bigint } - ) & - SharedInputNodeProperties; - export type ProcedureType = "query" | "mutation"; export type ParsedProcedure = { @@ -68,86 +35,141 @@ export type ParsedRouter = { nodeType: "router"; }; -function isRouter( - routerOrProcedure: RouterOrProcedure -): routerOrProcedure is Router { - return "router" in routerOrProcedure._def; -} +export type ParseRouterRefs = { + path: string[]; +}; -function isZodObject(maybeZodObject: any): maybeZodObject is z.ZodObject { - return ( - typeof maybeZodObject === "object" && - maybeZodObject && - "_def" in maybeZodObject && - maybeZodObject["_def"]?.typeName === "ZodObject" - ); -} +type SupportedInputType = "zod"; -const ignoreRouterKeys = new Set(["_def", "createCaller", "getErrorShape"]); +const inputParserMap = { + zod: (zodObject: AnyZodObject, refs: ParseReferences) => { + return zodSelectorFunction(zodObject._def, refs); + }, +}; + +const jsonSchemaParserMap = { + zod: zodToJsonSchema, +}; -function isQueryDef(_def: ProcedureDef): _def is QueryDef { - return "query" in _def; +function inputType(_: unknown): SupportedInputType | "unsupported" { + return "zod"; } -export function parseNode( - routerOrProcedure: RouterOrProcedure, - currentNodePath: string[], - parseRouterOptions: TrpcPanelExtraOptions -): ParsedRouter | ParsedProcedure | null { - if (isRouter(routerOrProcedure)) { - const children: ParsedRouterChildren = {}; - for (var path in routerOrProcedure) { - // Only process routes - if (ignoreRouterKeys.has(path)) continue; - const parsedNode = parseNode( - routerOrProcedure[path]!, - currentNodePath.concat([path]), - parseRouterOptions - ); - if (!parsedNode) { - // Would've already logged the error so just skip - - continue; +// Some things in the router are not procedures, these are those things keys +const skipSet = new Set(["createCaller", "_def", "getErrorShape"]); + +function parseRouter( + router: Router, + routerPath: string[], + options: TrpcPanelExtraOptions +): ParsedRouter { + const children: ParsedRouterChildren = {}; + var hasChild = false; + // .procedures contains procedures and routers + for (var [procedureOrRouterPath, child] of Object.entries(router)) { + if (skipSet.has(procedureOrRouterPath)) continue; + const newPath = routerPath.concat([procedureOrRouterPath]); + const parsedNode = (() => { + if (isRouter(child)) { + return parseRouter(child, newPath, options); + } + if (isProcedure(child)) { + return parseProcedure(child, newPath, options); } - children[path] = parsedNode; - } - return { children, nodeType: "router", path: currentNodePath }; - } else { - const { _def } = routerOrProcedure; - const { inputs } = _def; - const zodObjectInputs = inputs.filter((e) => - isZodObject(e) - ) as z.ZodObject[]; - if (inputs.length && zodObjectInputs.length != inputs.length) { - logParseError( - currentNodePath.join("."), - "found non ZodObject input." - ); return null; + })(); + if (!parsedNode) { + logParseError(newPath.join("."), "Couldn't parse node."); + continue; } - const mergedZodObject = zodObjectInputs.reduce( - (a, b) => a.merge(b), - z.object({}) + hasChild = true; + children[procedureOrRouterPath] = parsedNode; + } + if (!hasChild) + logParseError( + routerPath.join("."), + `Router doesn't have any successfully parsed children.` ); - const node = mapZodObjectToNode(mergedZodObject); - - if (!node) { - logParseError( - currentNodePath.join("."), - "contained unsupported zod type." - ); - return null; - } + return { children, nodeType: "router", path: routerPath }; +} +type NodeAndInputSchemaFromInputs = + | { + node: ParsedInputNode; + schema: ReturnType; + parseInputResult: "success"; + } + | { + parseInputResult: "failure"; + }; + +const emptyZodObject = z.object({}); +function nodeAndInputSchemaFromInputs( + inputs: unknown[], + _routerPath: string[], + options: TrpcPanelExtraOptions +): NodeAndInputSchemaFromInputs { + if (!inputs.length) { return { - // This is used to validate the form - inputSchema: zodToJsonSchema(mergedZodObject), - // This is used to build the UI - node, - nodeType: "procedure", - procedureType: isQueryDef(_def) ? "query" : "mutation", - pathFromRootRouter: currentNodePath, + parseInputResult: "success", + schema: zodToJsonSchema(emptyZodObject), + node: inputParserMap["zod"](emptyZodObject, { + path: [], + optional: false, + options, + }), }; } + if (inputs.length !== 1) { + return { parseInputResult: "failure" }; + } + const input = inputs[0]; + const iType = inputType(input); + if (iType == "unsupported") { + return { parseInputResult: "failure" }; + } + const jsonSchemaParser = jsonSchemaParserMap[iType]; + + return { + parseInputResult: "success", + schema: jsonSchemaParser(input as any), // + node: zodSelectorFunction((input as any)._def, { + path: [], + options, + optional: false, + }), + }; +} + +function parseProcedure( + procedure: Procedure, + path: string[], + options: TrpcPanelExtraOptions +): ParsedProcedure | null { + const { _def } = procedure; + const { inputs } = _def; + + const nodeAndInput = nodeAndInputSchemaFromInputs(inputs, path, options); + if (nodeAndInput.parseInputResult === "failure") { + return null; + } + + const t = (() => { + if (isQueryDef(_def)) return "query"; + if (isMutationDef(_def)) return "mutation"; + return null; + })(); + + if (!t) { + return null; + } + + return { + inputSchema: nodeAndInput.schema, + node: nodeAndInput.node, + nodeType: "procedure", + procedureType: t, + pathFromRootRouter: path, + }; } export type TrpcPanelExtraOptions = { @@ -155,9 +177,12 @@ export type TrpcPanelExtraOptions = { transformer?: "superjson"; }; -export function parseRouter( +export function parseRouterWithOptions( router: TRPCRouter, parseRouterOptions: TrpcPanelExtraOptions ) { - return parseNode(router, [], parseRouterOptions) as ParsedRouter; + if (!isRouter(router)) { + throw new Error("Non trpc router passed to trpc panel."); + } + return parseRouter(router, [], parseRouterOptions); } diff --git a/packages/trpc-panel/src/parse/parsed-node-types.ts b/packages/trpc-panel/src/parse/parsed-node-types.ts new file mode 100644 index 0000000..3229ca9 --- /dev/null +++ b/packages/trpc-panel/src/parse/parsed-node-types.ts @@ -0,0 +1,79 @@ +import { ZodTypeDef } from "zod"; +import { ZodDiscriminatedUnionDefUnversioned } from "./input-mappers/zod/parsers/parseZodDiscriminatedUnionDef"; +import { TrpcPanelExtraOptions } from "./parse-router"; + +type SharedInputNodeProperties = { + path: (string | number)[]; + optional: boolean; +}; + +type InputNodeTypes = ZodTypeDef | ZodDiscriminatedUnionDefUnversioned; + +export type ArrayNode = { + type: "array"; + childType: ParsedInputNode; +} & SharedInputNodeProperties; + +export type ObjectNode = { + type: "object"; + children: { [name: string]: ParsedInputNode }; +} & SharedInputNodeProperties; + +export type EnumNode = { + type: "enum"; + enumValues: string[]; +} & SharedInputNodeProperties; + +export type DiscriminatedUnionNode = { + type: "discriminated-union"; + discriminatedUnionValues: string[]; + discriminatedUnionChildrenMap: { + [value: string]: ParsedInputNode; + }; + discriminatorName: string; +} & SharedInputNodeProperties; + +export type LiteralNode = { + type: "literal"; + value: string | boolean | number | bigint; +} & SharedInputNodeProperties; + +export type StringNode = { + type: "string"; +} & SharedInputNodeProperties; + +export type NumberNode = { type: "number" } & SharedInputNodeProperties; + +export type BooleanNode = { type: "boolean" } & SharedInputNodeProperties; + +export type UnsupportedNode = { + type: "unsupported"; +} & SharedInputNodeProperties; + +export type ParsedInputNode = + | ArrayNode + | ObjectNode + | EnumNode + | DiscriminatedUnionNode + | LiteralNode + | StringNode + | NumberNode + | BooleanNode + | UnsupportedNode; + +export type ParseReferences = { + path: string[]; + optional: boolean; + // Doesn't do anything yet but maybe down the road we can extend with this + options: TrpcPanelExtraOptions; +}; + +export type ParseFunction< + InputNodeType extends InputNodeTypes, + ParsedNodeType extends ParsedInputNode +> = (def: InputNodeType, references: ParseReferences) => ParsedNodeType; + +export type ParserSelectorFunction = ( + inputNode: InputNodeType, + references: ParseReferences +) => ParsedInputNode; diff --git a/packages/trpc-panel/src/parse/router-type.ts b/packages/trpc-panel/src/parse/router-type.ts index 97032a8..32a3cb6 100644 --- a/packages/trpc-panel/src/parse/router-type.ts +++ b/packages/trpc-panel/src/parse/router-type.ts @@ -1,31 +1,85 @@ -export type ProcedureSharedProperties = { - inputs: unknown[]; - meta: unknown; -}; +import { z } from "zod"; -export type RouterDef = { - router: true; - procedures: any; -} & { [path: string]: RouterOrProcedure }; +const ZodObjectSchema = z.object({}); + +export function isZodObject( + obj: unknown +): obj is z.infer { + return ZodObjectSchema.safeParse(obj).success; +} + +const SharedProcedureDefPropertiesSchema = z.object({ + inputs: z.unknown().array(), + meta: z.unknown(), +}); + +const QueryDefSchema = SharedProcedureDefPropertiesSchema.merge( + z.object({ + query: z.literal(true), + }) +); -export type QueryDef = { - query: true; -} & ProcedureSharedProperties; +export function isQueryDef(obj: unknown): obj is QueryDef { + return QueryDefSchema.safeParse(obj).success; +} -export type MutationDef = { - mutation: true; -} & ProcedureSharedProperties; +type QueryDef = z.infer; -export type RouterOrProducedureDef = RouterDef | QueryDef | MutationDef; +const MutationDefSchema = SharedProcedureDefPropertiesSchema.merge( + z.object({ + mutation: z.literal(true), + }) +); -export type ProcedureDef = QueryDef | MutationDef; +export function isMutationDef(obj: unknown): obj is MutationDef { + return MutationDefSchema.safeParse(obj).success; +} + +export type MutationDef = z.infer; + +export const ProcedureDefSchema = QueryDefSchema.or(MutationDefSchema); + +export type ProcedureDefSharedProperties = z.infer< + typeof SharedProcedureDefPropertiesSchema +>; + +// Don't export this b/c it's just used to type check, use the is functions +const RouterDefSchema = z.object({ + router: z.literal(true), +}); + +export type RouterDef = { + router: true; + procedures: Record; +}; export type Router = { _def: RouterDef; -} & { [pathName: string]: RouterOrProcedure }; +} & { [key: string]: Router | Procedure }; -export type Procedure = { - _def: ProcedureDef; -}; +const RouterSchema = z.object({ + _def: RouterDefSchema, +}); + +export function isRouter(obj: unknown): obj is Router { + return RouterSchema.safeParse(obj).success; +} + +const ProcedureSchema = z.object({ + _def: ProcedureDefSchema, +}); + +export type Procedure = z.infer; + +export function isProcedure(obj: unknown | Function): obj is Procedure { + if (typeof obj !== "function" || !("_def" in obj)) return false; + return ProcedureDefSchema.safeParse((obj as any)._def).success; +} + +const QuerySchema = z.object({ + _def: QueryDefSchema, +}); + +export type Query = z.infer; export type RouterOrProcedure = Router | Procedure; diff --git a/packages/trpc-panel/src/react-app/components/form/Field.tsx b/packages/trpc-panel/src/react-app/components/form/Field.tsx index 49bf92b..8379fd7 100644 --- a/packages/trpc-panel/src/react-app/components/form/Field.tsx +++ b/packages/trpc-panel/src/react-app/components/form/Field.tsx @@ -1,14 +1,14 @@ import React from "react"; import { Control } from "react-hook-form"; -import { ParsedInputNode } from "src/parse/parse-router"; -import { ArrayField } from "src/react-app/components/form/fields/ArrayField"; -import { BooleanField } from "src/react-app/components/form/fields/BooleanField"; -import { DiscriminatedUnionField } from "src/react-app/components/form/fields/DiscriminatedUnionField"; -import { EnumField } from "src/react-app/components/form/fields/EnumField"; -import { LiteralField } from "src/react-app/components/form/fields/LiteralField"; -import { NumberField } from "src/react-app/components/form/fields/NumberField"; -import { ObjectField } from "src/react-app/components/form/fields/ObjectField"; -import { TextField } from "src/react-app/components/form/fields/TextField"; +import { ParsedInputNode } from "../../../parse/parsed-node-types"; +import { ArrayField } from "./fields/ArrayField"; +import { BooleanField } from "./fields/BooleanField"; +import { DiscriminatedUnionField } from "./fields/DiscriminatedUnionField"; +import { EnumField } from "./fields/EnumField"; +import { LiteralField } from "./fields/LiteralField"; +import { NumberField } from "./fields/NumberField"; +import { ObjectField } from "./fields/ObjectField"; +import { TextField } from "./fields/TextField"; export function Field({ inputNode, @@ -55,5 +55,7 @@ export function Field({ ); case "literal": return ; + case "unsupported": + return null; } } diff --git a/packages/trpc-panel/src/react-app/components/form/ProcedureForm/index.tsx b/packages/trpc-panel/src/react-app/components/form/ProcedureForm/index.tsx index af3837c..fcc2825 100644 --- a/packages/trpc-panel/src/react-app/components/form/ProcedureForm/index.tsx +++ b/packages/trpc-panel/src/react-app/components/form/ProcedureForm/index.tsx @@ -1,9 +1,6 @@ import React, { useEffect, useState } from "react"; import { Control, useForm, useFormState } from "react-hook-form"; -import { - ParsedInputNode, - ParsedProcedure, -} from "../../../../parse/parse-router"; +import { ParsedProcedure } from "../../../../parse/parse-router"; import { ajvResolver } from "@hookform/resolvers/ajv"; import { defaultFormValuesForNode } from "src/react-app/components/form/utils"; import { trpc } from "src/react-app/trpc"; @@ -18,6 +15,7 @@ import { CollapsableSection } from "src/react-app/components/CollapsableSection" import { CloseIcon } from "src/react-app/components/icons/CloseIcon"; import { ObjectField } from "src/react-app/components/form/fields/ObjectField"; import { fullFormats } from "ajv-formats/dist/formats"; +import type { ParsedInputNode } from "src/parse/parsed-node-types"; const TRPCErrorSchema = z.object({ shape: z.object({ @@ -127,7 +125,6 @@ export function ProcedureForm({ procedure.procedureType === "query" ? query.data : mutationResponse; const error = procedure.procedureType == "query" ? query.error : mutation.error; - return ( } diff --git a/packages/trpc-panel/src/react-app/components/form/fields/BooleanField.tsx b/packages/trpc-panel/src/react-app/components/form/fields/BooleanField.tsx index 04f3fb3..b580c69 100644 --- a/packages/trpc-panel/src/react-app/components/form/fields/BooleanField.tsx +++ b/packages/trpc-panel/src/react-app/components/form/fields/BooleanField.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Control, useController } from "react-hook-form"; -import { ParsedInputNode } from "src/parse/parse-router"; +import type { ParsedInputNode } from "src/parse/parsed-node-types"; import { BaseCheckboxField } from "src/react-app/components/form/fields/base/BaseCheckboxField"; export function BooleanField({ diff --git a/packages/trpc-panel/src/react-app/components/form/fields/DiscriminatedUnionField.tsx b/packages/trpc-panel/src/react-app/components/form/fields/DiscriminatedUnionField.tsx index 10ff6d3..452a0fd 100644 --- a/packages/trpc-panel/src/react-app/components/form/fields/DiscriminatedUnionField.tsx +++ b/packages/trpc-panel/src/react-app/components/form/fields/DiscriminatedUnionField.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Control, useController } from "react-hook-form"; -import { ParsedInputNode } from "src/parse/parse-router"; +import type { ParsedInputNode } from "src/parse/parsed-node-types"; import { BaseSelectField } from "src/react-app/components/form/fields/base/BaseSelectField"; import { ObjectField } from "src/react-app/components/form/fields/ObjectField"; import { defaultFormValuesForNode } from "src/react-app/components/form/utils"; diff --git a/packages/trpc-panel/src/react-app/components/form/fields/NumberField.tsx b/packages/trpc-panel/src/react-app/components/form/fields/NumberField.tsx index f51bad3..d4da585 100644 --- a/packages/trpc-panel/src/react-app/components/form/fields/NumberField.tsx +++ b/packages/trpc-panel/src/react-app/components/form/fields/NumberField.tsx @@ -1,7 +1,7 @@ import { Control, useController } from "react-hook-form"; import React, { useEffect, useState } from "react"; import { BaseTextField } from "./base/BaseTextField"; -import { ParsedInputNode } from "src/parse/parse-router"; +import type { ParsedInputNode } from "src/parse/parsed-node-types"; export function NumberField({ name, diff --git a/packages/trpc-panel/src/react-app/components/form/fields/ObjectField.tsx b/packages/trpc-panel/src/react-app/components/form/fields/ObjectField.tsx index b700a72..aa331d8 100644 --- a/packages/trpc-panel/src/react-app/components/form/fields/ObjectField.tsx +++ b/packages/trpc-panel/src/react-app/components/form/fields/ObjectField.tsx @@ -1,6 +1,6 @@ import React, { ReactNode } from "react"; import { Control } from "react-hook-form"; -import { ParsedInputNode } from "src/parse/parse-router"; +import type { ParsedInputNode } from "src/parse/parsed-node-types"; import { Field } from "src/react-app/components/form/Field"; import ObjectIcon from "@mui/icons-material/DataObjectOutlined"; import { InputGroupContainer } from "../../InputGroupContainer"; @@ -27,7 +27,6 @@ export function ObjectField({ ); } - return ( , options: RenderOptions) { { searchFor: routerReplaceSymbol, injectString: JSON.stringify( - parseRouter(router, { + parseRouterWithOptions(router, { ...defaultParseRouterOptions, ...options, }) diff --git a/packages/trpc-panel/tsconfig.json b/packages/trpc-panel/tsconfig.json index 36723f5..9627e71 100644 --- a/packages/trpc-panel/tsconfig.json +++ b/packages/trpc-panel/tsconfig.json @@ -10,9 +10,8 @@ "target": "ES6", "declaration": true, "module": "ES2022", - + "baseUrl": "./", "rootDir": "./", - "baseUrl": "./", "esModuleInterop": true, "moduleResolution": "node", "paths": {},