From 111f463f2d4629e0c4e0cf1250c0a2bb0521a8f8 Mon Sep 17 00:00:00 2001 From: Monika Bozhinova Date: Wed, 9 Jul 2025 15:47:01 +0200 Subject: [PATCH] Remove invalidate queries & Add mutation effects --- README.md | 2 +- package.json | 5 +- src/assets/queryConfig.context.tsx | 22 ++++ src/assets/useMutationEffects.ts | 52 +++++++++ src/commands/generate.command.ts | 6 +- src/commands/generate.ts | 2 +- src/generators/const/deps.const.ts | 18 ++- src/generators/const/options.const.ts | 2 +- src/generators/const/queries.const.ts | 3 +- src/generators/generate/generateQueries.ts | 31 ++++-- ...dateQueries.ts => generateQueryModules.ts} | 4 +- src/generators/generateCodeFromOpenAPIDoc.ts | 92 +++------------- .../templates/invalidate-queries.hbs | 34 ------ .../templates/partials/query-js-docs.hbs | 4 +- .../templates/partials/query-keys.hbs | 2 +- .../partials/query-use-infinite-query.hbs | 4 +- .../templates/partials/query-use-mutation.hbs | 19 ++-- src/generators/templates/queries.hbs | 14 ++- src/generators/templates/query-modules.hbs | 5 + src/generators/types/options.d.ts | 2 +- src/generators/utils/generate-files.utils.ts | 104 ++++++++++++++++++ .../generate/generate.endpoints.utils.ts | 25 ++++- .../utils/generate/generate.utils.ts | 11 +- .../utils/hbs/hbs.partials.utils.ts | 23 ++-- 24 files changed, 315 insertions(+), 171 deletions(-) create mode 100644 src/assets/queryConfig.context.tsx create mode 100644 src/assets/useMutationEffects.ts rename src/generators/generate/{generateInvalidateQueries.ts => generateQueryModules.ts} (77%) delete mode 100644 src/generators/templates/invalidate-queries.hbs create mode 100644 src/generators/templates/query-modules.hbs create mode 100644 src/generators/utils/generate-files.utils.ts diff --git a/README.md b/README.md index 3d7a0f02..e646020b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ yarn openapi-codegen generate --input http://localhost:3001/docs-json --standalo --axiosRequestConfig Include Axios request config parameters in query hooks (default: false) --infiniteQueries Generate infinite queries for paginated API endpoints (default: false) - --invalidateQueryOptions Add query invalidation options to mutation hooks (default: true) + --mutationEffects Add mutation effects options to mutation hooks (default: true) --standalone Generate any missing supporting classes/types, e.g., REST client class, React Query type extensions, etc. (default: false) --baseUrl (Requires `--standalone`) Base URL for the REST client; falls back to the OpenAPI spec if not provided diff --git a/package.json b/package.json index 2064b929..be6ed429 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@povio/openapi-codegen-cli", - "version": "0.10.8", + "version": "0.11.0", "main": "./dist/index.js", "bin": { "openapi-codegen": "./dist/sh.js" @@ -18,7 +18,8 @@ "lint": "eslint --fix", "format:check": "prettier --check .", "format:fix": "prettier --write .", - "push": "yarn exec ./scripts/publish.sh" + "push": "yarn exec ./scripts/publish.sh", + "dev:generate": "rm -rf ./output && yarn start generate --input http://localhost:4000/docs-json" }, "files": [ "dist/*", diff --git a/src/assets/queryConfig.context.tsx b/src/assets/queryConfig.context.tsx new file mode 100644 index 00000000..a7f18411 --- /dev/null +++ b/src/assets/queryConfig.context.tsx @@ -0,0 +1,22 @@ +import { createContext, use, useMemo } from "react"; + +export namespace QueryConfig { + interface Type { + preferUpdate?: boolean; + } + + const Context = createContext({}); + + type ProviderProps = Type; + + export const Provider = ({ preferUpdate, children }: React.PropsWithChildren) => { + const value = useMemo(() => ({ preferUpdate }), [preferUpdate]); + + return {children}; + }; + + export const useConfig = () => { + const context = use(Context); + return context ?? {}; + }; +} diff --git a/src/assets/useMutationEffects.ts b/src/assets/useMutationEffects.ts new file mode 100644 index 00000000..2efa1bda --- /dev/null +++ b/src/assets/useMutationEffects.ts @@ -0,0 +1,52 @@ +import { QueryKey, useQueryClient } from "@tanstack/react-query"; +import { useCallback } from "react"; + +import { QueryConfig } from "./queryConfig.context"; +import { QueryModule } from "./queryModules"; + +export interface MutationEffectsOptions { + invalidateCurrentModule?: boolean; + invalidateModules?: QueryModule[]; + invalidateKeys?: QueryKey[]; + preferUpdate?: boolean; +} + +export interface UseMutationEffectsOptions { + currentModule: QueryModule; +} + +export function useMutationEffects({ currentModule }: UseMutationEffectsOptions) { + const queryClient = useQueryClient(); + const config = QueryConfig.useConfig(); + + const runMutationEffects = useCallback( + async (data: TData, options: MutationEffectsOptions = {}, updateKeys?: QueryKey[]) => { + const { invalidateCurrentModule, invalidateModules, invalidateKeys, preferUpdate } = options; + const shouldUpdate = preferUpdate || (preferUpdate === undefined && config.preferUpdate); + + const isQueryKeyEqual = (keyA: QueryKey, keyB: QueryKey) => + keyA.length === keyB.length && keyA.every((item, index) => item === keyB[index]); + + queryClient.invalidateQueries({ + predicate: ({ queryKey }) => { + const isUpdateKey = updateKeys?.some((key) => isQueryKeyEqual(queryKey, key)); + if (shouldUpdate && isUpdateKey) { + return false; + } + + const isCurrentModule = invalidateCurrentModule && queryKey[0] === currentModule; + const isInvalidateModule = !!invalidateModules && invalidateModules.some((module) => queryKey[0] === module); + const isInvalidateKey = !!invalidateKeys && invalidateKeys.some((key) => isQueryKeyEqual(queryKey, key)); + return isCurrentModule || isInvalidateModule || isInvalidateKey; + }, + }); + + if (shouldUpdate && updateKeys) { + updateKeys.map((queryKey) => queryClient.setQueryData(queryKey, data)); + } + }, + [queryClient, currentModule, config.preferUpdate], + ); + + return { runMutationEffects }; +} diff --git a/src/commands/generate.command.ts b/src/commands/generate.command.ts index fc91c6c3..5b6e2dc3 100644 --- a/src/commands/generate.command.ts +++ b/src/commands/generate.command.ts @@ -63,11 +63,11 @@ class GenerateOptions implements GenerateParams { infiniteQueries!: boolean; @YargOption({ - envAlias: "invalidateQueryOptions", - default: DEFAULT_GENERATE_OPTIONS.invalidateQueryOptions, + envAlias: "mutationEffects", + default: DEFAULT_GENERATE_OPTIONS.mutationEffects, type: "boolean", }) - invalidateQueryOptions!: boolean; + mutationEffects!: boolean; @YargOption({ envAlias: "axiosRequestConfig", default: DEFAULT_GENERATE_OPTIONS.axiosRequestConfig, type: "boolean" }) axiosRequestConfig!: boolean; diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 0015af6a..7453d3f6 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -27,7 +27,7 @@ export type GenerateParams = { | "replaceOptionalWithNullish" | "infiniteQueries" | "axiosRequestConfig" - | "invalidateQueryOptions" + | "mutationEffects" >; export async function generate({ input, excludeTags, monorepo, prettier, verbose, ...params }: GenerateParams) { diff --git a/src/generators/const/deps.const.ts b/src/generators/const/deps.const.ts index e7c8f8c3..b8922877 100644 --- a/src/generators/const/deps.const.ts +++ b/src/generators/const/deps.const.ts @@ -50,14 +50,20 @@ export const STANDALONE_ASSETS: Record = { export const STANDALONE_APP_REST_CLIENT_FILE: GenerateFile = { fileName: "app-rest-client", extension: "ts" }; -// InvalidateQueryOptions +// QueryModules export const QUERY_MODULE_ENUM = "QueryModule"; -export const INVALIDATE_QUERIES = { - queryModuleEnum: QUERY_MODULE_ENUM, - optionsType: "InvalidateQueryOptions", - functionName: "invalidateQueries", +export const QUERY_MODULES_FILE: GenerateFile = { fileName: "queryModules", extension: "ts" }; + +// QueryConfig +export const QUERY_CONFIG_FILE: GenerateFile = { fileName: "queryConfig.context", extension: "tsx" }; + +// MutationEffects +export const MUTATION_EFFECTS = { + optionsType: "MutationEffectsOptions", + hookName: "useMutationEffects", + runFunctionName: "runMutationEffects", }; -export const INVALIDATE_QUERY_OPTIONS_FILE: GenerateFile = { fileName: "invalidateQueries", extension: "ts" }; +export const MUTATION_EFFECTS_FILE: GenerateFile = { fileName: "useMutationEffects", extension: "ts" }; // ZodExtended export const ZOD_EXTENDED = { diff --git a/src/generators/const/options.const.ts b/src/generators/const/options.const.ts index bce581da..c28fb721 100644 --- a/src/generators/const/options.const.ts +++ b/src/generators/const/options.const.ts @@ -48,5 +48,5 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = { queryTypesImportPath: TEMPLATE_IMPORTS.queryTypes.template, axiosRequestConfig: false, infiniteQueries: false, - invalidateQueryOptions: true, + mutationEffects: true, }; diff --git a/src/generators/const/queries.const.ts b/src/generators/const/queries.const.ts index a1fb167c..ae958f06 100644 --- a/src/generators/const/queries.const.ts +++ b/src/generators/const/queries.const.ts @@ -4,10 +4,9 @@ export const QUERY_HOOKS = { query: "useQuery", infiniteQuery: "useInfiniteQuery", mutation: "useMutation", - queryClient: "useQueryClient", }; export const QUERY_IMPORT: Import = { - bindings: [QUERY_HOOKS.query, QUERY_HOOKS.infiniteQuery, QUERY_HOOKS.mutation, QUERY_HOOKS.queryClient], + bindings: [QUERY_HOOKS.query, QUERY_HOOKS.infiniteQuery, QUERY_HOOKS.mutation], from: "@tanstack/react-query", }; diff --git a/src/generators/generate/generateQueries.ts b/src/generators/generate/generateQueries.ts index 80fd860f..e7777352 100644 --- a/src/generators/generate/generateQueries.ts +++ b/src/generators/generate/generateQueries.ts @@ -1,4 +1,4 @@ -import { INVALIDATE_QUERIES, QUERY_OPTIONS_TYPES } from "../const/deps.const"; +import { MUTATION_EFFECTS, QUERY_MODULE_ENUM, QUERY_OPTIONS_TYPES } from "../const/deps.const"; import { AXIOS_DEFAULT_IMPORT_NAME, AXIOS_IMPORT, AXIOS_REQUEST_CONFIG_TYPE } from "../const/endpoints.const"; import { QUERIES_MODULE_NAME, QUERY_HOOKS, QUERY_IMPORT } from "../const/queries.const"; import { EndpointParameter } from "../types/endpoint"; @@ -6,8 +6,9 @@ import { GenerateType, GenerateTypeParams, Import } from "../types/generate"; import { getUniqueArray } from "../utils/array.utils"; import { getEndpointsImports, getModelsImports } from "../utils/generate/generate.imports.utils"; import { - getInvalidateQueriesImportPath, + getMutationEffectsImportPath, getNamespaceName, + getQueryModulesImportPath, getQueryTypesImportPath, } from "../utils/generate/generate.utils"; import { getHbsTemplateDelegate } from "../utils/hbs/hbs-template.utils"; @@ -37,17 +38,21 @@ export function generateQueries({ resolver, data, tag = "" }: GenerateTypeParams bindings: [ ...(queryEndpoints.length > 0 ? [QUERY_HOOKS.query] : []), ...(resolver.options.infiniteQueries && infiniteQueryEndpoints.length > 0 ? [QUERY_HOOKS.infiniteQuery] : []), - ...(mutationEndpoints.length > 0 ? [QUERY_HOOKS.mutation, QUERY_HOOKS.queryClient] : []), + ...(mutationEndpoints.length > 0 ? [QUERY_HOOKS.mutation] : []), ], from: QUERY_IMPORT.from, }; - const invalidateQueriesImport: Import = { - bindings: [ - INVALIDATE_QUERIES.queryModuleEnum, - ...(mutationEndpoints.length > 0 ? [INVALIDATE_QUERIES.optionsType, INVALIDATE_QUERIES.functionName] : []), - ], - from: getInvalidateQueriesImportPath(resolver.options), + const hasMutationEffects = resolver.options.mutationEffects; + const queryModulesImport: Import = { + bindings: [QUERY_MODULE_ENUM], + from: getQueryModulesImportPath(resolver.options), + }; + + const hasMutationEffectsImport = hasMutationEffects && mutationEndpoints.length > 0; + const mutationEffectsImport: Import = { + bindings: [...(mutationEndpoints.length > 0 ? [MUTATION_EFFECTS.optionsType, MUTATION_EFFECTS.hookName] : [])], + from: getMutationEffectsImportPath(resolver.options), }; const queryTypesImport: Import = { @@ -80,8 +85,10 @@ export function generateQueries({ resolver, data, tag = "" }: GenerateTypeParams hasAxiosImport, axiosImport, queryImport, - hasInvalidateQueryOptions: resolver.options.invalidateQueryOptions, - invalidateQueriesImport, + hasMutationEffects, + queryModulesImport, + hasMutationEffectsImport, + mutationEffectsImport, queryTypesImport, modelsImports, endpointsImports, @@ -89,7 +96,7 @@ export function generateQueries({ resolver, data, tag = "" }: GenerateTypeParams tag, namespace: getNamespaceName({ type: GenerateType.Queries, tag, options: resolver.options }), queriesModuleName: QUERIES_MODULE_NAME, - queryModuleEnum: INVALIDATE_QUERIES.queryModuleEnum, + queryModuleEnum: QUERY_MODULE_ENUM, endpoints, queryEndpoints, }); diff --git a/src/generators/generate/generateInvalidateQueries.ts b/src/generators/generate/generateQueryModules.ts similarity index 77% rename from src/generators/generate/generateInvalidateQueries.ts rename to src/generators/generate/generateQueryModules.ts index b96b495b..ae844b0d 100644 --- a/src/generators/generate/generateInvalidateQueries.ts +++ b/src/generators/generate/generateQueryModules.ts @@ -2,7 +2,7 @@ import { GenerateType, GenerateTypeParams } from "../types/generate"; import { getNamespaceName } from "../utils/generate/generate.utils"; import { getHbsTemplateDelegate } from "../utils/hbs/hbs-template.utils"; -export function generateInvalidateQueries({ resolver, data }: Omit) { +export function generateQueryModules({ resolver, data }: Omit) { const modules: { tag: string; namespace: string }[] = []; data.forEach((_, tag) => { @@ -17,7 +17,7 @@ export function generateInvalidateQueries({ resolver, data }: Omit) { const importPath = cliOptions.standalone && cliOptions.importPath === "ts" ? "relative" : cliOptions.importPath; @@ -53,67 +48,12 @@ export function generateCodeFromOpenAPIDoc(openApiDoc: OpenAPIV3.Document, cliOp }); }); - const appAclContent = generateAppAcl(resolver, appAclTags); - if (appAclContent) { - const fileName = getOutputFileName({ - output: options.output, - fileName: getFileNameWithExtension(ACL_APP_ABILITY_FILE), - }); - generateFilesData.push({ fileName, content: appAclContent }); - } - - if (options.invalidateQueryOptions) { - generateFilesData.push({ - fileName: getOutputFileName({ - output: resolver.options.output, - fileName: getFileNameWithExtension(INVALIDATE_QUERY_OPTIONS_FILE), - }), - content: generateInvalidateQueries({ resolver, data }), - }); - } - - if (options.standalone) { - generateFilesData.push(...getStandaloneFiles(resolver)); - } - - if (hasZodExtendedFile(data)) { - const zodContent = generateZod(resolver); - if (zodContent) { - const fileName = getOutputFileName({ - output: options.output, - fileName: getFileNameWithExtension(ZOD_EXTENDED_FILE), - }); - generateFilesData.push({ fileName, content: zodContent }); - } - } - - return generateFilesData; -} - -function getStandaloneFiles(resolver: SchemaResolver) { - const generateFilesData: GenerateFileData[] = []; - - Object.values(STANDALONE_ASSETS).forEach((file) => { - const fileName = getFileNameWithExtension(file); - generateFilesData.push({ - content: readAssetSync(fileName), - fileName: getOutputFileName({ output: resolver.options.output, fileName }), - }); - }); - - generateFilesData.push({ - fileName: getOutputFileName({ - output: resolver.options.output, - fileName: getFileNameWithExtension(STANDALONE_APP_REST_CLIENT_FILE), - }), - content: generateAppRestClient(resolver), - }); + generateFilesData.push( + ...getAclFiles(appAclTags, resolver), + ...getMutationEffectsFiles(data, resolver), + ...getZodExtendedFiles(data, resolver), + ...getStandaloneFiles(resolver), + ); return generateFilesData; } - -function hasZodExtendedFile(data: GenerateData) { - return Array.from(data.values()).some(({ endpoints }) => - endpoints.some((endpoint) => endpoint.parameters.some((param) => param.parameterSortingEnumSchemaName)), - ); -} diff --git a/src/generators/templates/invalidate-queries.hbs b/src/generators/templates/invalidate-queries.hbs deleted file mode 100644 index cf368a96..00000000 --- a/src/generators/templates/invalidate-queries.hbs +++ /dev/null @@ -1,34 +0,0 @@ -import { QueryClient, QueryKey } from "@tanstack/react-query"; - -export const enum QueryModule { - {{#each modules as | module |}} - {{module.tag}} = "{{module.namespace}}", - {{/each}} -} - -export interface InvalidateQueryOptions { - invalidateCurrentModule?: boolean; - invalidateModules?: QueryModule[]; - invalidateKeys?: QueryKey[]; -} - -export async function invalidateQueries( - queryClient: QueryClient, - currentModule: QueryModule, - options: InvalidateQueryOptions = {}, -) { - const { invalidateCurrentModule, invalidateModules, invalidateKeys } = options; - - if (invalidateCurrentModule) { - await queryClient.invalidateQueries({ queryKey: [currentModule] }); - } - - if (invalidateModules) { - await Promise.all([...invalidateModules.map((module) => queryClient.invalidateQueries({ queryKey: [module] }))]); - } - - if (invalidateKeys) { - await Promise.all([...invalidateKeys.map((queryKey) => queryClient.invalidateQueries({ queryKey }))]); - } -} - diff --git a/src/generators/templates/partials/query-js-docs.hbs b/src/generators/templates/partials/query-js-docs.hbs index 432a90ee..1d9db5ed 100644 --- a/src/generators/templates/partials/query-js-docs.hbs +++ b/src/generators/templates/partials/query-js-docs.hbs @@ -4,7 +4,7 @@ * @summary {{addAsteriskAfterNewLine endpoint.summary}}{{/if}}{{#if endpoint.description}} * @description {{addAsteriskAfterNewLine endpoint.description}}{{/if}}{{#if endpoint.acl}} * @permission Requires `{{abilityFunctionName endpoint}}` ability {{/if}} -{{#if (endpointParams endpoint)}}{{#each (endpointParams endpoint infiniteQuery removePageParam=true) as | endpointParam |}} * @param { {{endpointParam.type}} } object.{{endpointParam.name}} {{{endpointParamDescription endpointParam}}} +{{#if (endpointParams endpoint)}}{{#each (endpointParams endpoint infiniteQuery excludePageParam=true) as | endpointParam |}} * @param { {{endpointParam.type}} } object.{{endpointParam.name}} {{{endpointParamDescription endpointParam}}} {{/each}}{{/if}} * @param { AppInfiniteQueryOptions } options Infinite query options * @returns { UseInfiniteQueryResult<{{{importedZodSchemaInferedType endpoint.response}}}> } {{endpoint.responseDescription}} * @statusCodes [{{commaSeparated endpoint.responseStatusCodes}}] @@ -25,7 +25,7 @@ * @description {{addAsteriskAfterNewLine endpoint.description}}{{/if}}{{#if endpoint.acl}} * @permission Requires `{{abilityFunctionName endpoint}}` ability {{/if}} {{#if (endpointParams endpoint includeFileParam=true)}}{{#each (endpointParams endpoint includeFileParam=true) as | endpointParam |}} * @param { {{endpointParam.type}} } mutation.{{endpointParam.name}} {{{endpointParamDescription endpointParam}}} -{{/each}}{{/if}} * @param { AppMutationOptions{{#if hasInvalidateQueryOptions}} & {{invalidateQueryOptionsType}}{{/if}} } options Mutation options +{{/each}}{{/if}} * @param { AppMutationOptions{{#if hasMutationEffects}} & {{mutationEffectsType}}{{/if}} } options Mutation options * @returns { UseMutationResult<{{#if endpoint.mediaDownload}}AxiosResponse<{{/if}}{{{importedZodSchemaInferedType endpoint.response}}}{{#if endpoint.mediaDownload}}>{{/if}}> } {{endpoint.responseDescription}} * @statusCodes [{{commaSeparated endpoint.responseStatusCodes}}] */{{/if}} \ No newline at end of file diff --git a/src/generators/templates/partials/query-keys.hbs b/src/generators/templates/partials/query-keys.hbs index ba45387b..d408bcd1 100644 --- a/src/generators/templates/partials/query-keys.hbs +++ b/src/generators/templates/partials/query-keys.hbs @@ -4,7 +4,7 @@ export const keys = { {{endpointName endpoint}}: ({{{genEndpointParams endpoint}}}) => [...keys.all, "{{endpoint.path}}", {{{endpointArgs endpoint}}}] as const, {{#if ../generateInfiniteQueries}} {{#if (isInfiniteQuery endpoint)}} - {{endpointName endpoint}}Infinite: ({{{genEndpointParams endpoint removePageParam=true}}}) => [...keys.all, "{{endpoint.path}}", "infinite", {{{endpointArgs endpoint removePageParam=true}}}] as const, + {{endpointName endpoint}}Infinite: ({{{genEndpointParams endpoint excludePageParam=true}}}) => [...keys.all, "{{endpoint.path}}", "infinite", {{{endpointArgs endpoint excludePageParam=true}}}] as const, {{/if}} {{/if}} {{/each}} diff --git a/src/generators/templates/partials/query-use-infinite-query.hbs b/src/generators/templates/partials/query-use-infinite-query.hbs index 0425143a..368d5407 100644 --- a/src/generators/templates/partials/query-use-infinite-query.hbs +++ b/src/generators/templates/partials/query-use-infinite-query.hbs @@ -1,9 +1,9 @@ {{! Js docs }} {{{genQueryJsDocs endpoint infiniteQuery=true}}} {{! Infinite query definition}} -export const {{infiniteQueryName endpoint}} = ({{#if (endpointParams endpoint)}}{ {{{endpointArgs endpoint removePageParam=true}}} }: { {{{genEndpointParams endpoint removePageParam=true}}} }, {{/if}}options?: AppInfiniteQueryOptions{{#if hasAxiosRequestConfig}}, {{axiosRequestConfigName}}?: {{axiosRequestConfigType}}{{/if}}) => { +export const {{infiniteQueryName endpoint}} = ({{#if (endpointParams endpoint)}}{ {{{endpointArgs endpoint excludePageParam=true}}} }: { {{{genEndpointParams endpoint excludePageParam=true}}} }, {{/if}}options?: AppInfiniteQueryOptions{{#if hasAxiosRequestConfig}}, {{axiosRequestConfigName}}?: {{axiosRequestConfigType}}{{/if}}) => { return {{infiniteQueryHook}}({ - queryKey: keys.{{endpointName endpoint}}Infinite({{#if (endpointParams endpoint)}}{{{endpointArgs endpoint removePageParam=true}}}{{/if}}), + queryKey: keys.{{endpointName endpoint}}Infinite({{#if (endpointParams endpoint)}}{{{endpointArgs endpoint excludePageParam=true}}}{{/if}}), queryFn: ({ pageParam }) => {{importedEndpointName endpoint}}({{{endpointArgs endpoint replacePageParam=true}}}{{#if hasAxiosRequestConfig}}, {{axiosRequestConfigName}}{{/if}}), initialPageParam: 1, getNextPageParam: ({ {{pageParamName}}, {{totalItemsName}}, {{limitParamName}}: limitParam }) => { diff --git a/src/generators/templates/partials/query-use-mutation.hbs b/src/generators/templates/partials/query-use-mutation.hbs index d6bdae10..47772156 100644 --- a/src/generators/templates/partials/query-use-mutation.hbs +++ b/src/generators/templates/partials/query-use-mutation.hbs @@ -1,8 +1,9 @@ {{! Js docs }} {{{genQueryJsDocs endpoint mutation=true}}} {{! Mutation definition}} -export const {{queryName endpoint mutation=true}} = (options?: AppMutationOptions{{#if hasInvalidateQueryOptions}} & {{invalidateQueryOptionsType}}{{/if}}{{#if hasAxiosRequestConfig}}, {{axiosRequestConfigName}}?: {{axiosRequestConfigType}}{{/if}}) => { - {{#if hasInvalidateQueryOptions}} const queryClient = useQueryClient();{{/if}} +export const {{queryName endpoint mutation=true}} = (options?: AppMutationOptions{{#if hasMutationEffects}} & {{mutationEffectsType}}{{/if}}{{#if hasAxiosRequestConfig}}, {{axiosRequestConfigName}}?: {{axiosRequestConfigType}}{{/if}}) => { + {{! Use mutation effects }} + {{#if hasMutationEffects}}const { runMutationEffects } = useMutationEffects({ currentModule: {{queriesModuleName}} });{{/if}} return {{queryHook}}({ mutationFn: {{#if endpoint.mediaUpload}}async {{/if}}({{#if (endpointParams endpoint includeFileParam=true)}} { {{{endpointArgs endpoint includeFileParam=true}}} } {{/if}}) => {{#if endpoint.mediaUpload}} { @@ -18,11 +19,15 @@ export const {{queryName endpoint mutation=true}} = (options?: AppMutationOption return uploadInstructions; }{{/if}}, - ...options, {{#if hasInvalidateQueryOptions}} - onSuccess: (...args) => { - {{! Invalidation }} - invalidateQueries(queryClient, {{queriesModuleName}}, options); - options?.onSuccess?.(...args); + ...options, {{#if hasMutationEffects}} + onSuccess: async (resData, variables, context) => { + {{! Mutation effects }} + {{#if updateQueryEndpoints}} + const { {{endpointArgs endpoint includeOnlyRequiredParams=true excludeBodyParam=true}} } = variables; + const updateKeys = [{{#each updateQueryEndpoints as | endpoint |}}keys.{{endpointName endpoint}}({{{endpointArgs endpoint includeOnlyRequiredParams=true}}}), {{/each}}]; + {{/if}} + await runMutationEffects(resData, options{{#if updateQueryEndpoints}}, updateKeys{{/if}}); + options?.onSuccess?.(resData, variables, context); },{{/if}} }); }; \ No newline at end of file diff --git a/src/generators/templates/queries.hbs b/src/generators/templates/queries.hbs index 234faccc..4bcc2efa 100644 --- a/src/generators/templates/queries.hbs +++ b/src/generators/templates/queries.hbs @@ -4,9 +4,13 @@ {{/if}} {{! React query import }} {{{genImport queryImport}}} -{{! Invalidate queries import }} -{{#if hasInvalidateQueryOptions}} -{{{genImport invalidateQueriesImport}}} +{{! Query modules import }} +{{#if hasMutationEffects}} +{{{genImport queryModulesImport}}} +{{/if}} +{{! Mutation effects import }} +{{#if hasMutationEffectsImport}} +{{{genImport mutationEffectsImport}}} {{/if}} {{! React query types import }} {{{genImport queryTypesImport}}} @@ -23,7 +27,7 @@ export namespace {{namespace}} { {{/if}} -export const {{queriesModuleName}} = {{#if hasInvalidateQueryOptions}}{{queryModuleEnum}}.{{tag}}{{else}}"{{namespace}}"{{/if}}; +export const {{queriesModuleName}} = {{#if hasMutationEffects}}{{queryModuleEnum}}.{{tag}}{{else}}"{{namespace}}"{{/if}}; {{! Query keys export}} {{{genQueryKeys queryEndpoints}}} @@ -32,7 +36,7 @@ export const {{queriesModuleName}} = {{#if hasInvalidateQueryOptions}}{{queryMod {{#each endpoints as | endpoint |}} {{{genQuery endpoint}}} -{{{genMutation endpoint}}} +{{{genMutation endpoint ../queryEndpoints}}} {{{genInfiniteQuery endpoint}}} diff --git a/src/generators/templates/query-modules.hbs b/src/generators/templates/query-modules.hbs new file mode 100644 index 00000000..0408533b --- /dev/null +++ b/src/generators/templates/query-modules.hbs @@ -0,0 +1,5 @@ +export const enum QueryModule { + {{#each modules as | module |}} + {{module.tag}} = "{{module.namespace}}", + {{/each}} +} \ No newline at end of file diff --git a/src/generators/types/options.d.ts b/src/generators/types/options.d.ts index 680e6ba5..05718e60 100644 --- a/src/generators/types/options.d.ts +++ b/src/generators/types/options.d.ts @@ -23,7 +23,7 @@ interface QueriesGenerateOptions { queryTypesImportPath: string; axiosRequestConfig?: boolean; infiniteQueries?: boolean; - invalidateQueryOptions?: boolean; + mutationEffects?: boolean; } interface GenerateConfig { diff --git a/src/generators/utils/generate-files.utils.ts b/src/generators/utils/generate-files.utils.ts new file mode 100644 index 00000000..794ee47f --- /dev/null +++ b/src/generators/utils/generate-files.utils.ts @@ -0,0 +1,104 @@ +import { ACL_APP_ABILITY_FILE } from "../const/acl.const"; +import { + MUTATION_EFFECTS_FILE, + QUERY_CONFIG_FILE, + QUERY_MODULES_FILE, + STANDALONE_APP_REST_CLIENT_FILE, + STANDALONE_ASSETS, + ZOD_EXTENDED_FILE, +} from "../const/deps.const"; +import { SchemaResolver } from "../core/SchemaResolver.class"; +import { generateAppAcl } from "../generate/generateAcl"; +import { generateAppRestClient } from "../generate/generateAppRestClient"; +import { generateQueryModules } from "../generate/generateQueryModules"; +import { generateZod } from "../generate/generateZod"; +import { GenerateData, GenerateFile, GenerateFileData } from "../types/generate"; +import { getOutputFileName, readAssetSync } from "./file.utils"; +import { getFileNameWithExtension } from "./generate/generate.utils"; + +export function getAclFiles(appAclTags: string[], resolver: SchemaResolver): GenerateFileData[] { + const appAclContent = generateAppAcl(resolver, appAclTags); + if (!appAclContent) { + return []; + } + + return [ + { + fileName: getOutputFileName({ + output: resolver.options.output, + fileName: getFileNameWithExtension(ACL_APP_ABILITY_FILE), + }), + content: appAclContent, + }, + ]; +} + +export function getMutationEffectsFiles(data: GenerateData, resolver: SchemaResolver): GenerateFileData[] { + if (!resolver.options.mutationEffects) { + return []; + } + + return [ + ...getAssetFiles([QUERY_CONFIG_FILE, MUTATION_EFFECTS_FILE], resolver), + { + fileName: getOutputFileName({ + output: resolver.options.output, + fileName: getFileNameWithExtension(QUERY_MODULES_FILE), + }), + content: generateQueryModules({ resolver, data }), + }, + ]; +} + +export function getStandaloneFiles(resolver: SchemaResolver): GenerateFileData[] { + if (!resolver.options.standalone) { + return []; + } + + return [ + ...getAssetFiles(Object.values(STANDALONE_ASSETS), resolver), + { + fileName: getOutputFileName({ + output: resolver.options.output, + fileName: getFileNameWithExtension(STANDALONE_APP_REST_CLIENT_FILE), + }), + content: generateAppRestClient(resolver), + }, + ]; +} + +export function getZodExtendedFiles(data: GenerateData, resolver: SchemaResolver): GenerateFileData[] { + const hasZodExtendedFile = Array.from(data.values()).some(({ endpoints }) => + endpoints.some((endpoint) => endpoint.parameters.some((param) => param.parameterSortingEnumSchemaName)), + ); + if (!hasZodExtendedFile) { + return []; + } + + const zodContent = generateZod(resolver); + if (!zodContent) { + return []; + } + + return [ + { + fileName: getOutputFileName({ + output: resolver.options.output, + fileName: getFileNameWithExtension(ZOD_EXTENDED_FILE), + }), + content: zodContent, + }, + ]; +} + +function getAssetFiles(files: GenerateFile[], resolver: SchemaResolver): GenerateFileData[] { + return files.reduce((acc, file) => [...acc, getAssetFile(file, resolver)], [] as GenerateFileData[]); +} + +function getAssetFile(file: GenerateFile, resolver: SchemaResolver): GenerateFileData { + const fileName = getFileNameWithExtension(file); + return { + fileName: getOutputFileName({ output: resolver.options.output, fileName }), + content: readAssetSync(fileName), + }; +} diff --git a/src/generators/utils/generate/generate.endpoints.utils.ts b/src/generators/utils/generate/generate.endpoints.utils.ts index 90ed5d42..a985b9b4 100644 --- a/src/generators/utils/generate/generate.endpoints.utils.ts +++ b/src/generators/utils/generate/generate.endpoints.utils.ts @@ -3,11 +3,12 @@ import { INFINITE_QUERY_PARAMS } from "src/generators/const/queries.const"; import { SchemaResolver } from "src/generators/core/SchemaResolver.class"; import { GenerateType } from "src/generators/types/generate"; import { GenerateOptions } from "src/generators/types/options"; -import { DEFAULT_HEADERS } from "../../const/endpoints.const"; +import { BODY_PARAMETER_NAME, DEFAULT_HEADERS } from "../../const/endpoints.const"; import { Endpoint } from "../../types/endpoint"; import { invalidVariableNameCharactersToCamel, isValidPropertyName } from "../js.utils"; import { isSchemaObject } from "../openapi-schema.utils"; import { isPrimitiveType } from "../openapi.utils"; +import { isQuery } from "../query.utils"; import { decapitalize, snakeToCamel } from "../string.utils"; import { formatTag } from "../tag.utils"; import { primitiveTypeToTsType } from "../ts.utils"; @@ -35,9 +36,11 @@ export function mapEndpointParamsToFunctionParams( resolver: SchemaResolver, endpoint: Endpoint, options?: { - removePageParam?: boolean; + excludeBodyParam?: boolean; + excludePageParam?: boolean; replacePageParam?: boolean; includeFileParam?: boolean; + includeOnlyRequiredParams?: boolean; }, ) { const params = endpoint.parameters.map((param) => { @@ -80,7 +83,12 @@ export function mapEndpointParamsToFunctionParams( } return a.required ? -1 : 1; }) - .filter((param) => !options?.removePageParam || param.name !== INFINITE_QUERY_PARAMS.pageParamName) + .filter( + (param) => + (!options?.excludeBodyParam || param.name !== BODY_PARAMETER_NAME) && + (!options?.excludePageParam || param.name !== INFINITE_QUERY_PARAMS.pageParamName) && + (!options?.includeOnlyRequiredParams || param.required), + ) .map((param) => ({ ...param, name: options?.replacePageParam && param.name === INFINITE_QUERY_PARAMS.pageParamName ? "pageParam" : param.name, @@ -119,3 +127,14 @@ export function getEndpointConfig(endpoint: Endpoint) { }; return endpointConfig; } + +export function getUpdateQueryEndpoints(endpoint: Endpoint, endpoints: Endpoint[]) { + return endpoints.filter( + (e) => + isQuery(e) && + e.parameters + .filter((param) => param.parameterObject?.required) + .every((pathParam) => endpoint.parameters.some((param) => param.name === pathParam.name)) && + e.response === endpoint.response, + ); +} diff --git a/src/generators/utils/generate/generate.utils.ts b/src/generators/utils/generate/generate.utils.ts index 337c8b1d..f475bba7 100644 --- a/src/generators/utils/generate/generate.utils.ts +++ b/src/generators/utils/generate/generate.utils.ts @@ -1,5 +1,6 @@ import { - INVALIDATE_QUERY_OPTIONS_FILE, + MUTATION_EFFECTS_FILE, + QUERY_MODULES_FILE, STANDALONE_APP_REST_CLIENT_FILE, STANDALONE_ASSETS, StandaloneAssetEnum, @@ -64,8 +65,12 @@ export function getQueryTypesImportPath(options: GenerateOptions) { return `${getImportPath(options)}${STANDALONE_ASSETS[StandaloneAssetEnum.ReactQueryTypes].fileName}`; } -export function getInvalidateQueriesImportPath(options: GenerateOptions) { - return `${getImportPath(options)}${INVALIDATE_QUERY_OPTIONS_FILE.fileName}`; +export function getQueryModulesImportPath(options: GenerateOptions) { + return `${getImportPath(options)}${QUERY_MODULES_FILE.fileName}`; +} + +export function getMutationEffectsImportPath(options: GenerateOptions) { + return `${getImportPath(options)}${MUTATION_EFFECTS_FILE.fileName}`; } export function getZodExtendedImportPath(options: GenerateOptions) { diff --git a/src/generators/utils/hbs/hbs.partials.utils.ts b/src/generators/utils/hbs/hbs.partials.utils.ts index 1e6a1bdb..629a2868 100644 --- a/src/generators/utils/hbs/hbs.partials.utils.ts +++ b/src/generators/utils/hbs/hbs.partials.utils.ts @@ -1,13 +1,17 @@ import Handlebars from "handlebars"; import { CASL_ABILITY_BINDING } from "src/generators/const/acl.const"; -import { INVALIDATE_QUERIES, ZOD_EXTENDED } from "src/generators/const/deps.const"; +import { MUTATION_EFFECTS, ZOD_EXTENDED } from "src/generators/const/deps.const"; import { AXIOS_REQUEST_CONFIG_NAME, AXIOS_REQUEST_CONFIG_TYPE } from "src/generators/const/endpoints.const"; import { BLOB_SCHEMA } from "src/generators/const/zod.const"; import { SchemaResolver } from "src/generators/core/SchemaResolver.class"; import { INFINITE_QUERY_RESPONSE_PARAMS, QUERIES_MODULE_NAME, QUERY_HOOKS } from "../../const/queries.const"; import { Endpoint } from "../../types/endpoint"; import { GenerateZodSchemaData, Import } from "../../types/generate"; -import { getEndpointConfig, mapEndpointParamsToFunctionParams } from "../generate/generate.endpoints.utils"; +import { + getEndpointConfig, + getUpdateQueryEndpoints, + mapEndpointParamsToFunctionParams, +} from "../generate/generate.endpoints.utils"; import { getHbsPartialTemplateDelegate } from "../hbs/hbs-template.utils"; import { isInfiniteQuery, isMutation, isQuery } from "../query.utils"; @@ -72,6 +76,7 @@ function registerGenerateEndpointConfigHelper(resolver: SchemaResolver) { if (Object.keys(endpointConfig).length === 0) { return hasAxiosRequestConfig ? AXIOS_REQUEST_CONFIG_NAME : ""; } + return getHbsPartialTemplateDelegate("endpoint-config")({ endpointConfig, hasAxiosRequestConfig, @@ -99,6 +104,7 @@ function registerGenerateQueryKeysHelper(resolver: SchemaResolver) { if (queryEndpoints.length === 0) { return ""; } + return getHbsPartialTemplateDelegate("query-keys")({ queryEndpoints, queriesModuleName: QUERIES_MODULE_NAME, @@ -127,11 +133,13 @@ function registerGenerateQueryHelper(resolver: SchemaResolver) { } function registerGenerateMutationHelper(resolver: SchemaResolver) { - Handlebars.registerHelper(PartialsHelpers.Mutation, (endpoint: Endpoint) => { + Handlebars.registerHelper(PartialsHelpers.Mutation, (endpoint: Endpoint, queryEndpoints: Endpoint[]) => { if (!isMutation(endpoint)) { return; } + const updateQueryEndpoints = getUpdateQueryEndpoints(endpoint, queryEndpoints); + return getHbsPartialTemplateDelegate("query-use-mutation")({ endpoint, queryHook: QUERY_HOOKS.mutation, @@ -139,8 +147,9 @@ function registerGenerateMutationHelper(resolver: SchemaResolver) { hasAxiosRequestConfig: resolver.options.axiosRequestConfig, axiosRequestConfigName: AXIOS_REQUEST_CONFIG_NAME, axiosRequestConfigType: AXIOS_REQUEST_CONFIG_TYPE, - hasInvalidateQueryOptions: resolver.options.invalidateQueryOptions, - invalidateQueryOptionsType: INVALIDATE_QUERIES.optionsType, + hasMutationEffects: resolver.options.mutationEffects, + mutationEffectsType: MUTATION_EFFECTS.optionsType, + updateQueryEndpoints, }); }); } @@ -173,8 +182,8 @@ function registerGenerateQueryJsDocsHelper(resolver: SchemaResolver) { query: options.hash.query, mutation: options.hash.mutation, infiniteQuery: options.hash.infiniteQuery, - hasInvalidateQueryOptions: resolver.options.invalidateQueryOptions, - invalidateQueryOptionsType: INVALIDATE_QUERIES.optionsType, + hasMutationEffects: resolver.options.mutationEffects, + mutationEffectsType: MUTATION_EFFECTS.optionsType, }), ); }