diff --git a/examples/react-app/petstore.yaml b/examples/react-app/petstore.yaml index ad15f64..cfe7026 100644 --- a/examples/react-app/petstore.yaml +++ b/examples/react-app/petstore.yaml @@ -77,6 +77,19 @@ paths: application/json: schema: $ref: '#/components/schemas/Error' + /not-defined: + get: + deprecated: true + description: This path is not fully defined. + responses: + default: + description: unexpected error + post: + deprecated: true + description: This path is not defined at all. + responses: + default: + description: unexpected error /pets/{id}: get: description: Returns a user based on a single ID, if the user does not have access to the pet diff --git a/examples/react-app/src/App.tsx b/examples/react-app/src/App.tsx index 220db20..41ee5ff 100644 --- a/examples/react-app/src/App.tsx +++ b/examples/react-app/src/App.tsx @@ -3,6 +3,8 @@ import { useDefaultClientAddPet, useDefaultClientFindPets, useDefaultClientFindPetsKey, + useDefaultClientGetNotDefined, + useDefaultClientPostNotDefined, } from "../openapi/queries"; import { useState } from "react"; import { queryClient } from "./queryClient"; @@ -13,6 +15,12 @@ function App() { const { data, error, refetch } = useDefaultClientFindPets({ tags, limit }); + // This is an example of a query that is not defined in the OpenAPI spec + // this defaults to any - here we are showing how to override the type + // Note - this is marked as deprecated in the OpenAPI spec and being passed to the client + const { data: notDefined } = useDefaultClientGetNotDefined(); + const { mutate: mutateNotDefined } = useDefaultClientPostNotDefined(); + const { mutate: addPet } = useDefaultClientAddPet(); if (error) diff --git a/src/createExports.ts b/src/createExports.ts index e57e904..d4d25e1 100644 --- a/src/createExports.ts +++ b/src/createExports.ts @@ -1,4 +1,4 @@ -import ts from "typescript"; +import ts, { JSDoc } from "typescript"; import { sync } from "glob"; import { join } from "path"; import fs from "fs"; @@ -45,9 +45,27 @@ export const createExports = (generatedClientsPath: string) => { const httpMethodName = properties .find((p) => p.name?.getText(node) === "method") ?.initializer?.getText(node)!; + + + const getAllChildren = (tsNode: ts.Node): Array => { + const childItems = tsNode.getChildren(node); + if (childItems.length) { + const allChildren = childItems.map(getAllChildren); + return [tsNode].concat(allChildren.flat()); + } + return [tsNode]; + } + + const children = getAllChildren(method); + const jsDoc = children.filter((c) => c.kind === ts.SyntaxKind.JSDoc).map((c) => { + return (c as JSDoc).comment + }); + const hasDeprecated = children + .some((c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag); + return httpMethodName === "'GET'" - ? createUseQuery(node, className, method) - : createUseMutation(node, className, method); + ? createUseQuery(node, className, method, jsDoc, hasDeprecated) + : createUseMutation(node, className, method, jsDoc, hasDeprecated); }) .flat(); }) diff --git a/src/createUseMutation.ts b/src/createUseMutation.ts index 71832c2..c673696 100644 --- a/src/createUseMutation.ts +++ b/src/createUseMutation.ts @@ -1,10 +1,13 @@ import ts from "typescript"; import { capitalizeFirstLetter } from "./common"; +import { addJSDocToNode } from "./util"; export const createUseMutation = ( node: ts.SourceFile, className: string, - method: ts.MethodDeclaration + method: ts.MethodDeclaration, + jsDoc: (string | ts.NodeArray | undefined)[] = [], + deprecated: boolean = false ) => { const methodName = method.name?.getText(node)!; // Awaited> @@ -26,11 +29,22 @@ export const createUseMutation = ( ] ); + const TData = ts.factory.createIdentifier("TData"); + const TError = ts.factory.createIdentifier("TError"); + const TContext = ts.factory.createIdentifier("TContext"); + + const mutationResult = ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier(`${className}${methodName}MutationResult`), + undefined, + awaitedResponseDataType + ); + const responseDataType = ts.factory.createTypeParameterDeclaration( undefined, - "TData", + TData, undefined, - awaitedResponseDataType + ts.factory.createTypeReferenceNode(mutationResult.name) ); const methodParameters = @@ -49,7 +63,7 @@ export const createUseMutation = ( ) : ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); - return ts.factory.createVariableStatement( + const exportHook = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList( [ @@ -65,13 +79,13 @@ export const createUseMutation = ( responseDataType, ts.factory.createTypeParameterDeclaration( undefined, - "TError", + TError, undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) ), ts.factory.createTypeParameterDeclaration( undefined, - "TContext", + TContext, undefined, ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) ), @@ -88,14 +102,10 @@ export const createUseMutation = ( ts.factory.createTypeReferenceNode( ts.factory.createIdentifier("UseMutationOptions"), [ - awaitedResponseDataType, - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword - ), + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), methodParameters, - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword - ), + ts.factory.createTypeReferenceNode(TContext), ] ), ts.factory.createLiteralTypeNode( @@ -108,101 +118,79 @@ export const createUseMutation = ( ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createAsExpression( - ts.factory.createCallExpression( - ts.factory.createIdentifier("useMutation"), - undefined, - [ - ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier("mutationFn"), - ts.factory.createArrowFunction( - undefined, - undefined, - method.parameters.length !== 0 - ? [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createObjectBindingPattern( - method.parameters.map((param) => { - return ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier( - param.name.getText(node) - ), - undefined - ); - }) - ), - undefined, - undefined, - undefined + ts.factory.createCallExpression( + ts.factory.createIdentifier("useMutation"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + methodParameters, + ts.factory.createTypeReferenceNode(TContext), + ], + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("mutationFn"), + ts.factory.createArrowFunction( + undefined, + undefined, + method.parameters.length !== 0 + ? [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createObjectBindingPattern( + method.parameters.map((param) => { + return ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier( + param.name.getText(node) + ), + undefined + ); + }) ), - ] - : [], - undefined, - ts.factory.createToken( - ts.SyntaxKind.EqualsGreaterThanToken - ), - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(className), - ts.factory.createIdentifier(methodName) - ), - undefined, - method.parameters.map((params) => - ts.factory.createIdentifier( - params.name.getText(node) + undefined, + undefined, + undefined + ), + ] + : [], + undefined, + ts.factory.createToken( + ts.SyntaxKind.EqualsGreaterThanToken + ), + ts.factory.createAsExpression( + ts.factory.createAsExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(className), + ts.factory.createIdentifier(methodName) + ), + undefined, + method.parameters.map((params) => + ts.factory.createIdentifier( + params.name.getText(node) + ) ) + ), + ts.factory.createKeywordTypeNode( + ts.SyntaxKind.UnknownKeyword ) - ) - ) - ), - ts.factory.createSpreadAssignment( - ts.factory.createIdentifier("options") - ), - ]), - ] - ), - // Omit>, TError, params, TContext>, 'data'> & { data: TData }; - ts.factory.createIntersectionTypeNode([ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Omit"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseMutationResult"), - [ - awaitedResponseDataType, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("TError"), - undefined ), - methodParameters, + ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("TContext"), - undefined - ), - ] - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("data") - ), - ] - ), - ts.factory.createTypeLiteralNode([ - ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier("data"), - undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("TData"), - undefined + ts.factory.createIdentifier("Promise"), + [ts.factory.createTypeReferenceNode(TData)] + ) + ) ) ), + ts.factory.createSpreadAssignment( + ts.factory.createIdentifier("options") + ), ]), - ]) + ] ) ) ), @@ -210,4 +198,8 @@ export const createUseMutation = ( ts.NodeFlags.Const ) ); + + const hookWithJsDoc = addJSDocToNode(exportHook, node, deprecated, jsDoc); + + return [mutationResult, hookWithJsDoc]; }; diff --git a/src/createUseQuery.ts b/src/createUseQuery.ts index 90dde10..3c06227 100644 --- a/src/createUseQuery.ts +++ b/src/createUseQuery.ts @@ -1,10 +1,13 @@ import ts from "typescript"; import { capitalizeFirstLetter } from "./common"; +import { addJSDocToNode } from "./util"; export const createUseQuery = ( node: ts.SourceFile, className: string, - method: ts.MethodDeclaration + method: ts.MethodDeclaration, + jsDoc: (string | ts.NodeArray | undefined)[] = [], + deprecated: boolean = false ) => { const methodName = method.name?.getText(node)!; let requestParam = []; @@ -43,10 +46,7 @@ export const createUseQuery = ( const customHookName = `use${className}${capitalizeFirstLetter(methodName)}`; const queryKey = `${customHookName}Key`; - const queryKeyGenericType = ts.factory.createTypeReferenceNode( - "TQueryKey", - undefined - ); + const queryKeyGenericType = ts.factory.createTypeReferenceNode("TQueryKey"); const queryKeyConstraint = ts.factory.createTypeReferenceNode("Array", [ ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), ]); @@ -69,219 +69,228 @@ export const createUseQuery = ( ), ] ); + // DefaultResponseDataType + // export type MyClassMethodDefaultResponse = Awaited> + const defaultApiResponse = ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier( + `${capitalizeFirstLetter(className)}${capitalizeFirstLetter( + methodName + )}DefaultResponse` + ), + undefined, + awaitedResponseDataType + ); + + const TData = ts.factory.createIdentifier("TData"); + const TError = ts.factory.createIdentifier("TError"); const responseDataType = ts.factory.createTypeParameterDeclaration( undefined, - "TData", + TData.text, undefined, - awaitedResponseDataType + ts.factory.createTypeReferenceNode(defaultApiResponse.name) ); - return [ - // QueryKey - ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(queryKey), - undefined, - undefined, - ts.factory.createStringLiteral( - `${className}${capitalizeFirstLetter(methodName)}` - ) - ), - ], - ts.NodeFlags.Const - ) + // Return Type + // export const classNameMethodNameQueryResult = UseQueryResult; + const returnTypeExport = ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier( + `${capitalizeFirstLetter(className)}${capitalizeFirstLetter( + methodName + )}QueryResult` ), - // Custom hook - ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(customHookName), - undefined, + [ + ts.factory.createTypeParameterDeclaration( + undefined, + TData, + undefined, + ts.factory.createTypeReferenceNode(defaultApiResponse.name) + ), + ts.factory.createTypeParameterDeclaration( + undefined, + TError, + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + ), + ], + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("UseQueryResult"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ], + ), + ); + + // QueryKey + const queryKeyExport = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(queryKey), + undefined, + undefined, + ts.factory.createStringLiteral( + `${className}${capitalizeFirstLetter(methodName)}` + ) + ), + ], + ts.NodeFlags.Const + ) + ); + + // Custom hook + const hookExport = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(customHookName), + undefined, + undefined, + ts.factory.createArrowFunction( undefined, - ts.factory.createArrowFunction( - undefined, - ts.factory.createNodeArray([ - ts.factory.createTypeParameterDeclaration( - undefined, - "TQueryKey", - queryKeyConstraint, - ts.factory.createArrayTypeNode( - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword - ) - ) - ), - responseDataType, - ts.factory.createTypeParameterDeclaration( - undefined, - "TError", - undefined, + ts.factory.createNodeArray([ + responseDataType, + ts.factory.createTypeParameterDeclaration( + undefined, + TError, + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) + ), + ts.factory.createTypeParameterDeclaration( + undefined, + "TQueryKey", + queryKeyConstraint, + ts.factory.createArrayTypeNode( ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) - ), - ]), - [ - ...requestParam, - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("queryKey"), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - queryKeyGenericType - ), - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("options"), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Omit"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseQueryOptions"), - [ - awaitedResponseDataType, - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword - ), - awaitedResponseDataType, - ts.factory.createArrayTypeNode( - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword - ) - ), - ] - ), - ts.factory.createUnionTypeNode([ - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("queryKey") - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("queryFn") - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("initialData") - ), - ]), - ] - ) - ), - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createAsExpression( - ts.factory.createCallExpression( - ts.factory.createIdentifier("useQuery"), - undefined, + ) + ), + ]), + [ + ...requestParam, + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("queryKey"), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + queryKeyGenericType + ), + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("options"), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Omit"), [ - ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier("queryKey"), - ts.factory.createArrayLiteralExpression( - [ - ts.factory.createIdentifier(queryKey), - ts.factory.createSpreadElement( - ts.factory.createParenthesizedExpression( - ts.factory.createBinaryExpression( - ts.factory.createIdentifier("queryKey"), - ts.factory.createToken( - ts.SyntaxKind.QuestionQuestionToken - ), - method.parameters.length - ? ts.factory.createArrayLiteralExpression([ - ts.factory.createObjectLiteralExpression( - method.parameters.map((param) => - ts.factory.createShorthandPropertyAssignment( - ts.factory.createIdentifier( - param.name.getText(node) - ) - ) - ) - ), - ]) - : ts.factory.createArrayLiteralExpression( - [] - ) - ) - ) - ), - ], - false - ) + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("UseQueryOptions"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ] + ), + ts.factory.createUnionTypeNode([ + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("queryKey") ), - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier("queryFn"), - ts.factory.createArrowFunction( - undefined, - undefined, - [], - undefined, - ts.factory.createToken( - ts.SyntaxKind.EqualsGreaterThanToken - ), - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(className), - ts.factory.createIdentifier(methodName) - ), - undefined, - method.parameters.map((param) => - ts.factory.createIdentifier( - param.name.getText(node) - ) - ) - ) - ) + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("queryFn") ), - ts.factory.createSpreadAssignment( - ts.factory.createIdentifier("options") + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("initialData") ), ]), ] - ), - // Omit>, TError>, 'data'> & { data: TData }; - ts.factory.createIntersectionTypeNode([ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Omit"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseQueryResult"), - [ - awaitedResponseDataType, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("TError"), - undefined - ), - ] - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("data") - ), - ] + ) + ), + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createIdentifier("useQuery"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ], + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("queryKey"), + ts.factory.createArrayLiteralExpression( + [ + ts.factory.createIdentifier(queryKey), + ts.factory.createSpreadElement( + ts.factory.createParenthesizedExpression( + ts.factory.createBinaryExpression( + ts.factory.createIdentifier("queryKey"), + ts.factory.createToken( + ts.SyntaxKind.QuestionQuestionToken + ), + method.parameters.length + ? ts.factory.createArrayLiteralExpression([ + ts.factory.createObjectLiteralExpression( + method.parameters.map((param) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier( + param.name.getText(node) + ) + ) + ) + ), + ]) + : ts.factory.createArrayLiteralExpression([]) + ) + ) + ), + ], + false + ) ), - ts.factory.createTypeLiteralNode([ - ts.factory.createPropertySignature( + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("queryFn"), + ts.factory.createArrowFunction( undefined, - ts.factory.createIdentifier("data"), undefined, - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("TData"), - undefined + [], + undefined, + ts.factory.createToken( + ts.SyntaxKind.EqualsGreaterThanToken + ), + ts.factory.createAsExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(className), + ts.factory.createIdentifier(methodName) + ), + undefined, + method.parameters.map((param) => + ts.factory.createIdentifier( + param.name.getText(node) + ) + ) + ), + ts.factory.createTypeReferenceNode(TData) ) - ), - ]), - ]) - ) + ) + ), + ts.factory.createSpreadAssignment( + ts.factory.createIdentifier("options") + ), + ]), + ] ) - ), - ], - ts.NodeFlags.Const - ) - ), - ]; + ) + ), + ], + ts.NodeFlags.Const + ) + ); + const hookWithJsDoc = addJSDocToNode(hookExport, node, deprecated, jsDoc); + + return [defaultApiResponse, returnTypeExport, queryKeyExport, hookWithJsDoc]; }; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..8dd7f73 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,44 @@ +import ts from 'typescript'; + +export function addJSDocToNode( + node: T, + sourceFile: ts.SourceFile, + deprecated: boolean, + jsDoc: (string | ts.NodeArray | undefined)[] = [], +): T { + const deprecatedString = deprecated ? "@deprecated" : ""; + + const jsDocString = [deprecatedString] + .concat( + jsDoc.map((comment) => { + if (typeof comment === "string") { + return comment; + } + if (Array.isArray(comment)) { + return comment.map((c) => c.getText(sourceFile)).join("\n"); + } + return ""; + }) + ) + // remove empty lines + .filter(Boolean) + // trim + .map((comment) => comment.trim()) + // add * to each line + .map((comment) => `* ${comment}`) + // join lines + .join("\n") + // replace new lines with \n * + .replace(/\n/g, "\n * "); + + const nodeWithJSDoc = jsDocString + ? ts.addSyntheticLeadingComment( + node, + ts.SyntaxKind.MultiLineCommentTrivia, + `*\n ${jsDocString}\n `, + true + ) + : node; + + return nodeWithJSDoc; +} \ No newline at end of file