Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
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);
}
}
1 change: 1 addition & 0 deletions packages/query/src/framework-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export interface FrameworkAdapter {
generateQueryArguments(args: {
operationName: string;
definitions: string;
overrideVariableType?: string;
mutator?: GeneratorMutator;
isRequestOptions: boolean;
type?: (typeof QueryType)[keyof typeof QueryType];
Expand Down
2 changes: 2 additions & 0 deletions packages/query/src/frameworks/angular.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export const createAngularAdapter = ({
generateQueryArguments({
operationName,
definitions,
overrideVariableType,
mutator,
isRequestOptions,
type,
Expand All @@ -205,6 +206,7 @@ export const createAngularAdapter = ({
operationName,
mutator,
definitions,
overrideVariableType,
type,
prefix,
hasQueryV5,
Expand Down
2 changes: 2 additions & 0 deletions packages/query/src/frameworks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ const withDefaults = (adapter: FrameworkAdapterConfig): FrameworkAdapter => ({
generateQueryArguments({
operationName,
definitions,
overrideVariableType,
mutator,
isRequestOptions,
type,
Expand All @@ -130,6 +131,7 @@ const withDefaults = (adapter: FrameworkAdapterConfig): FrameworkAdapter => ({
operationName,
mutator,
definitions,
overrideVariableType,
type,
prefix,
hasQueryV5: adapter.hasQueryV5,
Expand Down
2 changes: 2 additions & 0 deletions packages/query/src/frameworks/svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ export const createSvelteAdapter = ({
generateQueryArguments({
operationName,
definitions,
overrideVariableType,
mutator,
isRequestOptions,
type,
Expand All @@ -180,6 +181,7 @@ export const createSvelteAdapter = ({
operationName,
mutator,
definitions,
overrideVariableType,
type,
prefix,
hasQueryV5,
Expand Down
71 changes: 65 additions & 6 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,6 +118,12 @@ export const generateMutationHook = async ({
})
: undefined;

const useFlatInput = !!override.useFlatInput;

if (useFlatInput && props.length > 1) {
checkFlatInputCollisions(operationName, props, queryParams);
}

const definitions = props
.map(({ definition, type }) =>
type === GetterPropType.BODY
Expand All @@ -126,9 +134,56 @@ export const generateMutationHook = async ({
)
.join(';');

const properties = props
.map(({ name, type }) => (type === GetterPropType.BODY ? 'data' : name))
.join(',');
let properties: string;
let flatMutationCallArgs: string | undefined;
let flatVariableType: 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);
flatVariableType = types.join(' & ');

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 {
properties = props
.map(({ name, type }) => (type === GetterPropType.BODY ? 'data' : name))
.join(',');
}

const errorType = getQueryErrorType(
operationName,
Expand All @@ -145,6 +200,7 @@ export const generateMutationHook = async ({
operationName,
mutator,
definitions,
overrideVariableType: flatVariableType,
prefix: adapter.getQueryOptionsDefinitionPrefix(),
hasQueryV5: adapter.hasQueryV5,
hasQueryV5WithInfiniteQueryOptionsError:
Expand All @@ -171,6 +227,7 @@ export const generateMutationHook = async ({
const mutationArguments = adapter.generateQueryArguments({
operationName,
definitions,
overrideVariableType: flatVariableType,
mutator,
isRequestOptions,
httpClient,
Expand All @@ -181,6 +238,7 @@ export const generateMutationHook = async ({
const mutationArgumentsForOptions = adapter.generateQueryArguments({
operationName,
definitions,
overrideVariableType: flatVariableType,
mutator,
isRequestOptions,
httpClient,
Expand Down Expand Up @@ -221,11 +279,11 @@ ${hooksOptionImplementation}


const mutationFn: MutationFunction<Awaited<ReturnType<${dataType}>>, ${
definitions ? `{${definitions}}` : 'void'
flatVariableType ?? (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 Expand Up @@ -282,7 +340,8 @@ ${

const mutationReturnType = adapter.getMutationReturnType({
dataType,
variableType: definitions ? `{${definitions}}` : 'void',
variableType:
flatVariableType ?? (definitions ? `{${definitions}}` : 'void'),
});

const mutationHookBody = adapter.generateMutationHookBody({
Expand Down
Loading
Loading