-
-
Notifications
You must be signed in to change notification settings - Fork 623
feat(query): add useFlatInput option to merge params into a single object #3167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
bbbb4a5
dce0b67
2fcb230
3d5fcbe
47475c0
95e9137
c2bb8d4
781a7d7
6acdc3d
3ce5061
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import { | ||
| type GetterProps, | ||
| GetterPropType, | ||
| type GetterQueryParam, | ||
| } from '@orval/core'; | ||
|
|
||
| export function checkFlatInputCollisions( | ||
| operationName: string, | ||
| props: GetterProps, | ||
| queryParams?: GetterQueryParam, | ||
| ): void { | ||
| const pathParamNames = props | ||
| .filter( | ||
| (p) => | ||
| p.type === GetterPropType.PARAM || | ||
| p.type === GetterPropType.NAMED_PATH_PARAMS, | ||
| ) | ||
| .map((p) => p.name); | ||
|
|
||
| const queryFieldNames = queryParams?.fieldNames ?? []; | ||
|
|
||
| const allNames = [...pathParamNames, ...queryFieldNames]; | ||
| const seen = new Set<string>(); | ||
|
|
||
| for (const name of allNames) { | ||
| if (seen.has(name)) { | ||
| throw new Error( | ||
| `useFlatInput: duplicate parameter name "${name}" found in operation "${operationName}". ` + | ||
| `Path params, query params, and body fields must have unique names when using useFlatInput.`, | ||
| ); | ||
| } | ||
| seen.add(name); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,7 @@ import { | |
| } from '@orval/core'; | ||
|
|
||
| import { getHookOptions, getQueryErrorType, getQueryOptions } from './client'; | ||
| import { checkFlatInputCollisions } from './flat-input'; | ||
| import type { FrameworkAdapter } from './framework-adapter'; | ||
| import { generateMutationHook } from './mutation-generator'; | ||
| import { | ||
|
|
@@ -159,6 +160,7 @@ const generateQueryImplementation = ({ | |
| useInfinite, | ||
| useInvalidate, | ||
| useSetQueryData, | ||
| useFlatInput, | ||
| adapter, | ||
| }: { | ||
| queryOption: { | ||
|
|
@@ -189,6 +191,7 @@ const generateQueryImplementation = ({ | |
| useInfinite?: boolean; | ||
| useInvalidate?: boolean; | ||
| useSetQueryData?: boolean; | ||
| useFlatInput: boolean; | ||
| adapter: FrameworkAdapter; | ||
| }) => { | ||
| const { | ||
|
|
@@ -373,7 +376,54 @@ const generateQueryImplementation = ({ | |
| // This avoids TS1016 "required param cannot follow optional param" | ||
| const httpFirstParam = adapter.getHttpFirstParam(mutator); | ||
|
|
||
| const queryOptionsFn = `export const ${queryOptionsFnName} = <TData = ${TData}, TError = ${errorType}>(${httpFirstParam}${queryProps} ${queryArgumentsForOptions}) => { | ||
| const pathParams = props.filter( | ||
| (p) => | ||
| p.type === GetterPropType.PARAM || | ||
| p.type === GetterPropType.NAMED_PATH_PARAMS, | ||
| ); | ||
| const queryParamProp = props.find( | ||
| (p) => p.type === GetterPropType.QUERY_PARAM, | ||
| ); | ||
| const bodyProp = props.find((p) => p.type === GetterPropType.BODY); | ||
| const hasMultiplePropTypes = | ||
| (pathParams.length > 0 ? 1 : 0) + | ||
| (queryParamProp ? 1 : 0) + | ||
| (bodyProp ? 1 : 0) > | ||
| 1; | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This check only counts prop categories, not actual exposed inputs. A GET with two path params still evaluates to one category, so queries keep positional args even though the mutation generator flattens the same shape with 🤖 Prompt for AI Agents |
||
| let flatInputType = ''; | ||
| let flatInputDestructure = ''; | ||
|
|
||
| if (useFlatInput && hasMultiplePropTypes) { | ||
| checkFlatInputCollisions(operationName, props, queryParams); | ||
|
|
||
| const pathParamTypes = pathParams.map((p) => p.definition).join(';\n '); | ||
| const pathType = pathParams.length > 0 ? `{ ${pathParamTypes} }` : ''; | ||
| const queryType = queryParamProp ? queryParams?.schema.name : ''; | ||
| const bodyType = bodyProp | ||
| ? bodyProp.definition.split(':').slice(1).join(':').trim() | ||
| : ''; | ||
|
|
||
| const types = [pathType, queryType, bodyType].filter(Boolean); | ||
| flatInputType = types.join(' & '); | ||
|
|
||
| const pathNames = pathParams.map((p) => p.name); | ||
| const queryFieldNames = queryParams?.fieldNames ?? []; | ||
|
|
||
| if (queryParamProp && bodyProp) { | ||
| const knownNames = [...pathNames, ...queryFieldNames]; | ||
| flatInputDestructure = `{ ${[...knownNames, `...${bodyProp.name}`].join(', ')} }: ${flatInputType},`; | ||
| } else if (queryParamProp) { | ||
| flatInputDestructure = `{ ${[...pathNames, '...params'].join(', ')} }: ${flatInputType},`; | ||
| } else if (bodyProp) { | ||
| flatInputDestructure = `{ ${[...pathNames, `...${bodyProp.name}`].join(', ')} }: ${flatInputType},`; | ||
| } | ||
| } | ||
|
|
||
| const flatProps = | ||
| useFlatInput && flatInputDestructure ? flatInputDestructure : queryProps; | ||
|
|
||
| const queryOptionsFn = `export const ${queryOptionsFnName} = <TData = ${TData}, TError = ${errorType}>(${httpFirstParam}${flatProps} ${queryArgumentsForOptions}) => { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The new flat This branch changes 🤖 Prompt for AI Agents |
||
|
|
||
| ${hookOptions} | ||
|
|
||
|
|
@@ -438,11 +488,6 @@ ${hookOptions} | |
|
|
||
| const queryHookName = camel(`${operationPrefix}-${name}`); | ||
|
|
||
| const overrideTypes = ` | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${definedInitialDataQueryPropsDefinitions} ${definedInitialDataQueryArguments} ${optionalQueryClientArgument}\n ): ${definedInitialDataReturnType} | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${queryPropDefinitions} ${undefinedInitialDataQueryArguments} ${optionalQueryClientArgument}\n ): ${returnType} | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${queryPropDefinitions} ${queryArguments} ${optionalQueryClientArgument}\n ): ${returnType}`; | ||
|
|
||
| const prefetch = generatePrefetch({ | ||
| usePrefetch, | ||
| type, | ||
|
|
@@ -506,6 +551,26 @@ export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${q | |
|
|
||
| const queryInvocationSuffix = adapter.getQueryInvocationSuffix(); | ||
|
|
||
| const hookProps = | ||
| useFlatInput && flatInputDestructure | ||
| ? flatInputDestructure | ||
| : adapter.getHookPropsDefinitions(props); | ||
|
|
||
| const hookPropsForDefinedInitialData = | ||
| useFlatInput && flatInputDestructure | ||
| ? flatInputDestructure | ||
| : definedInitialDataQueryPropsDefinitions; | ||
|
|
||
| const hookPropsForOverloads = | ||
| useFlatInput && flatInputDestructure | ||
| ? flatInputDestructure | ||
| : queryPropDefinitions; | ||
|
|
||
| const overrideTypes = ` | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${hookPropsForDefinedInitialData} ${definedInitialDataQueryArguments} ${optionalQueryClientArgument}\n ): ${definedInitialDataReturnType} | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${hookPropsForOverloads} ${undefinedInitialDataQueryArguments} ${optionalQueryClientArgument}\n ): ${returnType} | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${hookPropsForOverloads} ${queryArguments} ${optionalQueryClientArgument}\n ): ${returnType}`; | ||
|
|
||
| return ` | ||
| ${queryOptionsFn} | ||
|
|
||
|
|
@@ -516,9 +581,7 @@ export type ${pascal(name)}QueryError = ${errorType} | |
|
|
||
| ${adapter.shouldGenerateOverrideTypes() ? overrideTypes : ''} | ||
| ${doc} | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${adapter.getHookPropsDefinitions( | ||
| props, | ||
| )} ${queryArguments} ${optionalQueryClientArgument} \n ): ${returnType} { | ||
| export function ${queryHookName}<TData = ${TData}, TError = ${errorType}>(\n ${hookProps} ${queryArguments} ${optionalQueryClientArgument} \n ): ${returnType} { | ||
|
|
||
| ${queryInit} | ||
|
|
||
|
|
@@ -598,6 +661,12 @@ export const generateQueryHook = async ( | |
| const query = override.query; | ||
| const isRequestOptions = override.requestOptions !== false; | ||
| const operationQueryOptions = operations[operationId]?.query; | ||
|
|
||
| if (override.useFlatInput && override.useNamedParameters) { | ||
| throw new Error( | ||
| 'useFlatInput and useNamedParameters cannot be used together. useFlatInput already flattens all parameters into a single object.', | ||
| ); | ||
| } | ||
| const isExactOptionalPropertyTypes = | ||
| !!context.output.tsconfig?.compilerOptions?.exactOptionalPropertyTypes; | ||
|
|
||
|
|
@@ -827,6 +896,7 @@ ${queryKeyFns}`; | |
| useInvalidate: query.useInvalidate, | ||
| useSetQueryData: | ||
| operationQueryOptions?.useSetQueryData ?? query.useSetQueryData, | ||
| useFlatInput: !!override.useFlatInput, | ||
| adapter, | ||
| }); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also validate request-body field names here.
allNamesonly contains path and query names. If the body also exposespage,limit, ororgId, flat-input generation will silently pull that field out of...datainstead of rejecting the operation, even though this helper says body collisions are covered.🤖 Prompt for AI Agents