Skip to content
Draft
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
4 changes: 4 additions & 0 deletions docs/content/docs/guides/angular-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@ const request$ = http.get<Pets>(url, { params: httpParams })

Validation is automatically skipped for primitive response types (`string`, `void`, etc.) and for operations using a custom mutator.

## Flat Input

When `useFlatInput: true` is set, Orval flattens path params, query params, and body into a single object argument.

## Full Example

See the [Angular Query sample](https://github.com/orval-labs/orval/tree/master/samples/angular-query) on GitHub.
12 changes: 12 additions & 0 deletions docs/content/docs/guides/react-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,18 @@ export const useSetListPetsQueryData = () => {
};
```

## Flat Input

When `useFlatInput: true` is set, Orval flattens path params, query params, and body into a single object argument:

```ts
// Before
useGetUserOrders(userId, params, options);

// After
useGetUserOrders({ userId, status, page }, options);
```

## Full Example

See the [complete React Query example](https://github.com/orval-labs/orval/blob/master/samples/react-query/basic) on GitHub.
4 changes: 4 additions & 0 deletions docs/content/docs/guides/solid-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ export const setListPetsQueryData = (
};
```

## Flat Input

When `useFlatInput: true` is set, Orval flattens path params, query params, and body into a single object argument.

## Full Example

See the [complete Solid Query example](https://github.com/orval-labs/orval/tree/master/samples/solid-query) on GitHub.
4 changes: 4 additions & 0 deletions docs/content/docs/guides/svelte-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ export const setListPetsQueryData = (
};
```

## Flat Input

When `useFlatInput: true` is set, Orval flattens path params, query params, and body into a single object argument.

## Full Example

See the [complete Svelte Query example](https://github.com/orval-labs/orval/tree/master/samples/svelte-query) on GitHub.
4 changes: 4 additions & 0 deletions docs/content/docs/guides/vue-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ export const setListPetsQueryData = (
};
```

## Flat Input

When `useFlatInput: true` is set, Orval flattens path params, query params, and body into a single object argument.

## Full Example

See the [complete Vue Query example](https://github.com/orval-labs/orval/tree/master/samples/vue-query) on GitHub.
9 changes: 9 additions & 0 deletions docs/content/docs/reference/configuration/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1286,6 +1286,15 @@ Use TypeScript `type` instead of `interface`.

Use named parameters object instead of positional arguments.

### useFlatInput

**Type:** `Boolean`
**Default:** `false`

Flatten path params, query params, and body into a single object argument for query hooks and mutations. Uses TypeScript intersection types.

Cannot be used together with `useNamedParameters`.

### useDeprecatedOperations

**Type:** `Boolean`
Expand Down
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
127 changes: 127 additions & 0 deletions packages/query/src/flat-input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
type GetterProps,
GetterPropType,
type GetterQueryParam,
} from '@orval/core';

export interface FlatInputResult {
type: string;
destructure: string;
properties: string;
callArgs: string;
}

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);
}
}

export function buildFlatInput(
props: GetterProps,
queryParams?: GetterQueryParam,
bodyDefinition?: string,
bodyTypeName?: string,
): FlatInputResult | undefined {
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 hasHeaders = props.some((p) => p.type === GetterPropType.HEADER);

if (hasHeaders) {
return undefined;
}

const propCount =
pathParams.length + (queryParamProp ? 1 : 0) + (bodyProp ? 1 : 0);

if (propCount < 2 && pathParams.length < 2) {
return undefined;
}

const pathType =
pathParams.length > 0
? `{ ${pathParams.map((p) => p.definition).join('; ')} }`
: '';
const queryType = queryParamProp ? (queryParams?.schema.name ?? '') : '';
const bodyType = bodyProp
? bodyTypeName
? `${bodyTypeName}<${bodyDefinition}>`
: (bodyDefinition ?? '')
: '';

const types = [pathType, queryType, bodyType].filter(Boolean);
const flatType = types.join(' & ');

if (!flatType) {
return undefined;
}

const pathNames = pathParams.map((p) => p.name);
const queryFieldNames = queryParams?.fieldNames ?? [];
const knownNames = [...pathNames, ...queryFieldNames];

let destructureNames: string[];
if (bodyProp) {
destructureNames = [...knownNames, '...data'];
} else if (queryParamProp) {
destructureNames = [...pathNames, '...params'];
} else {
destructureNames = [...pathNames];
}

const properties = destructureNames.join(', ');
const destructure = `{ ${properties} }: ${flatType},`;

const callArgs = props
.filter((p) => p.type !== GetterPropType.HEADER)
.map((p) => {
if (
p.type === GetterPropType.PARAM ||
p.type === GetterPropType.NAMED_PATH_PARAMS
) {
return p.name;
}
if (p.type === GetterPropType.QUERY_PARAM) {
return queryFieldNames.length > 0
? `{ ${queryFieldNames.join(', ')} }`
: p.name;
}
if (p.type === GetterPropType.BODY) {
return 'data';
}
return p.name;
})
.join(', ');

return { type: flatType, destructure, properties, callArgs };
}
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
29 changes: 23 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 { buildFlatInput, 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,15 @@ export const generateMutationHook = async ({
})
: undefined;

const useFlatInput = !!override.useFlatInput;

if (useFlatInput) {
checkFlatInputCollisions(operationName, props, queryParams);
}
const flatInput = useFlatInput
? buildFlatInput(props, queryParams, body.definition, mutator?.bodyTypeName)
: undefined;

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

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

const errorType = getQueryErrorType(
operationName,
Expand All @@ -145,6 +158,7 @@ export const generateMutationHook = async ({
operationName,
mutator,
definitions,
overrideVariableType: flatInput?.type,
prefix: adapter.getQueryOptionsDefinitionPrefix(),
hasQueryV5: adapter.hasQueryV5,
hasQueryV5WithInfiniteQueryOptionsError:
Expand All @@ -171,6 +185,7 @@ export const generateMutationHook = async ({
const mutationArguments = adapter.generateQueryArguments({
operationName,
definitions,
overrideVariableType: flatInput?.type,
mutator,
isRequestOptions,
httpClient,
Expand All @@ -181,6 +196,7 @@ export const generateMutationHook = async ({
const mutationArgumentsForOptions = adapter.generateQueryArguments({
operationName,
definitions,
overrideVariableType: flatInput?.type,
mutator,
isRequestOptions,
httpClient,
Expand Down Expand Up @@ -221,11 +237,11 @@ ${hooksOptionImplementation}


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

return ${operationName}(${adapter.getMutationHttpPrefix(mutator)}${properties}${
return ${operationName}(${adapter.getMutationHttpPrefix(mutator)}${flatInput?.callArgs ?? properties}${
properties ? ',' : ''
}${getMutationRequestArgs(isRequestOptions, httpClient, mutator)})
}
Expand Down Expand Up @@ -282,7 +298,8 @@ ${

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

const mutationHookBody = adapter.generateMutationHookBody({
Expand Down
Loading