Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions packages/core/src/getters/query-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,5 +222,6 @@ export function getQueryParams({
deps: schemas,
isOptional: allOptional,
requiredNullableKeys,
fieldNames: types.map(({ name }) => name),
};
}
3 changes: 3 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ export interface NormalizedOverrideOutput {
useDeprecatedOperations?: boolean;
useBigInt?: boolean;
useNamedParameters?: boolean;
useFlatInput?: boolean;
enumGenerationType: EnumGeneration;
suppressReadonlyModifier?: boolean;
/**
Expand Down Expand Up @@ -521,6 +522,7 @@ export interface OverrideOutput {
useDeprecatedOperations?: boolean;
useBigInt?: boolean;
useNamedParameters?: boolean;
useFlatInput?: boolean;
enumGenerationType?: EnumGeneration;
suppressReadonlyModifier?: boolean;
/**
Expand Down Expand Up @@ -1203,6 +1205,7 @@ export interface GetterQueryParam {
isOptional: boolean;
originalSchema?: OpenApiSchemaObject;
requiredNullableKeys?: string[];
fieldNames?: string[];
}

export type GetterPropType =
Expand Down
34 changes: 34 additions & 0 deletions packages/query/src/flat-input.ts
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.`,
Comment on lines +27 to +36
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Also validate request-body field names here.

allNames only contains path and query names. If the body also exposes page, limit, or orgId, flat-input generation will silently pull that field out of ...data instead of rejecting the operation, even though this helper says body collisions are covered.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query/src/flat-input.ts` around lines 20 - 29, The duplicate-name
check currently builds allNames from pathParamNames and queryFieldNames only, so
body fields are not validated; update useFlatInput to also collect body field
names (e.g., requestBody.fieldNames or derive bodyFieldNames from the
operation's requestBody schema) and include them in allNames (const allNames =
[...pathParamNames, ...queryFieldNames, ...bodyFieldNames]); keep using the
existing seen Set and throw the same Error (including operationName) when a
duplicate is found so path, query, and body field name collisions are rejected
consistently.

);
}
seen.add(name);
}
}
87 changes: 73 additions & 14 deletions packages/query/src/mutation-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
getMutationRequestArgs,
getQueryErrorType,
} from './client';
import { checkFlatInputCollisions } from './flat-input';
import type { FrameworkAdapter } from './framework-adapter';
import { getQueryOptionsDefinition } from './query-options';

Expand Down Expand Up @@ -102,6 +103,7 @@ export const generateMutationHook = async ({
response,
operationId,
override,
queryParams,
} = verbOptions;
const { route, context, output } = options;
const query = override.query;
Expand All @@ -116,19 +118,72 @@ export const generateMutationHook = async ({
})
: undefined;

const definitions = props
.map(({ definition, type }) =>
type === GetterPropType.BODY
? mutator?.bodyTypeName
? `data: ${mutator.bodyTypeName}<${body.definition}>`
: `data: ${body.definition}`
: definition,
)
.join(';');
const useFlatInput = !!override.useFlatInput;

const properties = props
.map(({ name, type }) => (type === GetterPropType.BODY ? 'data' : name))
.join(',');
if (useFlatInput && props.length > 1) {
checkFlatInputCollisions(operationName, props, queryParams);
}

let definitions: string;
let properties: string;
let flatMutationCallArgs: string | undefined;

if (useFlatInput && props.length > 1) {
const pathParamProps = props.filter(
(p) =>
p.type === GetterPropType.PARAM ||
p.type === GetterPropType.NAMED_PATH_PARAMS,
);
const queryParamProp = props.find(
(p) => p.type === GetterPropType.QUERY_PARAM,
);
const bodyPropEntry = props.find((p) => p.type === GetterPropType.BODY);
const queryFieldNames = queryParams?.fieldNames ?? [];

const pathType =
pathParamProps.length > 0
? `{ ${pathParamProps.map((p) => p.definition).join('; ')} }`
: '';
const queryType = queryParamProp ? (queryParams?.schema.name ?? '') : '';
const bodyType = bodyPropEntry
? mutator?.bodyTypeName
? `${mutator.bodyTypeName}<${body.definition}>`
: body.definition
: '';
const types = [pathType, queryType, bodyType].filter(Boolean);
definitions = types.join(' & ');

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const knownNames = [
...pathParamProps.map((p) => p.name),
...queryFieldNames,
];
const hasBody = !!bodyPropEntry;
properties = hasBody
? [...knownNames, '...data'].join(', ')
: knownNames.join(', ');

const pathArgs = pathParamProps.map((p) => p.name).join(', ');
const queryArgs =
queryFieldNames.length > 0 ? `{ ${queryFieldNames.join(', ')} }` : '';
const bodyArg = hasBody ? 'data' : '';
flatMutationCallArgs = [pathArgs, queryArgs, bodyArg]
.filter(Boolean)
.join(', ');
} else {
definitions = props
.map(({ definition, type }) =>
type === GetterPropType.BODY
? mutator?.bodyTypeName
? `data: ${mutator.bodyTypeName}<${body.definition}>`
: `data: ${body.definition}`
: definition,
)
.join(';');

properties = props
.map(({ name, type }) => (type === GetterPropType.BODY ? 'data' : name))
.join(',');
}

const errorType = getQueryErrorType(
operationName,
Expand Down Expand Up @@ -221,11 +276,15 @@ ${hooksOptionImplementation}


const mutationFn: MutationFunction<Awaited<ReturnType<${dataType}>>, ${
definitions ? `{${definitions}}` : 'void'
definitions
? flatMutationCallArgs
? definitions
: `{${definitions}}`
: 'void'
}> = (${properties ? 'props' : ''}) => {
${properties ? `const {${properties}} = props ?? {};` : ''}

return ${operationName}(${adapter.getMutationHttpPrefix(mutator)}${properties}${
return ${operationName}(${adapter.getMutationHttpPrefix(mutator)}${flatMutationCallArgs ?? properties}${
properties ? ',' : ''
}${getMutationRequestArgs(isRequestOptions, httpClient, mutator)})
}
Expand Down
88 changes: 79 additions & 9 deletions packages/query/src/query-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -159,6 +160,7 @@ const generateQueryImplementation = ({
useInfinite,
useInvalidate,
useSetQueryData,
useFlatInput,
adapter,
}: {
queryOption: {
Expand Down Expand Up @@ -189,6 +191,7 @@ const generateQueryImplementation = ({
useInfinite?: boolean;
useInvalidate?: boolean;
useSetQueryData?: boolean;
useFlatInput: boolean;
adapter: FrameworkAdapter;
}) => {
const {
Expand Down Expand Up @@ -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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

useFlatInput still won't flatten queries with only path params.

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 props.length > 1.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query/src/query-generator.ts` around lines 388 - 393, The current
hasMultiplePropTypes check only counts categories and fails for cases like
multiple path params; change it to count actual exposed props instead of
categories. Replace the boolean expression for hasMultiplePropTypes with a
totalProps calculation that sums pathParams.length plus the number of props in
queryParamProp (if present) plus the number of props in bodyProp (if present),
then set hasMultiplePropTypes = totalProps > 1 so useFlatInput will flatten when
there are multiple exposed inputs (mirror the mutation generator's props.length
> 1 behavior).

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}) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

The new flat queryOptionsFn signature doesn't match its existing callers.

This branch changes queryOptionsFn to accept a single flattened object, but queryProperties / httpFunctionProps were already derived from the pre-flat prop shape, and Lines 498-506 still feed queryProps into generatePrefetch(). The new searchItems path+query+body case is exactly this shape, so the emitted query options/hooks keep calling the old split signature with missing or positional args.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/query/src/query-generator.ts` around lines 413 - 426, The new
queryOptionsFn signature was changed to a single flattened object (useFlatInput
/ flatInputDestructure) but callers still build and pass the old split props
(queryProps) into generatePrefetch and into queryProperties/httpFunctionProps,
causing mismatched args; update the code so that when useFlatInput is true you
derive queryProperties/httpFunctionProps from the flattened shape (flatProps /
flatInputDestructure) or simply pass flatProps into generatePrefetch and any
caller that previously used queryProps, ensuring queryProperties,
httpFunctionProps and generatePrefetch all consume the same flat shape as
queryOptionsFn.


${hookOptions}

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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}

Expand All @@ -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}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -827,6 +896,7 @@ ${queryKeyFns}`;
useInvalidate: query.useInvalidate,
useSetQueryData:
operationQueryOptions?.useSetQueryData ?? query.useSetQueryData,
useFlatInput: !!override.useFlatInput,
adapter,
});
}
Expand Down
Loading
Loading