Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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/*",
Expand Down
22 changes: 22 additions & 0 deletions src/assets/queryConfig.context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createContext, use, useMemo } from "react";

export namespace QueryConfig {
interface Type {
preferUpdate?: boolean;
}

const Context = createContext<Type>({});

type ProviderProps = Type;

export const Provider = ({ preferUpdate, children }: React.PropsWithChildren<ProviderProps>) => {
const value = useMemo(() => ({ preferUpdate }), [preferUpdate]);

return <Context.Provider value={value}>{children}</Context.Provider>;
};

export const useConfig = () => {
const context = use(Context);
return context ?? {};
};
}
52 changes: 52 additions & 0 deletions src/assets/useMutationEffects.ts
Original file line number Diff line number Diff line change
@@ -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 <TData>(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 };
}
6 changes: 3 additions & 3 deletions src/commands/generate.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export type GenerateParams = {
| "replaceOptionalWithNullish"
| "infiniteQueries"
| "axiosRequestConfig"
| "invalidateQueryOptions"
| "mutationEffects"
>;

export async function generate({ input, excludeTags, monorepo, prettier, verbose, ...params }: GenerateParams) {
Expand Down
18 changes: 12 additions & 6 deletions src/generators/const/deps.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,20 @@ export const STANDALONE_ASSETS: Record<StandaloneAssetEnum, GenerateFile> = {

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 = {
Expand Down
2 changes: 1 addition & 1 deletion src/generators/const/options.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,5 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = {
queryTypesImportPath: TEMPLATE_IMPORTS.queryTypes.template,
axiosRequestConfig: false,
infiniteQueries: false,
invalidateQueryOptions: true,
mutationEffects: true,
};
3 changes: 1 addition & 2 deletions src/generators/const/queries.const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
};

Expand Down
31 changes: 19 additions & 12 deletions src/generators/generate/generateQueries.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
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";
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";
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -80,16 +85,18 @@ export function generateQueries({ resolver, data, tag = "" }: GenerateTypeParams
hasAxiosImport,
axiosImport,
queryImport,
hasInvalidateQueryOptions: resolver.options.invalidateQueryOptions,
invalidateQueriesImport,
hasMutationEffects,
queryModulesImport,
hasMutationEffectsImport,
mutationEffectsImport,
queryTypesImport,
modelsImports,
endpointsImports,
includeNamespace: resolver.options.tsNamespaces,
tag,
namespace: getNamespaceName({ type: GenerateType.Queries, tag, options: resolver.options }),
queriesModuleName: QUERIES_MODULE_NAME,
queryModuleEnum: INVALIDATE_QUERIES.queryModuleEnum,
queryModuleEnum: QUERY_MODULE_ENUM,
endpoints,
queryEndpoints,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenerateTypeParams, "tag">) {
export function generateQueryModules({ resolver, data }: Omit<GenerateTypeParams, "tag">) {
const modules: { tag: string; namespace: string }[] = [];

data.forEach((_, tag) => {
Expand All @@ -17,7 +17,7 @@ export function generateInvalidateQueries({ resolver, data }: Omit<GenerateTypeP
});
});

const hbsTemplate = getHbsTemplateDelegate(resolver, "invalidate-queries");
const hbsTemplate = getHbsTemplateDelegate(resolver, "query-modules");

return hbsTemplate({ modules });
}
92 changes: 16 additions & 76 deletions src/generators/generateCodeFromOpenAPIDoc.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,20 @@
import { OpenAPIV3 } from "openapi-types";
import { ACL_APP_ABILITY_FILE } from "./const/acl.const";
import {
INVALIDATE_QUERY_OPTIONS_FILE,
STANDALONE_APP_REST_CLIENT_FILE,
STANDALONE_ASSETS,
ZOD_EXTENDED_FILE,
} from "./const/deps.const";
import { DEFAULT_GENERATE_OPTIONS } from "./const/options.const";
import { getDataFromOpenAPIDoc } from "./core/getDataFromOpenAPIDoc";
import { SchemaResolver } from "./core/SchemaResolver.class";
import { generateAcl, generateAppAcl } from "./generate/generateAcl";
import { generateAppRestClient } from "./generate/generateAppRestClient";
import { generateAcl } from "./generate/generateAcl";
import { generateEndpoints } from "./generate/generateEndpoints";
import { generateInvalidateQueries } from "./generate/generateInvalidateQueries";
import { generateModels } from "./generate/generateModels";
import { generateQueries } from "./generate/generateQueries";
import { generateZod } from "./generate/generateZod";
import { GenerateData, GenerateFileData, GenerateType, GenerateTypeParams } from "./types/generate";
import { GenerateFileData, GenerateType, GenerateTypeParams } from "./types/generate";
import { GenerateOptions } from "./types/options";
import { getOutputFileName, readAssetSync } from "./utils/file.utils";
import { getFileNameWithExtension, getTagFileName } from "./utils/generate/generate.utils";
import { getOutputFileName } from "./utils/file.utils";
import {
getAclFiles,
getMutationEffectsFiles,
getStandaloneFiles,
getZodExtendedFiles,
} from "./utils/generate-files.utils";
import { getTagFileName } from "./utils/generate/generate.utils";

export function generateCodeFromOpenAPIDoc(openApiDoc: OpenAPIV3.Document, cliOptions: Partial<GenerateOptions>) {
const importPath = cliOptions.standalone && cliOptions.importPath === "ts" ? "relative" : cliOptions.importPath;
Expand Down Expand Up @@ -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)),
);
}
Loading