From 3a080398d650e993722f3fcecbbfbb671bed7046 Mon Sep 17 00:00:00 2001 From: Landon Gavin Date: Wed, 3 Jan 2024 00:01:16 -0500 Subject: [PATCH] Add JSDoc to mutations --- examples/react-app/petstore.yaml | 6 + examples/react-app/src/App.tsx | 3 + src/createExports.ts | 2 +- src/createUseMutation.ts | 192 +++++++++++++++---------------- src/createUseQuery.ts | 79 ++----------- src/util.ts | 44 +++++++ 6 files changed, 158 insertions(+), 168 deletions(-) create mode 100644 src/util.ts diff --git a/examples/react-app/petstore.yaml b/examples/react-app/petstore.yaml index 6b21d5d..cfe7026 100644 --- a/examples/react-app/petstore.yaml +++ b/examples/react-app/petstore.yaml @@ -84,6 +84,12 @@ paths: 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 9a6ec4e..41ee5ff 100644 --- a/examples/react-app/src/App.tsx +++ b/examples/react-app/src/App.tsx @@ -4,6 +4,7 @@ import { useDefaultClientFindPets, useDefaultClientFindPetsKey, useDefaultClientGetNotDefined, + useDefaultClientPostNotDefined, } from "../openapi/queries"; import { useState } from "react"; import { queryClient } from "./queryClient"; @@ -16,7 +17,9 @@ function App() { // 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(); diff --git a/src/createExports.ts b/src/createExports.ts index eb28674..d4d25e1 100644 --- a/src/createExports.ts +++ b/src/createExports.ts @@ -65,7 +65,7 @@ export const createExports = (generatedClientsPath: string) => { return httpMethodName === "'GET'" ? createUseQuery(node, className, method, jsDoc, hasDeprecated) - : createUseMutation(node, className, method); + : 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 e762f33..d7d137e 100644 --- a/src/createUseQuery.ts +++ b/src/createUseQuery.ts @@ -1,5 +1,6 @@ import ts from "typescript"; import { capitalizeFirstLetter } from "./common"; +import { addJSDocToNode } from './util'; export const createUseQuery = ( node: ts.SourceFile, @@ -69,6 +70,7 @@ export const createUseQuery = ( ] ); // DefaultResponseDataType + // export type MyClassMethodDefaultResponse = Awaited> const defaultApiResponse = ts.factory.createTypeAliasDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier( @@ -90,32 +92,8 @@ export const createUseQuery = ( ts.factory.createTypeReferenceNode(defaultApiResponse.name) ); - // Omit>, TError>, 'data'> & { data: TData|undefined }; - const responseReturnType = ts.factory.createIntersectionTypeNode([ - ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("Omit"), [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseQueryResult"), - [ - defaultApiResponse.type, - ts.factory.createTypeReferenceNode(TError, undefined), - ] - ), - ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral("data")), - ]), - ts.factory.createTypeLiteralNode([ - ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier("data"), - undefined, - ts.factory.createUnionTypeNode([ - ts.factory.createTypeReferenceNode(TData, undefined), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UndefinedKeyword), - ]) - ), - ]), - ]); - // Return Type + // export const classNameMethodNameQueryResult = UseQueryResult; const returnTypeExport = ts.factory.createTypeAliasDeclaration( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createIdentifier( @@ -137,7 +115,13 @@ export const createUseQuery = ( ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword) ), ], - responseReturnType + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("UseQueryResult"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ], + ), ); // QueryKey @@ -208,8 +192,6 @@ export const createUseQuery = ( [ ts.factory.createTypeReferenceNode(TData), ts.factory.createTypeReferenceNode(TError), - ts.factory.createTypeReferenceNode(TData), - queryKeyGenericType, ] ), ts.factory.createUnionTypeNode([ @@ -234,8 +216,6 @@ export const createUseQuery = ( [ ts.factory.createTypeReferenceNode(TData), ts.factory.createTypeReferenceNode(TError), - ts.factory.createTypeReferenceNode(TData), - queryKeyGenericType, ], [ ts.factory.createObjectLiteralExpression([ @@ -313,42 +293,7 @@ export const createUseQuery = ( ts.NodeFlags.Const ) ); + const hookWithJsDoc = addJSDocToNode(hookExport, node, deprecated, jsDoc); - 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(node)) - .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 hookWithJSDoc = jsDocString - ? ts.addSyntheticLeadingComment( - hookExport, - ts.SyntaxKind.MultiLineCommentTrivia, - `*\n ${jsDocString}\n `, - true, - ) - : hookExport; - - return [defaultApiResponse, returnTypeExport, queryKeyExport, hookWithJSDoc]; + 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