diff --git a/.changeset/unwrap-return-type.md b/.changeset/unwrap-return-type.md new file mode 100644 index 000000000..8910c4f6c --- /dev/null +++ b/.changeset/unwrap-return-type.md @@ -0,0 +1,16 @@ +--- +'@kubb/plugin-axios': minor +'@kubb/plugin-fetch': minor +'@kubb/plugin-react-query': minor +'@kubb/plugin-vue-query': minor +'@kubb/plugin-swr': minor +--- + +Add a `returnType` option (`'full' | 'data'`, default `'full'`) to the standalone client +functions and the class-based SDK. `'data'` resolves a call to the bare success body instead of +the full `{ status, data, error, contentType, request, response }` result, once `throwOnError` +(on by default) rules out the error branch. + +`plugin-react-query`, `plugin-vue-query`, and `plugin-swr` now read this option off the +registered client plugin, so their generated hooks work with either setting instead of assuming +the full result. diff --git a/examples/advanced/src/gen/.kubb/client.ts b/examples/advanced/src/gen/.kubb/client.ts index 72bedcf64..2c6612afd 100644 --- a/examples/advanced/src/gen/.kubb/client.ts +++ b/examples/advanced/src/gen/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/axios/src/gen/.kubb/client.ts b/examples/axios/src/gen/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/examples/axios/src/gen/.kubb/client.ts +++ b/examples/axios/src/gen/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/fetch/src/gen/.kubb/client.ts b/examples/fetch/src/gen/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/examples/fetch/src/gen/.kubb/client.ts +++ b/examples/fetch/src/gen/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/mcp/src/gen/.kubb/client.ts b/examples/mcp/src/gen/.kubb/client.ts index ff65e0b9d..94b20e2b9 100644 --- a/examples/mcp/src/gen/.kubb/client.ts +++ b/examples/mcp/src/gen/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/react-query/src/gen/.kubb/client.ts b/examples/react-query/src/gen/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/examples/react-query/src/gen/.kubb/client.ts +++ b/examples/react-query/src/gen/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/sdk/src/gen/.kubb/client.ts b/examples/sdk/src/gen/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/examples/sdk/src/gen/.kubb/client.ts +++ b/examples/sdk/src/gen/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/simple-single/src/gen/.kubb/client.ts b/examples/simple-single/src/gen/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/examples/simple-single/src/gen/.kubb/client.ts +++ b/examples/simple-single/src/gen/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/swr/src/gen/.kubb/client.ts b/examples/swr/src/gen/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/examples/swr/src/gen/.kubb/client.ts +++ b/examples/swr/src/gen/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/examples/vue-query/src/gen/.kubb/client.ts b/examples/vue-query/src/gen/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/examples/vue-query/src/gen/.kubb/client.ts +++ b/examples/vue-query/src/gen/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/internals/client/src/builders/generics.test.ts b/internals/client/src/builders/generics.test.ts index 6f55ad37e..9d65bbd9a 100644 --- a/internals/client/src/builders/generics.test.ts +++ b/internals/client/src/builders/generics.test.ts @@ -1,7 +1,7 @@ import { ast } from 'kubb/kit' import { resolverTs } from '@kubb/plugin-ts' import { describe, expect, test } from 'vitest' -import { buildRequestResultGenerics } from './generics.ts' +import { buildRequestResultGenerics, buildResultType } from './generics.ts' const node = ast.factory.createOperation({ operationId: 'getPetById', @@ -17,3 +17,13 @@ describe('buildRequestResultGenerics', () => { expect(buildRequestResultGenerics({ node, types: resolverTs })).toBe('GetPetByIdResponses, ThrowOnError') }) }) + +describe('buildResultType', () => { + test('names RequestResult for the default full return type', () => { + expect(buildResultType({ node, types: resolverTs, returnType: 'full' })).toBe('RequestResult') + }) + + test('names UnwrappedResult when returnType is data', () => { + expect(buildResultType({ node, types: resolverTs, returnType: 'data' })).toBe('UnwrappedResult') + }) +}) diff --git a/internals/client/src/builders/generics.ts b/internals/client/src/builders/generics.ts index 451b61f7c..14390f2fd 100644 --- a/internals/client/src/builders/generics.ts +++ b/internals/client/src/builders/generics.ts @@ -1,5 +1,6 @@ import type { ast } from 'kubb/kit' import type { OperationTypeNames } from '../resolveOperationTypes.ts' +import type { ReturnTypeOption } from '../types.ts' /** * Builds the `RequestResult` generic arguments for one operation: the per-status responses record @@ -12,3 +13,16 @@ import type { OperationTypeNames } from '../resolveOperationTypes.ts' export function buildRequestResultGenerics({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): string { return `${types.response.responses(node)}, ThrowOnError` } + +/** + * Builds the result type name an operation's function signature and return statement use: + * `RequestResult` for the default `returnType: 'full'`, or the runtime's `UnwrappedResult` when + * `returnType: 'data'` narrows a resolved call down to the bare success body. + * + * @example + * `buildResultType({ node, types, returnType: 'data' }) // 'UnwrappedResult'` + */ +export function buildResultType({ node, types, returnType }: { node: ast.OperationNode; types: OperationTypeNames; returnType: ReturnTypeOption }): string { + const generics = buildRequestResultGenerics({ node, types }) + return returnType === 'data' ? `UnwrappedResult<${generics}>` : `RequestResult<${generics}>` +} diff --git a/internals/client/src/builders/returnStatement.test.ts b/internals/client/src/builders/returnStatement.test.ts index 27f5d005a..0029cf66f 100644 --- a/internals/client/src/builders/returnStatement.test.ts +++ b/internals/client/src/builders/returnStatement.test.ts @@ -14,8 +14,15 @@ const node = ast.factory.createOperation({ describe('buildReturnStatement', () => { test('forwards the call config and casts to the operation RequestResult', () => { const callConfig = "{ method: 'POST', url: '/pet', ...config }" - expect(buildReturnStatement({ node, types: resolverTs, callConfig })).toBe( + expect(buildReturnStatement({ node, types: resolverTs, callConfig, returnType: 'full' })).toBe( "return request({ method: 'POST', url: '/pet', ...config }) as Promise>", ) }) + + test('routes the call through unwrapResult when returnType is data', () => { + const callConfig = "{ method: 'POST', url: '/pet', ...config }" + expect(buildReturnStatement({ node, types: resolverTs, callConfig, returnType: 'data' })).toBe( + "return unwrapResult(request({ method: 'POST', url: '/pet', ...config }), config.throwOnError) as Promise>", + ) + }) }) diff --git a/internals/client/src/builders/returnStatement.ts b/internals/client/src/builders/returnStatement.ts index 19d680019..835115e03 100644 --- a/internals/client/src/builders/returnStatement.ts +++ b/internals/client/src/builders/returnStatement.ts @@ -1,15 +1,35 @@ import type { ast } from 'kubb/kit' import type { OperationTypeNames } from '../resolveOperationTypes.ts' -import { buildRequestResultGenerics } from './generics.ts' +import type { ReturnTypeOption } from '../types.ts' +import { buildResultType } from './generics.ts' /** - * Builds the return statement of a generated operation function. The runtime call already resolves - * to `{ data, error, request, response }`; the generated code forwards that result and casts it to - * the operation's `RequestResult`, which carries the `throwOnError` discrimination. + * Builds the return statement of a generated operation function. With the default + * `returnType: 'full'` the runtime call already resolves to `{ data, error, request, response }`, + * so the generated code just forwards that result and casts it to the operation's `RequestResult`. + * With `returnType: 'data'` it instead routes the call through the runtime's `unwrapResult`, which + * narrows the resolved value down to the bare success body the same way the `RequestResult` type + * already does, keeping the `throwOnError` default in one place instead of restating it per call. * * @example * `return request({ method: 'POST', url: '/pet', ...config }) as Promise>` + * @example + * `return unwrapResult(request({ method: 'POST', url: '/pet', ...config }), config.throwOnError) as Promise>` */ -export function buildReturnStatement({ node, types, callConfig }: { node: ast.OperationNode; types: OperationTypeNames; callConfig: string }): string { - return `return request(${callConfig}) as Promise>` +export function buildReturnStatement({ + node, + types, + callConfig, + returnType, +}: { + node: ast.OperationNode + types: OperationTypeNames + callConfig: string + returnType: ReturnTypeOption +}): string { + const resultType = buildResultType({ node, types, returnType }) + if (returnType === 'data') { + return `return unwrapResult(request(${callConfig}), config.throwOnError) as Promise<${resultType}>` + } + return `return request(${callConfig}) as Promise<${resultType}>` } diff --git a/internals/client/src/builders/sdkMethod.test.ts b/internals/client/src/builders/sdkMethod.test.ts index 9b979c22d..444135085 100644 --- a/internals/client/src/builders/sdkMethod.test.ts +++ b/internals/client/src/builders/sdkMethod.test.ts @@ -17,7 +17,7 @@ const node = ast.factory.createOperation({ describe('buildSdkMethod', () => { test('builds the call config without remapping, since query param names already match the spec', () => { - const method = buildSdkMethod({ node, name: 'updatePet', types: resolverTs, validator: undefined }) + const method = buildSdkMethod({ node, name: 'updatePet', types: resolverTs, validator: undefined, returnType: 'full' }) expect(method).toContain("url: '/pets/{pet_id}', ...config") expect(method).not.toContain('include_deleted') diff --git a/internals/client/src/builders/sdkMethod.ts b/internals/client/src/builders/sdkMethod.ts index c569a5cc6..aa5a986ec 100644 --- a/internals/client/src/builders/sdkMethod.ts +++ b/internals/client/src/builders/sdkMethod.ts @@ -3,7 +3,7 @@ import { buildJSDoc } from '@internals/utils' import { ast } from 'kubb/kit' import type { ResolverZod } from '@kubb/plugin-zod' import type { OperationTypeNames } from '../resolveOperationTypes.ts' -import type { ValidatorOptions } from '../types.ts' +import type { ReturnTypeOption, ValidatorOptions } from '../types.ts' import { buildReturnStatement } from './returnStatement.ts' import { type Auth, buildSecurityMetadata } from './security.ts' import { buildGroupedOptionsSignature } from './signature.ts' @@ -57,6 +57,7 @@ export function buildSdkMethod({ zodResolver, validator, security, + returnType, }: { node: ast.OperationNode name: string @@ -64,12 +65,13 @@ export function buildSdkMethod({ zodResolver?: ResolverZod | null validator: ValidatorOptions | undefined security?: Array + returnType: ReturnTypeOption }): string { if (!ast.isHttpOperationNode(node)) return '' - const signature = buildGroupedOptionsSignature({ node, types }) + const signature = buildGroupedOptionsSignature({ node, types, returnType }) const callConfig = buildCallConfig({ node, validator, zodResolver, security }) - const returnStatement = buildReturnStatement({ node, types, callConfig }) + const returnStatement = buildReturnStatement({ node, types, callConfig, returnType }) const generics = signature.generics.length ? `<${signature.generics.join(', ')}>` : '' const jsdoc = buildJSDoc(buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true })) diff --git a/internals/client/src/builders/signature.test.ts b/internals/client/src/builders/signature.test.ts index a3efd0c71..09c2f0bcb 100644 --- a/internals/client/src/builders/signature.test.ts +++ b/internals/client/src/builders/signature.test.ts @@ -31,18 +31,23 @@ const listPets = ast.factory.createOperation({ describe('buildGroupedOptionsSignature', () => { test('emits a single grouped options parameter with a ThrowOnError generic', () => { - const signature = buildGroupedOptionsSignature({ node: addPet, types: resolverTs }) + const signature = buildGroupedOptionsSignature({ node: addPet, types: resolverTs, returnType: 'full' }) expect(signature.paramsSignature).toBe('options: Options') expect(signature.generics).toStrictEqual(['ThrowOnError extends boolean = true']) }) test('defaults the options parameter when the operation has no required request data', () => { - const signature = buildGroupedOptionsSignature({ node: listPets, types: resolverTs }) + const signature = buildGroupedOptionsSignature({ node: listPets, types: resolverTs, returnType: 'full' }) expect(signature.paramsSignature).toBe('options: Options = {}') }) test('keys the return type on the plugin-ts per-status responses record', () => { - const signature = buildGroupedOptionsSignature({ node: addPet, types: resolverTs }) + const signature = buildGroupedOptionsSignature({ node: addPet, types: resolverTs, returnType: 'full' }) expect(signature.returnType).toBe('Promise>') }) + + test('keys the return type on UnwrappedResult when returnType is data', () => { + const signature = buildGroupedOptionsSignature({ node: addPet, types: resolverTs, returnType: 'data' }) + expect(signature.returnType).toBe('Promise>') + }) }) diff --git a/internals/client/src/builders/signature.ts b/internals/client/src/builders/signature.ts index efd4f12e8..51c1fc305 100644 --- a/internals/client/src/builders/signature.ts +++ b/internals/client/src/builders/signature.ts @@ -2,7 +2,8 @@ import type { ast } from 'kubb/kit' import { getRequestGroupOptionality } from '@internals/shared' import { createFunctionParameter, createFunctionParameters, functionPrinter } from '@kubb/plugin-ts' import type { OperationTypeNames } from '../resolveOperationTypes.ts' -import { buildRequestResultGenerics } from './generics.ts' +import type { ReturnTypeOption } from '../types.ts' +import { buildResultType } from './generics.ts' const declarationPrinter = functionPrinter({ mode: 'declaration' }) @@ -33,9 +34,16 @@ export type GroupedOptionsSignature = { * per-operation input type has to be emitted. Both names come from `types`, which is `plugin-ts` or * `plugin-zod`'s inferred types (see `resolveOperationTypes`). */ -export function buildGroupedOptionsSignature({ node, types }: { node: ast.OperationNode; types: OperationTypeNames }): GroupedOptionsSignature { +export function buildGroupedOptionsSignature({ + node, + types, + returnType, +}: { + node: ast.OperationNode + types: OperationTypeNames + returnType: ReturnTypeOption +}): GroupedOptionsSignature { const optionsName = types.response.options(node) - const resultGenerics = buildRequestResultGenerics({ node, types }) const { isOptional } = getRequestGroupOptionality(node) const paramsSignature = @@ -47,7 +55,7 @@ export function buildGroupedOptionsSignature({ node, types }: { node: ast.Operat return { paramsSignature, - returnType: `Promise>`, + returnType: `Promise<${buildResultType({ node, types, returnType })}>`, generics: ['ThrowOnError extends boolean = true'], } } diff --git a/internals/client/src/components/Operation.tsx b/internals/client/src/components/Operation.tsx index a1f3baa7b..24cb5a365 100644 --- a/internals/client/src/components/Operation.tsx +++ b/internals/client/src/components/Operation.tsx @@ -9,7 +9,7 @@ import { buildGroupedOptionsSignature } from '../builders/signature.ts' import { buildStyles } from '../builders/styles.ts' import { buildValidatorHooks } from '../builders/validator.ts' import type { OperationTypeNames } from '../resolveOperationTypes.ts' -import type { ValidatorOptions } from '../types.ts' +import type { ReturnTypeOption, ValidatorOptions } from '../types.ts' type Props = { /** @@ -33,6 +33,10 @@ type Props = { * The active validator option, driving the validator-hook wiring. */ validator?: ValidatorOptions + /** + * Shape of the value the generated function resolves to. + */ + returnType: ReturnTypeOption /** * Per-operation security, resolved from the spec into inline `Auth` objects and serialized onto the * call config's `security` field for the runtime `auth` resolver to consume. @@ -47,10 +51,10 @@ type Props = { * single `options` object to the resolved client and returns the `RequestResult`. The type, signature, * and call config are built with the AST factory, and only the jsx-renderer emits the source. */ -export function Operation({ name, node, types, zodResolver, validator, security, isExportable = true, isIndexable = true }: Props): KubbReactNode { +export function Operation({ name, node, types, zodResolver, validator, returnType, security, isExportable = true, isIndexable = true }: Props): KubbReactNode { if (!ast.isHttpOperationNode(node)) return null - const signature = buildGroupedOptionsSignature({ node, types }) + const signature = buildGroupedOptionsSignature({ node, types, returnType }) const validators = buildValidatorHooks({ node, validator, zodResolver }) const securityLiteral = buildSecurityMetadata({ security }) const stylesLiteral = buildStyles({ node }) @@ -94,8 +98,10 @@ export function Operation({ name, node, types, zodResolver, validator, security, .join(', ')} }` const eventType = `SuccessOf<${types.response.responses(node)}>` - const returnType = eventStream ? `Promise>` : signature.returnType - const returnStatement = eventStream ? `return toEventStream<${eventType}>(request(${callConfig}))` : buildReturnStatement({ node, types, callConfig }) + const functionReturnType = eventStream ? `Promise>` : signature.returnType + const returnStatement = eventStream + ? `return toEventStream<${eventType}>(request(${callConfig}))` + : buildReturnStatement({ node, types, callConfig, returnType }) return ( @@ -104,7 +110,7 @@ export function Operation({ name, node, types, zodResolver, validator, security, export={isExportable} generics={signature.generics} params={signature.paramsSignature} - returnType={returnType} + returnType={functionReturnType} JSDoc={{ comments: buildOperationComments(node, { link: 'urlPath', linkPosition: 'beforeDeprecated', splitLines: true }) }} > {mergeContentType ? 'const { client: request = client, contentType, ...config } = options' : 'const { client: request = client, ...config } = options'} diff --git a/internals/client/src/components/SdkClient.tsx b/internals/client/src/components/SdkClient.tsx index 8d3616b12..fe3a884f4 100644 --- a/internals/client/src/components/SdkClient.tsx +++ b/internals/client/src/components/SdkClient.tsx @@ -5,7 +5,7 @@ import type { KubbReactNode } from 'kubb/jsx' import { buildSdkMethod } from '../builders/sdkMethod.ts' import type { Auth } from '../builders/security.ts' import type { OperationTypeNames } from '../resolveOperationTypes.ts' -import type { ValidatorOptions } from '../types.ts' +import type { ReturnTypeOption, ValidatorOptions } from '../types.ts' type OperationData = { node: ast.OperationNode @@ -21,6 +21,7 @@ type Props = { isIndexable?: boolean operations: Array validator: ValidatorOptions | undefined + returnType: ReturnTypeOption children?: KubbReactNode } @@ -30,7 +31,7 @@ type Props = { * instance: `const api = new PetClient({ baseURL }); api.getPetById(...)`. A per-call `client` option * still overrides the instance client for a one-off call. */ -export function SdkClient({ name, isExportable = true, isIndexable = true, operations, validator, children }: Props): KubbReactNode { +export function SdkClient({ name, isExportable = true, isIndexable = true, operations, validator, returnType, children }: Props): KubbReactNode { const methods = operations.map(({ node, name: methodName, types, zodResolver, security }) => buildSdkMethod({ node, @@ -39,6 +40,7 @@ export function SdkClient({ name, isExportable = true, isIndexable = true, opera zodResolver, validator, security, + returnType, }), ) diff --git a/internals/client/src/generators/clientGenerator.tsx b/internals/client/src/generators/clientGenerator.tsx index eca24ef39..ce0c8db8b 100644 --- a/internals/client/src/generators/clientGenerator.tsx +++ b/internals/client/src/generators/clientGenerator.tsx @@ -25,7 +25,7 @@ export function createClientGenerator(na if (!ast.isHttpOperationNode(node)) return null const { config, driver, resolver, root } = ctx - const { output, validator, group } = ctx.options + const { output, validator, returnType, group } = ctx.options const types = resolveOperationTypes(driver) if (!types) { @@ -87,9 +87,13 @@ export function createClientGenerator(na banner={resolver.default.banner(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })} footer={resolver.default.footer(ctx.meta, { output, config, file: { path: meta.file.path, baseName: meta.file.baseName } })} > - + (na {meta.fileZod && importedZodNames.length > 0 && } - + ) }, diff --git a/internals/client/src/generators/sdkGenerator.tsx b/internals/client/src/generators/sdkGenerator.tsx index 08f71f0f8..eb2a780b7 100644 --- a/internals/client/src/generators/sdkGenerator.tsx +++ b/internals/client/src/generators/sdkGenerator.tsx @@ -138,7 +138,7 @@ export function createSdkGenerator(): Ge renderer: jsxRenderer, operations(nodes, ctx) { const { config, resolver, root } = ctx - const { output, group, validator, sdk } = ctx.options + const { output, group, validator, returnType, sdk } = ctx.options if (!sdk) return null @@ -165,8 +165,13 @@ export function createSdkGenerator(): Ge return ( - - + + {validator === 'zod' && ops.some((op) => op.node.requestBody?.content?.[0]?.schema != null) && } @@ -179,7 +184,7 @@ export function createSdkGenerator(): Ge ))} - + ) } diff --git a/internals/client/src/index.ts b/internals/client/src/index.ts index 04590d99e..18f280032 100644 --- a/internals/client/src/index.ts +++ b/internals/client/src/index.ts @@ -23,4 +23,4 @@ export type { ClientOperation } from './resolveClientOperation.ts' export { resolveOperationTypes } from './resolveOperationTypes.ts' export type { OperationTypeNames, OperationTypeSource } from './resolveOperationTypes.ts' export { resolverClient } from './resolver.ts' -export type { ContractClientFactory, Mode, Options, ValidatorOptions, ResolvedOptions, ResolverClient } from './types.ts' +export type { ContractClientFactory, Mode, Options, ReturnTypeOption, ValidatorOptions, ResolvedOptions, ResolverClient } from './types.ts' diff --git a/internals/client/src/resolveClientOperation.test.ts b/internals/client/src/resolveClientOperation.test.ts index 2ca076161..2cb71adfc 100644 --- a/internals/client/src/resolveClientOperation.test.ts +++ b/internals/client/src/resolveClientOperation.test.ts @@ -58,7 +58,24 @@ describe('resolveClientOperation', () => { cache: createTestCache(), }) - expect(result).toStrictEqual({ name: 'getPetById', path: '/root/getPetById.ts', clientPath: '/root/.kubb/client.ts' }) + expect(result).toStrictEqual({ name: 'getPetById', path: '/root/getPetById.ts', clientPath: '/root/.kubb/client.ts', returnType: 'full' }) + }) + + test("reads the client plugin's returnType, defaulting to 'full' when unset", () => { + const node = ast.factory.createOperation({ operationId: 'getPetById', method: 'GET', path: '/pets/{id}' }) + const driver = createDriver() + driver.getPlugin.mockReturnValueOnce({ options: { returnType: 'data' } }) + + const result = resolveClientOperation({ + clientPlugin: { pluginName: 'plugin-fetch' }, + driver, + node, + root: '/root', + output: { path: '.' }, + cache: createTestCache(), + }) + + expect(result?.returnType).toBe('data') }) test('reuses the cached result for the same client plugin and node instead of resolving again', () => { diff --git a/internals/client/src/resolveClientOperation.ts b/internals/client/src/resolveClientOperation.ts index 08a8bf78c..0914b1afd 100644 --- a/internals/client/src/resolveClientOperation.ts +++ b/internals/client/src/resolveClientOperation.ts @@ -4,10 +4,10 @@ import type { ast, Group, NodeCache, Output, Resolver } from 'kubb/kit' /** * The resolved contract `` for one operation: the generated function name, the file it lives in, - * and the contract runtime's `.kubb/client.ts` path (where `RequestConfig` / `ResponseErrorConfig` - * come from). + * the contract runtime's `.kubb/client.ts` path (where `RequestConfig` / `ResponseErrorConfig` + * come from), and the registered client plugin's `returnType`. */ -export type ClientOperation = { name: string; path: string; clientPath: string } +export type ClientOperation = { name: string; path: string; clientPath: string; returnType: 'full' | 'data' } /** * Resolves the contract client `` a consumer (query hook, MCP handler) imports, by looking up @@ -20,11 +20,14 @@ export type ClientOperation = { name: string; path: string; clientPath: string } * so several dependents reading the same client plugin for one operation in a single pass * (react-query's query/mutation/infinite generators, vue-query, swr, the MCP handler, ...) share * one computed result instead of each re-deriving the name and path. + * + * `returnType` mirrors the client plugin's own `returnType` option (`'full'` when unset), so a + * dependent's generated call body can match it instead of assuming the full `RequestResult` shape. */ export function resolveClientOperation(options: { clientPlugin: { pluginName: string } | null driver: { - getPlugin: (name: string) => { options?: { output?: Output; group?: Group | null } } | undefined + getPlugin: (name: string) => { options?: { output?: Output; group?: Group | null; returnType?: 'full' | 'data' } } | undefined getResolver: (name: string) => Resolver } node: ast.OperationNode @@ -45,6 +48,11 @@ export function resolveClientOperation(options: { group: plugin?.options?.group ?? undefined, }) - return { name: resolver.name(node.operationId), path: file.path, clientPath: path.resolve(root, '.kubb/client.ts') } + return { + name: resolver.name(node.operationId), + path: file.path, + clientPath: path.resolve(root, '.kubb/client.ts'), + returnType: plugin?.options?.returnType ?? 'full', + } }) } diff --git a/internals/client/src/types.ts b/internals/client/src/types.ts index 0c15e4b0d..9f10e8dbb 100644 --- a/internals/client/src/types.ts +++ b/internals/client/src/types.ts @@ -18,6 +18,15 @@ export type ValidatorOptions = false | 'zod' | { request?: 'zod'; response?: 'zo */ export type Mode = 'tag' | 'flat' +/** + * Shape of the value a generated operation function resolves to. + * - `'full'`: the complete `{ status, data, error, contentType, request, response }` result. + * - `'data'`: the bare success body once `throwOnError` (on by default) narrows away the error + * branch, falling back to the full result when a call sets `throwOnError: false` and still + * needs `error` to discriminate a failed response. + */ +export type ReturnTypeOption = 'full' | 'data' + /** * The resolver shared by the client plugins. Inherits the built-in camelCase `name` and `file`; * classes and tag groups use PascalCase (with a `Client` suffix for groups). @@ -73,6 +82,20 @@ export type Options = OutputOptions & { * @default false */ validator?: ValidatorOptions + /** + * Shape of the value a generated operation function resolves to. Applies to the standalone + * functions and the class-based SDK; not to the query-hook plugins (`@kubb/plugin-react-query`, + * `@kubb/plugin-vue-query`, `@kubb/plugin-swr`), which keep calling the client directly and + * expect the full result. + * + * @default 'full' + * @example + * ```ts + * pluginAxios({ returnType: 'data' }) + * // const pet = await getPetById({ path: { petId: 1 } }) // Pet, not { status, data, ... } + * ``` + */ + returnType?: ReturnTypeOption /** * Generates a class-based SDK instead of the standalone functions. Each tag client is an instance * class whose constructor takes a client config and builds its own client, so every environment is @@ -140,6 +163,7 @@ export type ResolvedOptions = { group: Group | null baseURL: Options['baseURL'] validator: NonNullable + returnType: ReturnTypeOption sdk: | { mode: Mode diff --git a/internals/tanstack-query/src/components/InfiniteQueryOptions.tsx b/internals/tanstack-query/src/components/InfiniteQueryOptions.tsx index 65769576a..1787524a0 100644 --- a/internals/tanstack-query/src/components/InfiniteQueryOptions.tsx +++ b/internals/tanstack-query/src/components/InfiniteQueryOptions.tsx @@ -5,7 +5,15 @@ import { createFunctionParameters, functionPrinter } from '@kubb/plugin-ts' import { File, Function } from 'kubb/jsx' import type { KubbReactNode } from 'kubb/jsx' import type { Infinite } from '../types.ts' -import { buildClientCall, buildGroupedRequestParam, buildQueryOptionsParams, buildResponseTypes, queryKeyGroupOrder, resolvePageParamType } from '../utils.ts' +import { + buildCallResultBody, + buildClientCall, + buildGroupedRequestParam, + buildQueryOptionsParams, + buildResponseTypes, + queryKeyGroupOrder, + resolvePageParamType, +} from '../utils.ts' type Props = { name: string @@ -33,6 +41,12 @@ type Props = { * Unwraps a request group inside the client call, used by vue-query to emit `toValue(...)`. */ unwrapName?: (name: string) => string + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } const declarationPrinter = functionPrinter({ mode: 'declaration' }) @@ -52,6 +66,7 @@ export function InfiniteQueryOptions({ queryKeyType = 'typeof queryKey', memberTypeWrapper, unwrapName, + returnType = 'full', }: Props): KubbReactNode { const { TData: queryFnDataType, TError: errorType } = buildResponseTypes(node, tsResolver) @@ -63,8 +78,7 @@ export function InfiniteQueryOptions({ const paramsNode = buildQueryOptionsParams(node, { resolver: tsResolver, memberTypeWrapper }) const paramsSignature = declarationPrinter.print(paramsNode) ?? '' - const queryFnBody = `const { data } = await ${buildClientCall(node, { clientName, signal: true, unwrapName })} - return data` + const queryFnBody = buildCallResultBody(buildClientCall(node, { clientName, signal: true, unwrapName }), { returnType, indent: ' ' }) const hasNewParams = nextParam != null || previousParam != null diff --git a/internals/tanstack-query/src/index.ts b/internals/tanstack-query/src/index.ts index 88bbd5139..a8f48a394 100644 --- a/internals/tanstack-query/src/index.ts +++ b/internals/tanstack-query/src/index.ts @@ -7,6 +7,7 @@ export { buildGroupedRequestParam, buildQueryKeyParams, buildQueryOptionsParams, + buildCallResultBody, buildClientCall, buildResponseTypes, classifyOperation, diff --git a/internals/tanstack-query/src/utils.test.ts b/internals/tanstack-query/src/utils.test.ts index 755c68dbd..c1c381750 100644 --- a/internals/tanstack-query/src/utils.test.ts +++ b/internals/tanstack-query/src/utils.test.ts @@ -1,6 +1,6 @@ import { ast } from 'kubb/kit' import { describe, expect, test } from 'vitest' -import { classifyOperation, hasQueryKeyParams } from './utils.ts' +import { buildCallResultBody, classifyOperation, hasQueryKeyParams } from './utils.ts' describe('classifyOperation', () => { test('classifies a GET as a query when methods include it', () => { @@ -104,3 +104,19 @@ describe('hasQueryKeyParams', () => { expect(hasQueryKeyParams(node)).toBe(true) }) }) + +describe('buildCallResultBody', () => { + test('reads data off the full result by default', () => { + expect(buildCallResultBody('getPetById({ throwOnError: true })')).toBe('const { data } = await getPetById({ throwOnError: true })\n return data') + }) + + test('indents the second line to match the caller', () => { + expect(buildCallResultBody('getPetById({ throwOnError: true })', { indent: ' ' })).toBe( + 'const { data } = await getPetById({ throwOnError: true })\n return data', + ) + }) + + test('returns the call directly when the client already resolves to bare data', () => { + expect(buildCallResultBody('getPetById({ throwOnError: true })', { returnType: 'data' })).toBe('return await getPetById({ throwOnError: true })') + }) +}) diff --git a/internals/tanstack-query/src/utils.ts b/internals/tanstack-query/src/utils.ts index 586c4b2e3..4c8737d7a 100644 --- a/internals/tanstack-query/src/utils.ts +++ b/internals/tanstack-query/src/utils.ts @@ -150,6 +150,28 @@ export function buildClientCall(node: ast.OperationNode, options: { clientName: return `${clientName}({ ${args.join(', ')} })` } +/** + * Builds the query/mutation body that resolves a `buildClientCall` expression down to the bare + * success body. `returnType` mirrors the registered client plugin's own `returnType` option: with + * `'data'` the call already resolves to the bare body, so it is returned directly; with `'full'` + * (the default) the body is read off the resolved `RequestResult`. `indent` matches the second + * line to the caller's own template, since the body is embedded as a raw string, not reprinted. + * + * @example + * ```ts + * buildCallResultBody(buildClientCall(node, { clientName: 'getPetById' })) + * // const { data } = await getPetById({ ...config, throwOnError: true }) + * // return data + * ``` + */ +export function buildCallResultBody(call: string, options: { returnType?: 'full' | 'data'; indent?: string } = {}): string { + const { returnType = 'full', indent = ' ' } = options + if (returnType === 'data') { + return `return await ${call}` + } + return `const { data } = await ${call}\n${indent}return data` +} + type ResponseTypes = { TData: string TError: string diff --git a/packages/plugin-axios/src/generators/__snapshots__/getPetByIdWithReturnTypeData/getPetById.ts b/packages/plugin-axios/src/generators/__snapshots__/getPetByIdWithReturnTypeData/getPetById.ts new file mode 100644 index 000000000..5d1eaebd5 --- /dev/null +++ b/packages/plugin-axios/src/generators/__snapshots__/getPetByIdWithReturnTypeData/getPetById.ts @@ -0,0 +1,18 @@ +/* eslint-disable no-alert, no-console */ + +import type { Options, UnwrappedResult } from './.kubb/client' +import type { GetPetByIdOptions, GetPetByIdResponses } from './GetPetById' +import { client, unwrapResult } from './.kubb/client' + +/** + * {@link /pet/:petId} + */ +export function getPetById( + options: Options, +): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/pet/{petId}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > +} diff --git a/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/petClient.ts b/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/petClient.ts new file mode 100644 index 000000000..1da7708b8 --- /dev/null +++ b/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/petClient.ts @@ -0,0 +1,40 @@ +/* eslint-disable no-alert, no-console */ + +import type { ClientConfig, ClientInstance, Options, UnwrappedResult } from './.kubb/client' +import type { DeletePetOptions, DeletePetResponses } from './DeletePet' +import type { GetPetByIdOptions, GetPetByIdResponses } from './GetPetById' +import { createClient, unwrapResult } from './.kubb/client' + +export class PetClient { + private readonly client: ClientInstance + + constructor(config: ClientConfig = {}) { + this.client = createClient(config) + } + + /** + * {@link /pet/:petId} + */ + public getPetById( + options: Options, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/pet/{petId}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } + + /** + * {@link /pet/:petId} + */ + public deletePet( + options: Options, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'DELETE', url: '/pet/{petId}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } +} diff --git a/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/projectClient.ts b/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/projectClient.ts new file mode 100644 index 000000000..110c5b56e --- /dev/null +++ b/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/projectClient.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-alert, no-console */ + +import type { ClientConfig, ClientInstance, Options, UnwrappedResult } from './.kubb/client' +import type { GetProjectOptions, GetProjectResponses } from './GetProject' +import { createClient, unwrapResult } from './.kubb/client' + +export class ProjectClient { + private readonly client: ClientInstance + + constructor(config: ClientConfig = {}) { + this.client = createClient(config) + } + + /** + * {@link /projects/:project_id} + */ + public getProject( + options: Options, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/projects/{project_id}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } +} diff --git a/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/storeClient.ts b/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/storeClient.ts new file mode 100644 index 000000000..0a617f778 --- /dev/null +++ b/packages/plugin-axios/src/generators/__snapshots__/sdkClassWithReturnTypeData/storeClient.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-alert, no-console */ + +import type { ClientConfig, ClientInstance, Options, UnwrappedResult } from './.kubb/client' +import type { GetInventoryOptions, GetInventoryResponses } from './GetInventory' +import { createClient, unwrapResult } from './.kubb/client' + +export class StoreClient { + private readonly client: ClientInstance + + constructor(config: ClientConfig = {}) { + this.client = createClient(config) + } + + /** + * {@link /store/inventory} + */ + public getInventory( + options: Options = {}, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/store/inventory', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } +} diff --git a/packages/plugin-axios/src/generators/clientGenerator.test.tsx b/packages/plugin-axios/src/generators/clientGenerator.test.tsx index 5f7974176..4d6ab5ffc 100644 --- a/packages/plugin-axios/src/generators/clientGenerator.test.tsx +++ b/packages/plugin-axios/src/generators/clientGenerator.test.tsx @@ -28,6 +28,7 @@ const defaultOptions: PluginAxios['resolvedOptions'] = { group: null, baseURL: undefined, validator: false, + returnType: 'full', sdk: undefined, resolver: resolverClient, } @@ -192,6 +193,8 @@ describe('clientGenerator operation', () => { // text/event-stream response returns a typed event stream instead of a one-shot result. { name: 'streamEventsSse', node: streamEventsNode, options: {} }, { name: 'addPetMultiStatusWithZod', node: createPetNode, options: { validator: 'zod' as const } }, + // returnType: 'data' unwraps the resolved call down to the bare success body. + { name: 'getPetByIdWithReturnTypeData', node: getPetByIdNode, options: { returnType: 'data' as const } }, // Two requirements referencing two schemes (oauth2 bearer + apiKey header). { name: 'getPetByIdWithSecurity', node: getPetByIdNode, options: {}, adapter: mockedAdapterWithDocument(securityDocument) }, ] as const satisfies Array<{ name: string; node: ast.OperationNode; options: Partial; adapter?: Adapter }> diff --git a/packages/plugin-axios/src/generators/sdkGenerator.test.tsx b/packages/plugin-axios/src/generators/sdkGenerator.test.tsx index 530c9172a..48e0400d3 100644 --- a/packages/plugin-axios/src/generators/sdkGenerator.test.tsx +++ b/packages/plugin-axios/src/generators/sdkGenerator.test.tsx @@ -27,6 +27,7 @@ const defaultOptions: PluginAxios['resolvedOptions'] = { group: null, baseURL: undefined, validator: false, + returnType: 'full', sdk: { mode: 'tag', name: undefined }, resolver: resolverClient, } @@ -95,6 +96,8 @@ describe('sdkGenerator operations', () => { { name: 'sdkClass', options: {} as Partial }, { name: 'sdkClassWithName', options: { sdk: { mode: 'tag', name: 'PetStore' } } as Partial }, { name: 'sdkSingle', options: { sdk: { mode: 'flat', name: 'PetStore' } } as Partial }, + // returnType: 'data' unwraps every SDK method down to the bare success body. + { name: 'sdkClassWithReturnTypeData', options: { returnType: 'data' } as Partial }, ] as const satisfies Array<{ name: string; options: Partial }> test.each(testData)('$name', async (props) => { diff --git a/packages/plugin-axios/src/plugin.ts b/packages/plugin-axios/src/plugin.ts index 48e255140..cec22e071 100644 --- a/packages/plugin-axios/src/plugin.ts +++ b/packages/plugin-axios/src/plugin.ts @@ -44,6 +44,7 @@ export const pluginAxios = definePlugin((options) => { override = [], baseURL, validator = false, + returnType = 'full', group, sdk, resolver: userResolver, @@ -57,6 +58,7 @@ export const pluginAxios = definePlugin((options) => { group: createGroupConfig(group), baseURL, validator, + returnType, sdk: sdk ? { mode: sdk.mode ?? 'tag', name: sdk.name } : undefined, resolver: userResolver ? Resolver.merge(resolverClient, userResolver) : resolverClient, } diff --git a/packages/plugin-axios/templates/axios.test.ts b/packages/plugin-axios/templates/axios.test.ts index 30bc1bb51..652a4c0f8 100644 --- a/packages/plugin-axios/templates/axios.test.ts +++ b/packages/plugin-axios/templates/axios.test.ts @@ -1,6 +1,6 @@ import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios' import { describe, expect, test, vi } from 'vitest' -import { type CallResult, createClientCore, parseEventStream, ResponseError, resolveAuth } from './axios.ts' +import { type CallResult, createClientCore, parseEventStream, ResponseError, resolveAuth, unwrapResult } from './axios.ts' import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, serializeCookies } from './serializers.ts' type Programmed = { data?: unknown; status?: number; statusText?: string } @@ -716,6 +716,19 @@ describe('getUrl', () => { }) }) +describe('unwrapResult', () => { + test('narrows a success result to its data', async () => { + const result = await unwrapResult(Promise.resolve({ data: { id: 1 }, error: undefined }), undefined) + expect(result).toStrictEqual({ id: 1 }) + }) + + test('falls back to the full result when throwOnError is false', async () => { + const full = { data: undefined, error: { message: 'not found' } } + const result = await unwrapResult(Promise.resolve(full), false) + expect(result).toBe(full) + }) +}) + describe('resolveAuth', () => { test('places a bearer token on the Authorization header', async () => { const headers: Record = {} diff --git a/packages/plugin-axios/templates/axios.ts b/packages/plugin-axios/templates/axios.ts index d928c18f3..e022ad571 100644 --- a/packages/plugin-axios/templates/axios.ts +++ b/packages/plugin-axios/templates/axios.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/packages/plugin-fetch/src/generators/__snapshots__/getPetByIdWithReturnTypeData/getPetById.ts b/packages/plugin-fetch/src/generators/__snapshots__/getPetByIdWithReturnTypeData/getPetById.ts new file mode 100644 index 000000000..5d1eaebd5 --- /dev/null +++ b/packages/plugin-fetch/src/generators/__snapshots__/getPetByIdWithReturnTypeData/getPetById.ts @@ -0,0 +1,18 @@ +/* eslint-disable no-alert, no-console */ + +import type { Options, UnwrappedResult } from './.kubb/client' +import type { GetPetByIdOptions, GetPetByIdResponses } from './GetPetById' +import { client, unwrapResult } from './.kubb/client' + +/** + * {@link /pet/:petId} + */ +export function getPetById( + options: Options, +): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/pet/{petId}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > +} diff --git a/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/petClient.ts b/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/petClient.ts new file mode 100644 index 000000000..1da7708b8 --- /dev/null +++ b/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/petClient.ts @@ -0,0 +1,40 @@ +/* eslint-disable no-alert, no-console */ + +import type { ClientConfig, ClientInstance, Options, UnwrappedResult } from './.kubb/client' +import type { DeletePetOptions, DeletePetResponses } from './DeletePet' +import type { GetPetByIdOptions, GetPetByIdResponses } from './GetPetById' +import { createClient, unwrapResult } from './.kubb/client' + +export class PetClient { + private readonly client: ClientInstance + + constructor(config: ClientConfig = {}) { + this.client = createClient(config) + } + + /** + * {@link /pet/:petId} + */ + public getPetById( + options: Options, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/pet/{petId}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } + + /** + * {@link /pet/:petId} + */ + public deletePet( + options: Options, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'DELETE', url: '/pet/{petId}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } +} diff --git a/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/projectClient.ts b/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/projectClient.ts new file mode 100644 index 000000000..110c5b56e --- /dev/null +++ b/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/projectClient.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-alert, no-console */ + +import type { ClientConfig, ClientInstance, Options, UnwrappedResult } from './.kubb/client' +import type { GetProjectOptions, GetProjectResponses } from './GetProject' +import { createClient, unwrapResult } from './.kubb/client' + +export class ProjectClient { + private readonly client: ClientInstance + + constructor(config: ClientConfig = {}) { + this.client = createClient(config) + } + + /** + * {@link /projects/:project_id} + */ + public getProject( + options: Options, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/projects/{project_id}', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } +} diff --git a/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/storeClient.ts b/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/storeClient.ts new file mode 100644 index 000000000..0a617f778 --- /dev/null +++ b/packages/plugin-fetch/src/generators/__snapshots__/sdkClassWithReturnTypeData/storeClient.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-alert, no-console */ + +import type { ClientConfig, ClientInstance, Options, UnwrappedResult } from './.kubb/client' +import type { GetInventoryOptions, GetInventoryResponses } from './GetInventory' +import { createClient, unwrapResult } from './.kubb/client' + +export class StoreClient { + private readonly client: ClientInstance + + constructor(config: ClientConfig = {}) { + this.client = createClient(config) + } + + /** + * {@link /store/inventory} + */ + public getInventory( + options: Options = {}, + ): Promise> { + const { client: request = this.client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/store/inventory', ...config }), config.throwOnError) as Promise< + UnwrappedResult + > + } +} diff --git a/packages/plugin-fetch/src/generators/clientGenerator.test.tsx b/packages/plugin-fetch/src/generators/clientGenerator.test.tsx index 44da2ef45..5d6394157 100644 --- a/packages/plugin-fetch/src/generators/clientGenerator.test.tsx +++ b/packages/plugin-fetch/src/generators/clientGenerator.test.tsx @@ -28,6 +28,7 @@ const defaultOptions: PluginFetch['resolvedOptions'] = { group: null, baseURL: undefined, validator: false, + returnType: 'full', sdk: undefined, resolver: resolverClient, } @@ -223,6 +224,8 @@ describe('clientGenerator operation', () => { // text/event-stream response returns a typed event stream instead of a one-shot result. { name: 'streamEventsSse', node: streamEventsNode, options: {} }, { name: 'addPetMultiStatusWithZod', node: createPetNode, options: { validator: 'zod' as const } }, + // returnType: 'data' unwraps the resolved call down to the bare success body. + { name: 'getPetByIdWithReturnTypeData', node: getPetByIdNode, options: { returnType: 'data' as const } }, // Operation-level security overriding the global default, oauth2 reduced to bearer. { name: 'addPetWithSecurity', node: createPetNode, options: {}, adapter: mockedAdapterWithDocument(securityDocument) }, // No operation-level security: falls back to the document's global `bearerAuth`. diff --git a/packages/plugin-fetch/src/generators/sdkGenerator.test.tsx b/packages/plugin-fetch/src/generators/sdkGenerator.test.tsx index 8465693f6..d61a1eb20 100644 --- a/packages/plugin-fetch/src/generators/sdkGenerator.test.tsx +++ b/packages/plugin-fetch/src/generators/sdkGenerator.test.tsx @@ -27,6 +27,7 @@ const defaultOptions: PluginFetch['resolvedOptions'] = { group: null, baseURL: undefined, validator: false, + returnType: 'full', sdk: { mode: 'tag', name: undefined }, resolver: resolverClient, } @@ -119,6 +120,8 @@ describe('sdkGenerator operations', () => { options: { sdk: { mode: 'flat', name: 'PetStore' } } as Partial, adapter: mockedAdapterWithDocument(securityDocument), }, + // returnType: 'data' unwraps every SDK method down to the bare success body. + { name: 'sdkClassWithReturnTypeData', options: { returnType: 'data' } as Partial }, ] as const satisfies Array<{ name: string; options: Partial; adapter?: Adapter }> test.each(testData)('$name', async (props) => { diff --git a/packages/plugin-fetch/src/plugin.ts b/packages/plugin-fetch/src/plugin.ts index a67ec1ca8..a2ea1807e 100644 --- a/packages/plugin-fetch/src/plugin.ts +++ b/packages/plugin-fetch/src/plugin.ts @@ -44,6 +44,7 @@ export const pluginFetch = definePlugin((options) => { override = [], baseURL, validator = false, + returnType = 'full', group, sdk, resolver: userResolver, @@ -57,6 +58,7 @@ export const pluginFetch = definePlugin((options) => { group: createGroupConfig(group), baseURL, validator, + returnType, sdk: sdk ? { mode: sdk.mode ?? 'tag', name: sdk.name } : undefined, resolver: userResolver ? Resolver.merge(resolverClient, userResolver) : resolverClient, } diff --git a/packages/plugin-fetch/templates/fetch.test.ts b/packages/plugin-fetch/templates/fetch.test.ts index e2dfc90b9..666a456f6 100644 --- a/packages/plugin-fetch/templates/fetch.test.ts +++ b/packages/plugin-fetch/templates/fetch.test.ts @@ -10,6 +10,7 @@ import { type ServerSentEvent, type Transport, type TransportResult, + unwrapResult, } from './fetch.ts' import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, serializeCookies } from './serializers.ts' @@ -649,6 +650,19 @@ describe('getUrl', () => { }) }) +describe('unwrapResult', () => { + test('narrows a success result to its data', async () => { + const result = await unwrapResult(Promise.resolve({ data: { id: 1 }, error: undefined }), undefined) + expect(result).toStrictEqual({ id: 1 }) + }) + + test('falls back to the full result when throwOnError is false', async () => { + const full = { data: undefined, error: { message: 'invalid' } } + const result = await unwrapResult(Promise.resolve(full), false) + expect(result).toBe(full) + }) +}) + describe('resolveAuth', () => { test('places a bearer token on the Authorization header', async () => { const headers: Record = {} diff --git a/packages/plugin-fetch/templates/fetch.ts b/packages/plugin-fetch/templates/fetch.ts index f80182aca..4829ea625 100644 --- a/packages/plugin-fetch/templates/fetch.ts +++ b/packages/plugin-fetch/templates/fetch.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/packages/plugin-react-query/src/components/MutationOptions.tsx b/packages/plugin-react-query/src/components/MutationOptions.tsx index d5e36d5b2..69c36b464 100644 --- a/packages/plugin-react-query/src/components/MutationOptions.tsx +++ b/packages/plugin-react-query/src/components/MutationOptions.tsx @@ -3,7 +3,7 @@ import type { FunctionParametersNode, ResolverTs } from '@kubb/plugin-ts' import { createFunctionParameter, createFunctionParameters, functionPrinter } from '@kubb/plugin-ts' import { File, Function } from 'kubb/jsx' import type { KubbReactNode } from 'kubb/jsx' -import { buildGroupedRequestParam, buildClientCall } from '@internals/tanstack-query' +import { buildCallResultBody, buildGroupedRequestParam, buildClientCall } from '@internals/tanstack-query' import { buildRequestConfigType, buildResponseTypes } from '../utils.ts' type Props = { @@ -12,6 +12,12 @@ type Props = { mutationKeyName: string node: ast.OperationNode tsResolver: ResolverTs + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } const declarationPrinter = functionPrinter({ mode: 'declaration' }) @@ -29,7 +35,7 @@ export function buildMutationConfigParamsNode(node: ast.OperationNode): Function }) } -export function MutationOptions({ name, clientName, node, tsResolver, mutationKeyName }: Props): KubbReactNode { +export function MutationOptions({ name, clientName, node, tsResolver, mutationKeyName, returnType = 'full' }: Props): KubbReactNode { const { TData, TError } = buildResponseTypes(node, tsResolver) const configParamsNode = buildMutationConfigParamsNode(node) @@ -41,8 +47,7 @@ export function MutationOptions({ name, clientName, node, tsResolver, mutationKe const groupedParamsNode = createFunctionParameters({ params: groupedParam ? [groupedParam] : [] }) const TRequest = hasMutationParams ? tsResolver.response.options(node) : 'undefined' const argBindingStr = hasMutationParams ? (callPrinter.print(groupedParamsNode) ?? '') : '_' - const mutationFnBody = `const { data } = await ${buildClientCall(node, { clientName, signal: false })} - return data` + const mutationFnBody = buildCallResultBody(buildClientCall(node, { clientName, signal: false }), { returnType }) return ( diff --git a/packages/plugin-react-query/src/components/QueryOptions.tsx b/packages/plugin-react-query/src/components/QueryOptions.tsx index 7fff89fd5..8b0fd158e 100644 --- a/packages/plugin-react-query/src/components/QueryOptions.tsx +++ b/packages/plugin-react-query/src/components/QueryOptions.tsx @@ -3,7 +3,7 @@ import type { ResolverTs } from '@kubb/plugin-ts' import { functionPrinter } from '@kubb/plugin-ts' import { File, Function } from 'kubb/jsx' import type { KubbReactNode } from 'kubb/jsx' -import { buildQueryOptionsParams, buildClientCall } from '@internals/tanstack-query' +import { buildCallResultBody, buildQueryOptionsParams, buildClientCall } from '@internals/tanstack-query' import { buildQueryKeyParams, buildResponseTypes } from '../utils.ts' type Props = { @@ -12,12 +12,18 @@ type Props = { queryKeyName: string node: ast.OperationNode tsResolver: ResolverTs + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } const declarationPrinter = functionPrinter({ mode: 'declaration' }) const callPrinter = functionPrinter({ mode: 'call' }) -export function QueryOptions({ name, clientName, node, tsResolver, queryKeyName }: Props): KubbReactNode { +export function QueryOptions({ name, clientName, node, tsResolver, queryKeyName, returnType = 'full' }: Props): KubbReactNode { const { TData, TError } = buildResponseTypes(node, tsResolver) const queryKeyParamsNode = buildQueryKeyParams(node, { resolver: tsResolver }) @@ -25,8 +31,7 @@ export function QueryOptions({ name, clientName, node, tsResolver, queryKeyName const paramsNode = buildQueryOptionsParams(node, { resolver: tsResolver }) const paramsSignature = declarationPrinter.print(paramsNode) ?? '' - const queryFnBody = `const { data } = await ${buildClientCall(node, { clientName, signal: true })} - return data` + const queryFnBody = buildCallResultBody(buildClientCall(node, { clientName, signal: true }), { returnType }) return ( diff --git a/packages/plugin-react-query/src/generators/infiniteQueryGenerator.tsx b/packages/plugin-react-query/src/generators/infiniteQueryGenerator.tsx index 600ee2eb1..8fc8e345e 100644 --- a/packages/plugin-react-query/src/generators/infiniteQueryGenerator.tsx +++ b/packages/plugin-react-query/src/generators/infiniteQueryGenerator.tsx @@ -116,6 +116,7 @@ export const infiniteQueryGenerator = defineGenerator({ previousParam={infiniteOptions.previousParam} initialPageParam={infiniteOptions.initialPageParam} queryParam={infiniteOptions.queryParam} + returnType={contractOp.returnType} /> diff --git a/packages/plugin-react-query/src/generators/mutationGenerator.tsx b/packages/plugin-react-query/src/generators/mutationGenerator.tsx index 2110a50c8..671377c52 100644 --- a/packages/plugin-react-query/src/generators/mutationGenerator.tsx +++ b/packages/plugin-react-query/src/generators/mutationGenerator.tsx @@ -80,7 +80,14 @@ export const mutationGenerator = defineGenerator({ - + {mutation && hooks && ( <> diff --git a/packages/plugin-react-query/src/generators/queryGenerator.tsx b/packages/plugin-react-query/src/generators/queryGenerator.tsx index f91352c0b..c5d45f13d 100644 --- a/packages/plugin-react-query/src/generators/queryGenerator.tsx +++ b/packages/plugin-react-query/src/generators/queryGenerator.tsx @@ -85,7 +85,14 @@ export const queryGenerator = defineGenerator({ - + {query && hooks && ( <> diff --git a/packages/plugin-react-query/src/generators/suspenseInfiniteQueryGenerator.tsx b/packages/plugin-react-query/src/generators/suspenseInfiniteQueryGenerator.tsx index a6c43c943..e8073ae58 100644 --- a/packages/plugin-react-query/src/generators/suspenseInfiniteQueryGenerator.tsx +++ b/packages/plugin-react-query/src/generators/suspenseInfiniteQueryGenerator.tsx @@ -110,6 +110,7 @@ export const suspenseInfiniteQueryGenerator = defineGenerator( previousParam={infiniteOptions.previousParam} initialPageParam={infiniteOptions.initialPageParam} queryParam={infiniteOptions.queryParam} + returnType={contractOp.returnType} /> diff --git a/packages/plugin-react-query/src/generators/suspenseQueryGenerator.tsx b/packages/plugin-react-query/src/generators/suspenseQueryGenerator.tsx index afd6d857f..04ae659a8 100644 --- a/packages/plugin-react-query/src/generators/suspenseQueryGenerator.tsx +++ b/packages/plugin-react-query/src/generators/suspenseQueryGenerator.tsx @@ -87,7 +87,14 @@ export const suspenseQueryGenerator = defineGenerator({ - + diff --git a/packages/plugin-swr/src/components/Mutation.tsx b/packages/plugin-swr/src/components/Mutation.tsx index 2506e06c3..7eda7005a 100644 --- a/packages/plugin-swr/src/components/Mutation.tsx +++ b/packages/plugin-swr/src/components/Mutation.tsx @@ -3,7 +3,7 @@ import type { FunctionParametersNode, ResolverTs } from '@kubb/plugin-ts' import { createFunctionParameter, createFunctionParameters, functionPrinter } from '@kubb/plugin-ts' import { File, Function, Type } from 'kubb/jsx' import type { KubbReactNode } from 'kubb/jsx' -import { buildGroupedRequestParam, buildClientCall } from '@internals/tanstack-query' +import { buildCallResultBody, buildGroupedRequestParam, buildClientCall } from '@internals/tanstack-query' import { buildRequestConfigType, getComments, resolveErrorNames } from '../utils.ts' type Props = { @@ -14,6 +14,12 @@ type Props = { mutationArgTypeName: string node: ast.OperationNode tsResolver: ResolverTs + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } const declarationPrinter = functionPrinter({ mode: 'declaration' }) @@ -49,7 +55,16 @@ function buildMutationParamsNode( }) } -export function Mutation({ name, clientName, mutationKeyName, mutationKeyTypeName, mutationArgTypeName, node, tsResolver }: Props): KubbReactNode { +export function Mutation({ + name, + clientName, + mutationKeyName, + mutationKeyTypeName, + mutationArgTypeName, + node, + tsResolver, + returnType = 'full', +}: Props): KubbReactNode { const responseName = tsResolver.response.response(node) const errorNames = resolveErrorNames(node, tsResolver) @@ -61,8 +76,7 @@ export function Mutation({ name, clientName, mutationKeyName, mutationKeyTypeNam const groupedParamsNode = createFunctionParameters({ params: groupedParam ? [groupedParam] : [] }) const argTypeBody = hasMutationParams ? tsResolver.response.options(node) : '' const argBindingStr = hasMutationParams ? (callPrinter.print(groupedParamsNode) ?? '') : '' - const mutationFnBody = `const { data } = await ${buildClientCall(node, { clientName, signal: false })} - return data` + const mutationFnBody = buildCallResultBody(buildClientCall(node, { clientName, signal: false }), { returnType, indent: ' ' }) const generics = [TData, TError, `${mutationKeyTypeName} | null`, mutationArgTypeName] diff --git a/packages/plugin-swr/src/components/QueryOptions.tsx b/packages/plugin-swr/src/components/QueryOptions.tsx index 62445d97a..b2017d1e1 100644 --- a/packages/plugin-swr/src/components/QueryOptions.tsx +++ b/packages/plugin-swr/src/components/QueryOptions.tsx @@ -3,22 +3,27 @@ import type { ResolverTs } from '@kubb/plugin-ts' import { functionPrinter } from '@kubb/plugin-ts' import { File, Function } from 'kubb/jsx' import type { KubbReactNode } from 'kubb/jsx' -import { buildQueryOptionsParams, buildClientCall } from '@internals/tanstack-query' +import { buildCallResultBody, buildQueryOptionsParams, buildClientCall } from '@internals/tanstack-query' type Props = { name: string clientName: string node: ast.OperationNode tsResolver: ResolverTs + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } const declarationPrinter = functionPrinter({ mode: 'declaration' }) -export function QueryOptions({ name, clientName, node, tsResolver }: Props): KubbReactNode { +export function QueryOptions({ name, clientName, node, tsResolver, returnType = 'full' }: Props): KubbReactNode { const paramsNode = buildQueryOptionsParams(node, { resolver: tsResolver }) const paramsSignature = declarationPrinter.print(paramsNode) ?? '' - const fetcherBody = `const { data } = await ${buildClientCall(node, { clientName, signal: false })} - return data` + const fetcherBody = buildCallResultBody(buildClientCall(node, { clientName, signal: false }), { returnType }) return ( diff --git a/packages/plugin-swr/src/generators/mutationGenerator.tsx b/packages/plugin-swr/src/generators/mutationGenerator.tsx index 74127854e..78396a6d1 100644 --- a/packages/plugin-swr/src/generators/mutationGenerator.tsx +++ b/packages/plugin-swr/src/generators/mutationGenerator.tsx @@ -87,6 +87,7 @@ export const mutationGenerator = defineGenerator({ mutationArgTypeName={mutationArgTypeName} node={node} tsResolver={tsResolver} + returnType={contractOp.returnType} /> )} diff --git a/packages/plugin-swr/src/generators/queryGenerator.tsx b/packages/plugin-swr/src/generators/queryGenerator.tsx index 1a025cd52..e2f30ea44 100644 --- a/packages/plugin-swr/src/generators/queryGenerator.tsx +++ b/packages/plugin-swr/src/generators/queryGenerator.tsx @@ -78,7 +78,7 @@ export const queryGenerator = defineGenerator({ - + {query && ( <> diff --git a/packages/plugin-vue-query/src/components/InfiniteQueryOptions.tsx b/packages/plugin-vue-query/src/components/InfiniteQueryOptions.tsx index a5c9a781f..eb2a30181 100644 --- a/packages/plugin-vue-query/src/components/InfiniteQueryOptions.tsx +++ b/packages/plugin-vue-query/src/components/InfiniteQueryOptions.tsx @@ -16,6 +16,12 @@ type Props = { nextParam: Infinite['nextParam'] previousParam: Infinite['previousParam'] queryParam: Infinite['queryParam'] + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } /** diff --git a/packages/plugin-vue-query/src/components/Mutation.tsx b/packages/plugin-vue-query/src/components/Mutation.tsx index 0733142a6..4770f8737 100644 --- a/packages/plugin-vue-query/src/components/Mutation.tsx +++ b/packages/plugin-vue-query/src/components/Mutation.tsx @@ -3,7 +3,7 @@ import type { FunctionParametersNode, ResolverTs } from '@kubb/plugin-ts' import { createFunctionParameter, createFunctionParameters, functionPrinter } from '@kubb/plugin-ts' import { File, Function } from 'kubb/jsx' import type { KubbReactNode } from 'kubb/jsx' -import { buildGroupedRequestParam, buildResponseTypes } from '@internals/tanstack-query' +import { buildCallResultBody, buildGroupedRequestParam, buildResponseTypes } from '@internals/tanstack-query' import { buildRequestConfigType, buildVueClientCall, getComments } from '../utils.ts' type Props = { @@ -13,6 +13,12 @@ type Props = { mutationKeyName: string node: ast.OperationNode tsResolver: ResolverTs + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } const declarationPrinter = functionPrinter({ mode: 'declaration' }) @@ -48,15 +54,14 @@ function buildMutationParamsNode( }) } -export function Mutation({ name, clientName, node, tsResolver, mutationKeyName }: Props): KubbReactNode { +export function Mutation({ name, clientName, node, tsResolver, mutationKeyName, returnType = 'full' }: Props): KubbReactNode { const { TData, TError } = buildResponseTypes(node, tsResolver) const groupedParam = buildGroupedRequestParam(node, { resolver: tsResolver }) const hasMutationParams = groupedParam !== null const groupedParamsNode = createFunctionParameters({ params: groupedParam ? [groupedParam] : [] }) const argBindingStr = hasMutationParams ? (callPrinter.print(groupedParamsNode) ?? '') : '' - const mutationFnBody = `const { data } = await ${buildVueClientCall(node, { clientName, signal: false })} - return data` + const mutationFnBody = buildCallResultBody(buildVueClientCall(node, { clientName, signal: false }), { returnType, indent: ' ' }) const TRequest = resolveMutationRequestType(node, tsResolver) const generics = [TData, TError, TRequest, 'TContext'].join(', ') diff --git a/packages/plugin-vue-query/src/components/QueryOptions.tsx b/packages/plugin-vue-query/src/components/QueryOptions.tsx index c565e5b0b..a749d8e36 100644 --- a/packages/plugin-vue-query/src/components/QueryOptions.tsx +++ b/packages/plugin-vue-query/src/components/QueryOptions.tsx @@ -3,7 +3,7 @@ import type { FunctionParametersNode, ResolverTs } from '@kubb/plugin-ts' import { functionPrinter } from '@kubb/plugin-ts' import { File, Function } from 'kubb/jsx' import type { KubbReactNode } from 'kubb/jsx' -import { buildQueryOptionsParams, buildResponseTypes } from '@internals/tanstack-query' +import { buildCallResultBody, buildQueryOptionsParams, buildResponseTypes } from '@internals/tanstack-query' import { buildVueClientCall, maybeRefOrGetter } from '../utils.ts' import { buildQueryKeyParamsNode } from './QueryKey.tsx' @@ -13,6 +13,12 @@ type Props = { queryKeyName: string node: ast.OperationNode tsResolver: ResolverTs + /** + * The registered client plugin's `returnType`, read by the caller off `resolveClientOperation`. + * + * @default 'full' + */ + returnType?: 'full' | 'data' } const declarationPrinter = functionPrinter({ mode: 'declaration' }) @@ -22,7 +28,7 @@ export function getQueryOptionsParams(node: ast.OperationNode, options: { resolv return buildQueryOptionsParams(node, { resolver: options.resolver, memberTypeWrapper: maybeRefOrGetter }) } -export function QueryOptions({ name, clientName, node, tsResolver, queryKeyName }: Props): KubbReactNode { +export function QueryOptions({ name, clientName, node, tsResolver, queryKeyName, returnType = 'full' }: Props): KubbReactNode { const { TData, TError } = buildResponseTypes(node, tsResolver) const queryKeyParamsNode = buildQueryKeyParamsNode(node, { resolver: tsResolver }) @@ -30,8 +36,7 @@ export function QueryOptions({ name, clientName, node, tsResolver, queryKeyName const paramsNode = getQueryOptionsParams(node, { resolver: tsResolver }) const paramsSignature = declarationPrinter.print(paramsNode) ?? '' - const queryFnBody = `const { data } = await ${buildVueClientCall(node, { clientName, signal: true })} - return data` + const queryFnBody = buildCallResultBody(buildVueClientCall(node, { clientName, signal: true }), { returnType }) return ( diff --git a/packages/plugin-vue-query/src/generators/infiniteQueryGenerator.tsx b/packages/plugin-vue-query/src/generators/infiniteQueryGenerator.tsx index f7de82358..153121b2b 100644 --- a/packages/plugin-vue-query/src/generators/infiniteQueryGenerator.tsx +++ b/packages/plugin-vue-query/src/generators/infiniteQueryGenerator.tsx @@ -112,6 +112,7 @@ export const infiniteQueryGenerator = defineGenerator({ previousParam={infiniteOptions.previousParam} initialPageParam={infiniteOptions.initialPageParam} queryParam={infiniteOptions.queryParam} + returnType={contractOp.returnType} /> diff --git a/packages/plugin-vue-query/src/generators/mutationGenerator.tsx b/packages/plugin-vue-query/src/generators/mutationGenerator.tsx index b3d09f43b..d9fa3766f 100644 --- a/packages/plugin-vue-query/src/generators/mutationGenerator.tsx +++ b/packages/plugin-vue-query/src/generators/mutationGenerator.tsx @@ -92,6 +92,7 @@ export const mutationGenerator = defineGenerator({ node={node} tsResolver={tsResolver} mutationKeyName={mutationKeyName} + returnType={contractOp.returnType} /> )} diff --git a/packages/plugin-vue-query/src/generators/queryGenerator.tsx b/packages/plugin-vue-query/src/generators/queryGenerator.tsx index 65bc807fa..64049e0d6 100644 --- a/packages/plugin-vue-query/src/generators/queryGenerator.tsx +++ b/packages/plugin-vue-query/src/generators/queryGenerator.tsx @@ -84,7 +84,14 @@ export const queryGenerator = defineGenerator({ - + {query && hooks && ( <> diff --git a/tests/3.0.x/__snapshots__/pluginAxios/default/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginAxios/default/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginAxios/default/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginAxios/default/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginAxios/paramsCasing/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginAxios/paramsCasing/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginAxios/paramsCasing/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginAxios/paramsCasing/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginAxios/sdkClass/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginAxios/sdkClass/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginAxios/sdkClass/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginAxios/sdkClass/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginAxios/sdkClassWithName/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginAxios/sdkClassWithName/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginAxios/sdkClassWithName/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginAxios/sdkClassWithName/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginAxios/sdkSingle/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginAxios/sdkSingle/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginAxios/sdkSingle/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginAxios/sdkSingle/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginAxios/zodTypes/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginAxios/zodTypes/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginAxios/zodTypes/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginAxios/zodTypes/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginFetch/default/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginFetch/default/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/tests/3.0.x/__snapshots__/pluginFetch/default/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginFetch/default/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginFetch/paramsCasing/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginFetch/paramsCasing/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/tests/3.0.x/__snapshots__/pluginFetch/paramsCasing/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginFetch/paramsCasing/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginFetch/sdkClass/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginFetch/sdkClass/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/tests/3.0.x/__snapshots__/pluginFetch/sdkClass/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginFetch/sdkClass/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginFetch/sdkClassWithName/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginFetch/sdkClassWithName/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/tests/3.0.x/__snapshots__/pluginFetch/sdkClassWithName/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginFetch/sdkClassWithName/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginFetch/sdkSingle/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginFetch/sdkSingle/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/tests/3.0.x/__snapshots__/pluginFetch/sdkSingle/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginFetch/sdkSingle/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginFetch/zodTypes/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginFetch/zodTypes/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/tests/3.0.x/__snapshots__/pluginFetch/zodTypes/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginFetch/zodTypes/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginFetch/zodTypesSdk/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginFetch/zodTypesSdk/.kubb/client.ts index f80182aca..4829ea625 100644 --- a/tests/3.0.x/__snapshots__/pluginFetch/zodTypesSdk/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginFetch/zodTypesSdk/.kubb/client.ts @@ -82,6 +82,27 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult = ThrowOnError extends true + ? RequestResult['data'] + : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginMcp/excludeByOperationId/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginMcp/excludeByOperationId/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginMcp/excludeByOperationId/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginMcp/excludeByOperationId/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginMcp/groupByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginMcp/groupByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginMcp/groupByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginMcp/groupByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginMcp/includeByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginMcp/includeByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginMcp/includeByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginMcp/includeByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginMcp/paramsCasing/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginMcp/paramsCasing/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginMcp/paramsCasing/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginMcp/paramsCasing/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginMcp/petStore/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginMcp/petStore/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginMcp/petStore/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginMcp/petStore/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginMcp/withClientPlugin/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginMcp/withClientPlugin/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginMcp/withClientPlugin/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginMcp/withClientPlugin/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/excludeByOperationId/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/excludeByOperationId/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/excludeByOperationId/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/excludeByOperationId/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/groupByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/groupByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/groupByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/groupByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/includeByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/includeByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/includeByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/includeByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/infinite/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/infinite/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/infinite/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/infinite/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/mutationDisabled/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/mutationDisabled/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/mutationDisabled/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/mutationDisabled/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/paramsCasing/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/paramsCasing/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/paramsCasing/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/paramsCasing/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/parserZod/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/parserZod/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/parserZod/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/parserZod/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/petStore/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/petStore/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/petStore/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/petStore/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/queryMethods/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/queryMethods/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/queryMethods/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/queryMethods/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/client.ts new file mode 100644 index 000000000..e022ad571 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/client.ts @@ -0,0 +1,749 @@ +import axios from 'axios' +import type { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' +import { applyHeaderStyles, defaultBodySerializer, defaultPathSerializer, defaultQuerySerializer, isDefaultJsonBody, serializeCookies } from './serializers' +import type { HeadersInit, PathParamStyle, PathSerializer, Serializers, Styles } from './serializers' +import { type StandardSchemaValidator, validateStandardSchema } from './standardSchema' + +/** + * HTTP status codes treated as a success, everything else is an error. + */ +export type SuccessStatusCode = '200' | '201' | '202' | '203' | '204' | '205' | '206' | '207' | '208' | '226' + +/** + * The success members of a per-status responses record. + */ +export type SuccessOf = TResponses[Extract] + +/** + * The error members of a per-status responses record, every documented status that is not a 2xx. + */ +export type ErrorOf = TResponses[Exclude] + +/** + * Converts a response record's string status key to its numeric literal, leaving non-numeric keys like `default` as `number`. + */ +export type ToStatusNumber = TStatus extends `${infer TNumber extends number}` ? TNumber : number + +/** + * The plain body of a per-status response, unwrapping the `{ contentType; data }` union so an error result keeps the bare body union on `error`. + */ +export type DataOf = T extends { contentType: string; data: infer TData } ? TData : T + +/** + * The success variant for a single status, flattened so the negotiated `contentType` sits next to `data` and `switch (result.contentType)` narrows it. + */ +export type SuccessVariant = TEntry extends { contentType: string; data: unknown } + ? TEntry extends { contentType: infer TContentType; data: infer TData } + ? { status: ToStatusNumber; data: TData; error: undefined; contentType: TContentType; request: TRequest; response: TResponse } + : never + : { status: ToStatusNumber; data: TEntry; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse } + +/** + * One result variant for a single documented status, keyed by the numeric `status` so a `switch (result.status)` narrows `data` or `error`. + */ +export type ResultByStatus = TStatus extends SuccessStatusCode + ? SuccessVariant + : { + status: ToStatusNumber + data: undefined + error: DataOf + contentType: string | undefined + request: TRequest + response: TResponse + } + +/** + * The union of every documented status' result variant. + */ +export type ResultUnion = { + [TStatus in keyof TResponses]: ResultByStatus +}[keyof TResponses] + +/** + * The union of just the success (2xx) status variants, selected by status code so an untyped error payload can never widen `data`. + */ +export type SuccessResultUnion = { + [TStatus in Extract]: ResultByStatus +}[Extract] + +/** + * The shape every generated function returns, discriminated by the top-level `status`, narrowing to the 2xx variants under `throwOnError` and to every documented status without it. + */ +export type RequestResult = ThrowOnError extends true + ? [SuccessResultUnion] extends [never] + ? { + status: number + data: SuccessOf + error: undefined + contentType: string | undefined + request: TRequest + response: TResponse + } + : SuccessResultUnion + : [ResultUnion] extends [never] + ? { status: number; data: undefined; error: undefined; contentType: string | undefined; request: TRequest; response: TResponse } + : ResultUnion + +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + +/** + * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. + */ +export type DataShape = { body?: unknown; cookies?: unknown; headers?: unknown; path?: unknown; query?: unknown } + +export type ResponseType = 'arraybuffer' | 'blob' | 'document' | 'json' | 'text' | 'stream' | 'formdata' + +/** + * Turns a raw response body into a parsed value, registered per media type as a codec's `deserialize` to handle formats the runtime does not decode itself. + */ +export type Deserializer = (raw: unknown, contentType: string) => T | Promise + +/** + * Serializes a request body for a single media type, registered per content type as a codec's `serialize` to encode formats the default serializer does not handle. + */ +export type ContentBodySerializer = (body: unknown, contentType?: string) => unknown + +/** + * A per-content-type codec registered on `codecs`, keyed by content type. `serialize` encodes the + * request body for that media type and `deserialize` decodes the response body. Either half is + * optional, so a codec can handle one direction. + */ +export type Codec = { + serialize?: ContentBodySerializer + deserialize?: Deserializer +} + +/** + * The per-call content type selection, where a bare string sets the request content type and the object form also sets the response format sent as `Accept`. + */ +export type ContentType = string | { request?: string; response?: string } + +/** + * A Standard Schema validator (zod, valibot, arktype) that parses a value before it is sent or after + * it is received. `runValidator` runs it through `validateStandardSchema`. Wired through the per-call + * `validator.request` / `validator.response` / `validator.error` hooks (`error` runs on the error body when a + * non-2xx call does not throw). + */ +export type Validator = StandardSchemaValidator + +/** + * A resolved security scheme carried on each generated call's `security` array and passed to the `auth` resolver. + */ +export type Auth = { + type: 'http' | 'apiKey' | 'oauth2' | 'openIdConnect' + scheme?: 'bearer' | 'basic' + name?: string + in?: 'header' | 'query' | 'cookie' +} + +/** + * The raw token a consumer returns for a scheme (or `user:password` for basic), or `undefined` to skip it. + */ +export type AuthToken = string | undefined + +/** + * Resolves the token for a security scheme, either a static token or a callback called per scheme until one returns a token. + */ +export type AuthResolver = AuthToken | ((auth: Auth) => AuthToken | Promise) + +/** + * Extra axios config the runtime spreads onto every request, an escape hatch for per-call fields it does not set itself such as `timeout`, `proxy`, and the progress callbacks. + */ +export type AxiosOptions = AxiosRequestConfig + +/** + * The request a generated function hands to the runtime, with `body` / `headers` / `path` / `query` from the grouped options. + */ +export type RequestConfig = { + baseURL?: string + url?: string + method?: 'GET' | 'PUT' | 'PATCH' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' + path?: Record + query?: unknown + params?: unknown + cookies?: Record + body?: TBody + headers?: HeadersInit + styles?: Styles + signal?: AbortSignal + options?: AxiosOptions + contentType?: ContentType + responseType?: ResponseType + throwOnError?: boolean + validateStatus?: (status: number) => boolean + client?: ClientInstance + transport?: AxiosInstance + serializer?: Serializers + codecs?: Record + validator?: { request?: Validator; response?: Validator; error?: Validator } + security?: Array + auth?: AuthResolver +} + +/** + * The grouped options object passed to every generated function: the request config minus the + * data-shaped keys and the literal `url`, plus the per-operation `Request`. + */ +export type Options = Omit< + RequestConfig, + keyof DataShape | 'url' +> & + TData & { + client?: ClientInstance + throwOnError?: ThrowOnError + } + +/** + * Client-level configuration shared by every call an instance makes, overridden by the per-call `RequestConfig`. + */ +export type ClientConfig = { + baseURL?: string + headers?: HeadersInit + options?: AxiosOptions + throwOnError?: boolean + validateStatus?: (status: number) => boolean + transport?: AxiosInstance + serializer?: Serializers + codecs?: Record + auth?: AuthResolver +} + +/** + * The result a resolved call produces before it is cast to `RequestResult` by the generated wrapper. + */ +export type CallResult = { + status: number + data: unknown + error: unknown + contentType: string | undefined + request: TRequest + response: TResponse +} + +export type InterceptorFn = (value: T) => T | Promise + +/** + * A single interceptor channel with a transport-agnostic `use` / `eject` / `update` API, backed by axios's native interceptor managers. + */ +export type InterceptorChannel = { + use: (fn: InterceptorFn) => number + eject: (id: number) => void + update: (id: number, fn: InterceptorFn) => void +} + +/** + * The three interceptor channels every client instance exposes, wrapping axios's native managers with `error` mapped onto the response rejection handler. + */ +export type Interceptors = { + request: InterceptorChannel + response: InterceptorChannel + error: InterceptorChannel +} + +/** + * A client instance: the callable send plus configuration, interceptors, and an isolated + * `createClient` factory. + */ +export type ClientInstance = { + (config: RequestConfig): Promise> + getConfig: () => ClientConfig + setConfig: (config: ClientConfig) => ClientConfig + getUrl: (config: RequestConfig) => string + interceptors: Interceptors + createClient: (config?: ClientConfig) => ClientInstance +} + +/** + * Thrown for a non-2xx response, so a resolved call always means success. + */ +export class ResponseError extends Error { + data: TError + status: number + statusText: string + contentType: string | undefined + request: TRequest + response: TResponse + + constructor(config: { data: TError; status: number; statusText: string; contentType?: string; request: TRequest; response: TResponse }) { + super(`Request failed with status ${config.status}${config.statusText ? ` ${config.statusText}` : ''}`) + this.name = 'ResponseError' + this.data = config.data + this.status = config.status + this.statusText = config.statusText + this.contentType = config.contentType + this.request = config.request + this.response = config.response + } +} + +export type ResponseErrorConfig = ResponseError + +function serializeHeaders(headers: HeadersInit | undefined): Record { + if (!headers) return {} + const entries = Array.isArray(headers) ? headers : Object.entries(headers) + const result: Record = {} + for (const [key, value] of entries) { + if (value === undefined || value === null) continue + result[key] = typeof value === 'string' ? value : typeof value === 'object' ? JSON.stringify(value) : String(value) + } + return result +} + +function mergeHeaders(...sources: Array): Record { + return Object.assign({}, ...sources.map(serializeHeaders)) +} + +function getHeader(headers: Record, name: string): string | undefined { + const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase()) + return key ? headers[key] : undefined +} + +function hasHeader(headers: Record, name: string): boolean { + return Object.keys(headers).some((k) => k.toLowerCase() === name.toLowerCase()) +} + +/** + * Joins the URL parts, interpolates URL-encoded `{param}` segments, and appends the serialized query, backing `getUrl`. + */ +function serializeUrl({ + parts, + pathParams, + search, + pathSerializer = defaultPathSerializer, + pathStyles, +}: { + parts: Array + pathParams: Record + search: string + pathSerializer?: PathSerializer + pathStyles?: Record +}): string { + const path = parts + .filter(Boolean) + .join('') + .replace(/\{([^{}]+)\}/g, (_, key: string) => pathSerializer({ name: key, value: pathParams[key], options: pathStyles?.[key] })) + return path + (search ? `?${search}` : '') +} + +/** + * Wraps an axios interceptor registration behind the shared `use` / `eject` / `update` API, mapping a stable external id onto axios's own so `update` can swap a handler in place. + */ +function createInterceptorChannel(register: (fn: InterceptorFn) => number, ejectNative: (id: number) => void): InterceptorChannel { + const ids = new Map() + let counter = 0 + return { + use(fn) { + const id = ++counter + ids.set(id, register(fn)) + return id + }, + eject(id) { + const nativeId = ids.get(id) + if (nativeId === undefined) return + ejectNative(nativeId) + ids.delete(id) + }, + update(id, fn) { + const nativeId = ids.get(id) + if (nativeId !== undefined) ejectNative(nativeId) + ids.set(id, register(fn)) + }, + } +} + +/** + * Walks the per-operation security in order and places the first resolved token on the request, mutating `headers` / `query` in place. + */ +export async function resolveAuth(params: { + security: Array | undefined + auth: AuthResolver | undefined + headers: Record + query: Record +}): Promise { + const { security, auth, headers, query } = params + if (!security?.length || auth === undefined) return + + for (const scheme of security) { + const token = typeof auth === 'function' ? await auth(scheme) : auth + if (token === undefined) continue + + if (scheme.type === 'apiKey') { + const name = scheme.name ?? 'Authorization' + if (scheme.in === 'query') { + if (query[name] === undefined) query[name] = token + } else if (scheme.in === 'cookie') { + headers['Cookie'] = [headers['Cookie'], `${name}=${token}`].filter(Boolean).join('; ') + } else if (!hasHeader(headers, name)) { + headers[name] = token + } + } else if (!hasHeader(headers, 'Authorization')) { + headers['Authorization'] = scheme.scheme === 'basic' ? `Basic ${btoa(token)}` : `Bearer ${token}` + } + return + } +} + +async function runValidator(validator: Validator | undefined, value: T): Promise { + if (!validator) return value + return validateStandardSchema(validator, value) +} + +/** + * The base media type of a `Content-Type` value, lowercased and stripped of any `; charset=...` parameters. + */ +function baseContentType(value: string | null | undefined): string | undefined { + if (!value) return undefined + return value.split(';')[0]!.trim().toLowerCase() || undefined +} + +/** + * Reads the negotiated response content type from the response headers as a base media type. + */ +function getResponseContentType(headers: Record | undefined): string | undefined { + if (!headers) return undefined + const value = headers['content-type'] ?? headers['Content-Type'] + return baseContentType(typeof value === 'string' ? value : undefined) +} + +/** + * Normalizes the `contentType` option to its `{ request, response }` form, treating a bare string as the request content type. + */ +function resolveContentType(contentType: ContentType | undefined): { request?: string; response?: string } { + if (typeof contentType === 'string') return { request: contentType } + return contentType ?? {} +} + +/** + * The per-concern serializers for a call, the per-call serializer winning over the client's and + * falling back to the defaults. + */ +function resolveSerializers({ config, requestConfig }: { config: { serializer?: Serializers }; requestConfig: { serializer?: Serializers } }) { + return { + querySerializer: requestConfig.serializer?.query ?? config.serializer?.query ?? defaultQuerySerializer, + bodySerializer: requestConfig.serializer?.body ?? config.serializer?.body ?? defaultBodySerializer, + pathSerializer: requestConfig.serializer?.path ?? config.serializer?.path ?? defaultPathSerializer, + } +} + +/** + * Resolves everything a call needs before it touches axios: merged headers with the negotiated + * content type, auth on headers or query, serialized cookies, the validated and serialized body, + * and the final axios request config with `throwOnError` riding `validateStatus`. + */ +async function resolveRequest({ + config, + requestConfig, +}: { + config: ClientConfig + requestConfig: RequestConfig +}): Promise<{ axiosConfig: AxiosRequestConfig; codecs: Record; throwOnError: boolean }> { + const { querySerializer, bodySerializer, pathSerializer } = resolveSerializers({ config, requestConfig }) + const codecs = { ...config.codecs, ...requestConfig.codecs } + + const headers = mergeHeaders(config.headers, applyHeaderStyles(requestConfig.headers, requestConfig.styles?.header)) + const { request: requestContentTypeOption, response: responseContentType } = resolveContentType(requestConfig.contentType) + const requestContentType = requestContentTypeOption ?? getHeader(headers, 'content-type') + if (responseContentType && !hasHeader(headers, 'accept')) { + headers['Accept'] = responseContentType + } + + const query: Record = { ...((requestConfig.query ?? requestConfig.params) as Record | undefined) } + + await resolveAuth({ + security: requestConfig.security, + auth: requestConfig.auth ?? config.auth, + headers, + query, + }) + + if (requestConfig.cookies) { + const cookie = serializeCookies(requestConfig.cookies, requestConfig.styles?.cookie) + if (cookie) headers['Cookie'] = [headers['Cookie'], cookie].filter(Boolean).join('; ') + } + + const validatedBody = await runValidator(requestConfig.validator?.request, requestConfig.body) + const requestContentTypeBase = baseContentType(requestContentType) + const contentCodec = requestContentTypeBase ? codecs[requestContentTypeBase] : undefined + const usesDefaultBodySerializer = !contentCodec?.serialize && bodySerializer === defaultBodySerializer + const body = contentCodec?.serialize + ? contentCodec.serialize(validatedBody, requestContentType) + : bodySerializer({ body: validatedBody, contentType: requestContentType, encoding: requestConfig.styles?.body }) + // A FormData body must keep its Content-Type unset so axios appends the multipart boundary. + if (body instanceof FormData) { + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === 'content-type') delete headers[key] + } + } else if (requestContentTypeOption) { + headers['Content-Type'] = requestContentTypeOption + } else if (usesDefaultBodySerializer && isDefaultJsonBody(validatedBody) && !hasHeader(headers, 'content-type')) { + headers['Content-Type'] = 'application/json' + } + + const pathParams = requestConfig.path ?? {} + const url = (requestConfig.url ?? '').replace(/\{([^{}]+)\}/g, (_, key: string) => + pathSerializer({ name: key, value: pathParams[key], options: requestConfig.styles?.path?.[key] }), + ) + + const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true + const validateStatus = + requestConfig.validateStatus ?? config.validateStatus ?? (throwOnError ? (status: number) => status >= 200 && status < 300 : () => true) + + const options = config.options || requestConfig.options ? { ...config.options, ...requestConfig.options } : undefined + + const axiosConfig: AxiosRequestConfig = { + ...options, // timeout, proxy, maxRedirects, decompress, onUploadProgress, … + url, + baseURL: requestConfig.baseURL ?? config.baseURL, + method: requestConfig.method ?? 'GET', + headers, + params: query, + paramsSerializer: (params) => querySerializer(params as Record, requestConfig.styles?.query), + data: body, + transformRequest: (data) => data, + signal: requestConfig.signal, + responseType: requestConfig.responseType, + validateStatus, + } + + // Only the fetch adapter exposes a streaming `response.data` (a ReadableStream) in the browser. + // The default XHR adapter buffers the whole body. Default streams to it, but respect an explicit adapter. + if (requestConfig.responseType === 'stream' && !axiosConfig.adapter) { + axiosConfig.adapter = 'fetch' + } + + return { axiosConfig, codecs, throwOnError } +} + +/** + * Turns an axios response into the call result: decodes the body through the matching codec and + * validates the success or error body. A thrown axios error never reaches this, so `error` here is + * always the body of a non-2xx response that `validateStatus` let through. + */ +async function settleResponse({ + response, + codecs, + validator, +}: { + response: AxiosResponse + codecs: Record + validator: { response?: Validator; error?: Validator } | undefined +}): Promise> { + const isSuccess = response.status >= 200 && response.status < 300 + const contentType = getResponseContentType(response.headers as Record) + let decoded: unknown = response.data + if (contentType) { + const codec = codecs[contentType] + if (codec?.deserialize) decoded = await codec.deserialize(response.data, contentType) + } + const data = isSuccess ? await runValidator(validator?.response, decoded) : undefined + const error = isSuccess ? undefined : await runValidator(validator?.error, decoded) + return { + status: response.status, + data, + error, + contentType, + request: response.config as TRequest, + response: response as TResponse, + } +} + +/** + * Builds the shared client core bound to an axios instance (defaulting to `axios.create()`), with `throwOnError` riding axios's `validateStatus`. + */ +export function createClientCore(options: ClientConfig = {}): ClientInstance { + let config: ClientConfig = { ...options } + const instance = config.transport ?? axios.create() + + const requestManager = instance.interceptors.request + const responseManager = instance.interceptors.response + const interceptors: Interceptors = { + request: createInterceptorChannel( + (fn) => requestManager.use(fn), + (id) => requestManager.eject(id), + ), + response: createInterceptorChannel( + (fn) => responseManager.use(fn), + (id) => responseManager.eject(id), + ), + error: createInterceptorChannel( + (fn) => + responseManager.use(undefined, async (error: unknown) => { + await fn(error as AxiosError) + return Promise.reject(error) + }), + (id) => responseManager.eject(id), + ), + } + + const client = (async (requestConfig: RequestConfig): Promise> => { + const activeInstance = requestConfig.transport ?? config.transport ?? instance + const { axiosConfig, codecs, throwOnError } = await resolveRequest({ config, requestConfig }) + + try { + const response = await activeInstance.request(axiosConfig) + return await settleResponse({ response, codecs, validator: requestConfig.validator }) + } catch (error) { + const axiosError = error as AxiosError + if (throwOnError && axiosError.response) { + throw new ResponseError({ + data: axiosError.response.data, + status: axiosError.response.status, + statusText: axiosError.response.statusText, + contentType: getResponseContentType(axiosError.response.headers as Record), + request: axiosError.config as TRequest, + response: axiosError.response as TResponse, + }) + } + throw error + } + }) as ClientInstance + + client.getConfig = () => config + client.setConfig = (next) => { + config = { ...config, ...next, headers: { ...serializeHeaders(config.headers), ...serializeHeaders(next.headers) } } + return config + } + client.getUrl = (requestConfig) => { + const { querySerializer, pathSerializer } = resolveSerializers({ config, requestConfig }) + const query: Record = { ...((requestConfig.query ?? requestConfig.params) as Record | undefined) } + return serializeUrl({ + parts: [requestConfig.baseURL ?? config.baseURL, requestConfig.url], + pathParams: requestConfig.path ?? {}, + search: querySerializer(query, requestConfig.styles?.query), + pathSerializer, + pathStyles: requestConfig.styles?.path, + }) + } + client.interceptors = interceptors + client.createClient = (next) => createClientCore({ ...config, ...next }) + + return client +} + +/** + * One decoded Server-Sent Event, with `data` parsed as JSON when valid and kept as the raw string otherwise. + */ +export type ServerSentEvent = { + data: TData + event?: string + id?: string + retry?: number +} + +async function* readBytes(stream: ReadableStream | AsyncIterable): AsyncGenerator { + if (!('getReader' in stream)) { + yield* stream + return + } + + const reader = stream.getReader() + try { + while (true) { + const { done, value } = await reader.read() + if (done) return + yield value + } + } finally { + await reader.cancel().catch(() => {}) + } +} + +function parseEvent(raw: string): ServerSentEvent | undefined { + const data: Array = [] + const event: ServerSentEvent = { data: undefined as TData } + let seen = false + + for (const line of raw.split('\n')) { + if (!line || line.startsWith(':')) continue + seen = true + const index = line.indexOf(':') + const field = index === -1 ? line : line.slice(0, index) + const value = index === -1 ? '' : line.slice(index + 1).replace(/^ /, '') + if (field === 'data') data.push(value) + else if (field === 'event') event.event = value + else if (field === 'id') event.id = value + else if (field === 'retry' && Number.isFinite(Number(value))) event.retry = Number(value) + } + + if (!seen) return undefined + + if (data.length) { + const joined = data.join('\n') + try { + event.data = JSON.parse(joined) as TData + } catch { + event.data = joined as TData + } + } + return event +} + +/** + * Parses a `text/event-stream` body into typed Server-Sent Events, consumed with `for await` and stopped early by breaking the loop. + */ +export async function* parseEventStream( + stream: ReadableStream | AsyncIterable, +): AsyncGenerator> { + const decoder = new TextDecoder() + const normalize = (text: string) => text.replace(/\r\n|\r/g, '\n') + let buffer = '' + + for await (const chunk of readBytes(stream)) { + const blocks = normalize(buffer + decoder.decode(chunk, { stream: true })).split('\n\n') + buffer = blocks.pop() ?? '' + for (const block of blocks) { + const event = parseEvent(block) + if (event) yield event + } + } + + const event = parseEvent(normalize(buffer + decoder.decode())) + if (event) yield event +} + +/** + * The resolved shape returned by a generated `text/event-stream` operation: the typed event + * `stream` plus the native `response`. + */ +export type EventStreamResult = { + stream: AsyncGenerator> + response: TResponse +} + +/** + * Wraps a transport result whose `data` is a streaming body into an `EventStreamResult`, exposing + * the parsed events as a typed async iterator. Generated SSE operations call this. + */ +export async function toEventStream(result: Promise<{ data: unknown; response: AxiosResponse }>): Promise> { + const { data, response } = await result + return { + response, + stream: parseEventStream(data as ReadableStream | AsyncIterable), + } +} + +export const client = createClientCore() + +export const createClient = (config?: Parameters[0]) => client.createClient(config) diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/serializers.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/serializers.ts new file mode 100644 index 000000000..dd2b2e1fc --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/serializers.ts @@ -0,0 +1,439 @@ +export type HeaderValue = string | number | boolean | null | undefined | object +export type HeadersInit = Array<[string, HeaderValue]> | Record + +/** + * The OpenAPI query-parameter serialization style. `form` is the default; `spaceDelimited` and + * `pipeDelimited` join arrays with a space or pipe, and `deepObject` renders objects as + * `key[prop]=value`. + */ +export type QueryStyle = 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject' + +/** + * The serialization metadata shared by the styled parameter locations: the OpenAPI `style` (typed per + * location through `TStyle`), `explode`, and `allowReserved` (keeps RFC 3986 reserved characters + * unencoded, used by query and request bodies). + */ +export type SerializationStyle = { + style?: TStyle + explode?: boolean + allowReserved?: boolean +} + +/** + * The per-parameter query serialization metadata carried by the generated request. + */ +export type QueryParamStyle = SerializationStyle + +/** + * Serializes the query object into a search string. The optional second argument carries the + * per-parameter OpenAPI `style` / `explode` / `allowReserved` metadata; without it arrays explode + * into repeated keys and nested objects use the `deepObject` style. + */ +export type QuerySerializer = (params: Record, options?: Record) => string + +/** + * The per-parameter cookie serialization metadata carried by the generated request. Cookies use the + * OpenAPI `form` style, so only `explode` is configurable. + */ +export type CookieParamStyle = { + explode?: boolean +} + +/** + * The per-parameter header serialization metadata carried by the generated request. Headers use the + * OpenAPI `simple` style, so only `explode` is configurable. + * + * @example + * ```ts + * // styles.header: { 'X-Ids': { explode: false } }, header [3, 4] -> 'X-Ids: 3,4' + * // styles.header: { 'X-Filter': { explode: true } }, header { role: 'admin' } -> 'X-Filter: role=admin' + * ``` + */ +export type HeaderParamStyle = { + explode?: boolean +} + +/** + * The per-property `encoding` metadata for an `application/x-www-form-urlencoded` or + * `multipart/form-data` request body. `contentType` overrides the part's media type; `style` / + * `explode` / `allowReserved` follow the OpenAPI query rules for urlencoded bodies. + */ +export type BodyEncoding = SerializationStyle & { + contentType?: string +} + +/** + * Serializes the request body. JSON by default; `FormData`, `URLSearchParams`, `Blob`, + * `ArrayBuffer`, and string bodies pass through untouched. The optional `encoding` argument carries + * the per-property OpenAPI `encoding` metadata for form bodies. + */ +export type BodySerializer = (args: { body: unknown; contentType?: string; encoding?: Record }) => BodyInit | undefined + +/** + * The OpenAPI path-parameter serialization style. `simple` is the default and emits the bare value; + * `label` prefixes a `.` and `matrix` prefixes a `;name=` segment. + */ +export type PathStyle = 'simple' | 'label' | 'matrix' + +/** + * The per-parameter serialization metadata carried by the generated request. `style` selects the + * OpenAPI style and `explode` controls how arrays and objects expand. + */ +export type PathParamStyle = SerializationStyle + +/** + * Serializes a single path parameter for interpolation into the URL, honoring the OpenAPI `style` / + * `explode` passed as `options`. Defaults to `simple` style with `explode: false`: primitives are + * URL-encoded, arrays join their members with commas, and objects flatten to `key,value` pairs. + */ +export type PathSerializer = (args: { name: string; value: unknown; options?: PathParamStyle }) => string + +/** + * The per-concern serializers, grouped so they can be set in one place and overridden per client or + * per call. Each field falls back to the matching `default*Serializer` when omitted. + */ +export type Serializers = { + query?: QuerySerializer + body?: BodySerializer + path?: PathSerializer +} + +/** + * The per-parameter OpenAPI `style` / `explode` metadata a generated request carries, grouped by + * location and keyed by parameter name. Mirrors the `serializer` grouping and feeds the default + * serializers; `body` carries the form `encoding` for a urlencoded or multipart body. + */ +export type Styles = { + path?: Record + query?: Record + header?: Record + cookie?: Record + body?: Record +} + +function isFormBody(body: unknown): body is BodyInit { + return ( + body instanceof FormData || + body instanceof URLSearchParams || + body instanceof Blob || + body instanceof ArrayBuffer || + ArrayBuffer.isView(body) || + typeof body === 'string' + ) +} + +export function isDefaultJsonBody(body: unknown): boolean { + return body !== undefined && body !== null && !isFormBody(body) +} + +/** + * Emits a `bigint` (`format: int64`) as a JSON number, which `JSON.stringify` refuses to do itself. + * Past the safe-integer range it throws, so an id never goes out silently truncated. + */ +function jsonReplacer(_key: string, value: unknown): unknown { + if (typeof value !== 'bigint') return value + if (value > Number.MAX_SAFE_INTEGER || value < Number.MIN_SAFE_INTEGER) + throw new TypeError(`Cannot serialize ${value}n as JSON without losing precision, register a serializer.body to send it another way.`) + return Number(value) +} + +function appendFormDataValue({ formData, key, value, contentType }: { formData: FormData; key: string; value: unknown; contentType?: string }): void { + if (value === undefined || value === null) return + if (value instanceof Blob) formData.append(key, value) + else if (typeof value === 'object' && !(value instanceof Date)) { + const json = JSON.stringify(value, jsonReplacer) + // A part's media type can only be set by wrapping the value in a typed Blob. + formData.append(key, contentType ? new Blob([json], { type: contentType }) : json) + } else formData.append(key, toValue(value)) +} + +/** + * Default body serializer: passes binary/form bodies through and JSON-serializes everything else. + * For `multipart/form-data` plain objects become `FormData` and for + * `application/x-www-form-urlencoded` they become `URLSearchParams`. When `encoding` is supplied each + * urlencoded property follows its OpenAPI `style` / `explode` / `allowReserved`. + * + * @example + * ```ts + * defaultBodySerializer({ body: { name: 'odie' } }) // '{"name":"odie"}' + * defaultBodySerializer({ body: { field: 'x' }, contentType: 'multipart/form-data' }) // FormData + * defaultBodySerializer({ body: { tags: ['a', 'b'] }, contentType: 'application/x-www-form-urlencoded', encoding: { tags: { explode: false } } }) // 'tags=a,b' + * defaultBodySerializer({ body: { meta: { a: 1 } }, contentType: 'multipart/form-data', encoding: { meta: { contentType: 'application/json' } } }) // FormData with a typed Blob part + * ``` + */ +export const defaultBodySerializer: BodySerializer = ({ body, contentType, encoding }) => { + if (body === undefined || body === null) return undefined + if (isFormBody(body)) return body as BodyInit + if (contentType?.includes('multipart/form-data')) { + const formData = new FormData() + for (const [key, value] of Object.entries(body as Record)) { + const partContentType = encoding?.[key]?.contentType + if (Array.isArray(value)) for (const item of value) appendFormDataValue({ formData, key, value: item, contentType: partContentType }) + else appendFormDataValue({ formData, key, value, contentType: partContentType }) + } + return formData + } + if (contentType?.includes('application/x-www-form-urlencoded')) { + if (encoding) return serializeUrlencodedBody(body as Record, encoding) + return new URLSearchParams(body as Record) + } + return JSON.stringify(body, jsonReplacer) +} + +function serializeUrlencodedBody(body: Record, encoding: Record): string { + const parts: Array = [] + for (const [key, value] of Object.entries(body)) { + const propertyEncoding = encoding[key] + parts.push(...(propertyEncoding ? serializeStyledQueryParam({ key, value, options: propertyEncoding }) : serializeDefaultQueryParam(key, value))) + } + return parts.join('&') +} + +function serializeCookie({ name, value, explode }: { name: string; value: unknown; explode: boolean }): string { + if (Array.isArray(value)) { + const items = value.filter(notNullish).map((item) => encodeURIComponent(toValue(item))) + return explode ? items.map((item) => `${name}=${item}`).join('; ') : `${name}=${items.join(',')}` + } + if (isRecord(value)) { + const entries = Object.entries(value).filter(([, item]) => notNullish(item)) + if (explode) return entries.map(([key, item]) => `${key}=${encodeURIComponent(toValue(item))}`).join('; ') + return `${name}=${entries + .flatMap(([key, item]) => [key, item]) + .map((item) => encodeURIComponent(toValue(item))) + .join(',')}` + } + return `${name}=${encodeURIComponent(toValue(value))}` +} + +/** + * Serializes cookie parameters into a `Cookie` header value using the OpenAPI `form` style, joined + * with `; `. Values are URL-encoded and `explode` is honored per parameter. + * + * @example + * ```ts + * serializeCookies({ session: 'abc', ids: [1, 2] }) // 'session=abc; ids=1,2' + * serializeCookies({ ids: [1, 2] }, { ids: { explode: true } }) // 'ids=1; ids=2' + * ``` + */ +export function serializeCookies(cookies: Record, styles?: Record): string { + const parts: Array = [] + for (const [name, value] of Object.entries(cookies)) { + if (value === undefined || value === null) continue + parts.push(serializeCookie({ name, value, explode: styles?.[name]?.explode ?? false })) + } + return parts.join('; ') +} + +function appendQueryValue({ search, key, value }: { search: URLSearchParams; key: string; value: unknown }): void { + if (value === undefined || value === null) return + if (Array.isArray(value)) { + for (const item of value) appendQueryValue({ search, key, value: item }) + return + } + if (isRecord(value)) { + for (const [prop, propValue] of Object.entries(value)) { + appendQueryValue({ search, key: `${key}[${prop}]`, value: propValue }) + } + return + } + search.append(key, toValue(value)) +} + +const queryDelimiters: Record = { form: ',', spaceDelimited: '%20', pipeDelimited: '|', deepObject: ',' } + +function notNullish(value: unknown): boolean { + return value !== undefined && value !== null +} + +/** + * Renders a primitive parameter value as a string, serializing `Date` to ISO-8601 so dates are + * stable across path, query, cookie, and header locations rather than locale-dependent. + */ +function toValue(value: unknown): string { + return value instanceof Date ? value.toISOString() : String(value) +} + +/** + * Percent-encodes a value, keeping RFC 3986 reserved characters intact (used when `allowReserved` is set). + */ +function encodeReserved(value: unknown): string { + return encodeURI(toValue(value)) +} + +/** + * Percent-encodes a value, escaping reserved characters (the default query/path encoder). + */ +function encodeComponent(value: unknown): string { + return encodeURIComponent(toValue(value)) +} + +/** + * Whether a value should expand into bracketed/keyed parts. Arrays and `Date` are excluded so they + * are serialized as a unit (a `Date` becomes an ISO string, not its enumerable own properties). + */ +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date) +} + +/** + * Expands an object or array into `deepObject` query parts, recursing into nested values so + * `{ a: { b: { c: 1 } } }` becomes `a[b][c]=1`. Primitives terminate the recursion. + */ +function serializeDeepObject({ key, value, encode }: { key: string; value: unknown; encode: (value: unknown) => string }): Array { + if (value === undefined || value === null) return [] + if (Array.isArray(value)) return value.flatMap((item, index) => serializeDeepObject({ key: `${key}[${index}]`, value: item, encode })) + if (isRecord(value)) { + return Object.entries(value) + .filter(([, item]) => notNullish(item)) + .flatMap(([prop, item]) => serializeDeepObject({ key: `${key}[${prop}]`, value: item, encode })) + } + return [`${encode(key)}=${encode(value)}`] +} + +function serializeStyledQueryArray({ + key, + value, + options, + encode, +}: { + key: string + value: Array + options: QueryParamStyle + encode: (value: unknown) => string +}): Array { + const items = value.filter(notNullish) + if (options.explode ?? true) return items.map((item) => `${encode(key)}=${encode(item)}`) + return [`${encode(key)}=${items.map(encode).join(queryDelimiters[options.style ?? 'form'])}`] +} + +function serializeStyledQueryObject({ + key, + value, + options, + encode, +}: { + key: string + value: Record + options: QueryParamStyle + encode: (value: unknown) => string +}): Array { + if ((options.style ?? 'form') === 'deepObject') return serializeDeepObject({ key, value, encode }) + const entries = Object.entries(value).filter(([, item]) => notNullish(item)) + if (options.explode ?? true) return entries.map(([prop, item]) => `${encode(prop)}=${encode(item)}`) + return [ + `${encode(key)}=${entries + .flatMap(([prop, item]) => [prop, item]) + .map(encode) + .join(',')}`, + ] +} + +function serializeStyledQueryParam({ key, value, options }: { key: string; value: unknown; options: QueryParamStyle }): Array { + if (value === undefined || value === null) return [] + const encode = options.allowReserved ? encodeReserved : encodeComponent + if (Array.isArray(value)) return serializeStyledQueryArray({ key, value, options, encode }) + if (isRecord(value)) return serializeStyledQueryObject({ key, value, options, encode }) + return [`${encode(key)}=${encode(value)}`] +} + +function serializeDefaultQueryParam(key: string, value: unknown): Array { + const search = new URLSearchParams() + appendQueryValue({ search, key, value }) + const result = search.toString() + return result ? [result] : [] +} + +/** + * Default query serializer. Members with `options` metadata follow their OpenAPI `style` / `explode` + * / `allowReserved`. Members without it keep the defaults: arrays explode into repeated keys and + * nested objects use the `deepObject` style (`key[prop]=value`). + * + * @example + * ```ts + * defaultQuerySerializer({ id: [3, 4, 5] }) // 'id=3&id=4&id=5' + * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'form', explode: false } }) // 'id=3,4,5' + * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'spaceDelimited', explode: false } }) // 'id=3%204%205' + * defaultQuerySerializer({ id: [3, 4, 5] }, { id: { style: 'pipeDelimited', explode: false } }) // 'id=3|4|5' + * defaultQuerySerializer({ a: { b: 1 } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D=1' + * defaultQuerySerializer({ a: { b: { c: 1 } } }, { a: { style: 'deepObject' } }) // 'a%5Bb%5D%5Bc%5D=1' + * ``` + */ +export const defaultQuerySerializer: QuerySerializer = (params, options) => { + const parts: Array = [] + for (const [key, value] of Object.entries(params)) { + const paramOptions = options?.[key] + parts.push(...(paramOptions ? serializeStyledQueryParam({ key, value, options: paramOptions }) : serializeDefaultQueryParam(key, value))) + } + return parts.join('&') +} + +function serializePathPrimitive({ name, value, style }: { name: string; value: unknown; style: PathStyle }): string { + const encoded = encodeComponent(value) + if (style === 'label') return `.${encoded}` + if (style === 'matrix') return `;${name}=${encoded}` + return encoded +} + +function serializePathArray({ name, value, style, explode }: { name: string; value: Array; style: PathStyle; explode: boolean }): string { + const items = value.map(encodeComponent) + if (style === 'label') return `.${items.join(explode ? '.' : ',')}` + if (style === 'matrix') return explode ? items.map((item) => `;${name}=${item}`).join('') : `;${name}=${items.join(',')}` + return items.join(',') +} + +function serializePathObject({ name, value, style, explode }: { name: string; value: Record; style: PathStyle; explode: boolean }): string { + const members = Object.entries(value).map(([key, item]) => + explode ? `${encodeComponent(key)}=${encodeComponent(item)}` : `${encodeComponent(key)},${encodeComponent(item)}`, + ) + if (style === 'label') return `.${members.join(explode ? '.' : ',')}` + if (style === 'matrix') return explode ? members.map((member) => `;${member}`).join('') : `;${name}=${members.join(',')}` + return members.join(',') +} + +/** + * Default path serializer honoring the OpenAPI `style` / `explode` metadata. Without metadata it + * falls back to `simple` style with `explode: false`. Replaces the previous `String(value)` + * interpolation, which emitted `[object Object]` for object path params. + * + * @example + * ```ts + * defaultPathSerializer({ name: 'id', value: [3, 4, 5] }) // '3,4,5' + * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'label', explode: true } }) // '.3.4.5' + * defaultPathSerializer({ name: 'id', value: [3, 4, 5], options: { style: 'matrix', explode: true } }) // ';id=3;id=4;id=5' + * defaultPathSerializer({ name: 'pt', value: { x: 1, y: 2 } }) // 'x,1,y,2' + * ``` + */ +export const defaultPathSerializer: PathSerializer = ({ name, value, options }) => { + if (value === undefined || value === null) return '' + const style = options?.style ?? 'simple' + const explode = options?.explode ?? false + if (Array.isArray(value)) return serializePathArray({ name, value, style, explode }) + if (isRecord(value)) return serializePathObject({ name, value, style, explode }) + return serializePathPrimitive({ name, value, style }) +} + +function serializeHeaderValue(value: unknown, explode: boolean): string { + if (Array.isArray(value)) return value.filter(notNullish).map(toValue).join(',') + if (!isRecord(value)) return toValue(value) + const entries = Object.entries(value).filter(([, item]) => notNullish(item)) + if (explode) return entries.map(([key, item]) => `${key}=${toValue(item)}`).join(',') + return entries + .flatMap(([key, item]) => [key, item]) + .map(toValue) + .join(',') +} + +/** + * Serializes array and object header parameters with the OpenAPI `simple` style before they are + * merged. Header values are not URL-encoded. Primitive values and headers without metadata pass + * through untouched. + */ +export function applyHeaderStyles(headers: HeadersInit | undefined, styles: Record | undefined): HeadersInit | undefined { + if (!headers || !styles) return headers + const entries = Array.isArray(headers) ? headers : Object.entries(headers) + return entries.map(([key, value]) => { + const style = styles[key] + if (!style || value === undefined || value === null || typeof value !== 'object') return [key, value] as [string, HeaderValue] + return [key, serializeHeaderValue(value, style.explode ?? false)] as [string, HeaderValue] + }) +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/standardSchema.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/standardSchema.ts new file mode 100644 index 000000000..27c488d23 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/.kubb/standardSchema.ts @@ -0,0 +1,54 @@ +/** + * A Standard Schema-compatible validator: a minimal duck-type covering Zod v3/v4, valibot, and + * arktype schemas. Only the `~standard.validate` method is required at runtime. + */ +export type StandardSchemaValidator = { + readonly '~standard': { + validate(value: unknown): StandardSchemaResult | Promise> + } +} + +/** + * The two possible outcomes of a Standard Schema `validate` call. A successful result carries + * `value`; a failed result carries `issues`. + */ +export type StandardSchemaResult = { readonly value: TOutput; readonly issues?: undefined } | { readonly issues: ReadonlyArray } + +/** + * One validation issue from a Standard Schema `validate` call. + */ +export type StandardSchemaIssue = { + readonly message?: string + readonly path?: ReadonlyArray +} + +/** + * Thrown by `validateStandardSchema` when validation fails. Carries the raw `issues` array from + * the schema's `validate` result so callers receive a uniform error shape regardless of which + * schema library is in use. + */ +export class ParseError extends Error { + readonly issues: ReadonlyArray + + constructor({ issues, message }: { issues: ReadonlyArray; message?: string }) { + super(message ?? 'Validation failed') + this.name = 'ParseError' + this.issues = issues + } +} + +/** + * Validates `value` against a Standard Schema-compatible `schema`. Returns the parsed output on + * success; throws `ParseError` with the schema's `issues` on failure. Handles both sync and async + * `validate` implementations transparently. + * + * @example + * const pet = await validateStandardSchema(PetSchema, rawData) + */ +export async function validateStandardSchema(schema: StandardSchemaValidator, value: unknown): Promise { + const result = await schema['~standard'].validate(value) + if (result.issues) { + throw new ParseError({ issues: result.issues }) + } + return (result as { value: TOutput }).value +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/addPet.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/addPet.ts new file mode 100644 index 000000000..98ed40e8c --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/addPet.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, UnwrappedResult } from '../.kubb/client' +import type { AddPetOptions, AddPetResponses } from '../types/AddPet' +import { client, unwrapResult } from '../.kubb/client' + +/** + * @description Add a new pet to the store + * @summary Add a new pet to the store + * {@link /pet} + */ +export function addPet(options: Options): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'POST', url: '/pet', security: [{ type: 'oauth2' }], ...config }), config.throwOnError) as Promise> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/deletePet.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/deletePet.ts new file mode 100644 index 000000000..902f84827 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/deletePet.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, UnwrappedResult } from '../.kubb/client' +import type { DeletePetOptions, DeletePetResponses } from '../types/DeletePet' +import { client, unwrapResult } from '../.kubb/client' + +/** + * @description delete a pet + * @summary Deletes a pet + * {@link /pet/:petId} + */ +export function deletePet(options: Options): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'DELETE', url: '/pet/{petId}', security: [{ type: 'oauth2' }], ...config }), config.throwOnError) as Promise> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/findPetsByStatus.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/findPetsByStatus.ts new file mode 100644 index 000000000..fffbdea89 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/findPetsByStatus.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, UnwrappedResult } from '../.kubb/client' +import type { FindPetsByStatusOptions, FindPetsByStatusResponses } from '../types/FindPetsByStatus' +import { client, unwrapResult } from '../.kubb/client' + +/** + * @description Multiple status values can be provided with comma separated strings + * @summary Finds Pets by status + * {@link /pet/findByStatus} + */ +export function findPetsByStatus(options: Options = {}): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/pet/findByStatus', security: [{ type: 'oauth2' }], styles: { query: { status: { explode: true } } }, ...config }), config.throwOnError) as Promise> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/getInventory.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/getInventory.ts new file mode 100644 index 000000000..58fcd7af8 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/getInventory.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, UnwrappedResult } from '../.kubb/client' +import type { GetInventoryOptions, GetInventoryResponses } from '../types/GetInventory' +import { client, unwrapResult } from '../.kubb/client' + +/** + * @description Returns a map of status codes to quantities + * @summary Returns pet inventories by status + * {@link /store/inventory} + */ +export function getInventory(options: Options = {}): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/store/inventory', security: [{ type: 'apiKey', name: 'api_key', in: 'header' }], ...config }), config.throwOnError) as Promise> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/getPetById.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/getPetById.ts new file mode 100644 index 000000000..c4a17c147 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/getPetById.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, UnwrappedResult } from '../.kubb/client' +import type { GetPetByIdOptions, GetPetByIdResponses } from '../types/GetPetById' +import { client, unwrapResult } from '../.kubb/client' + +/** + * @description Returns a single pet + * @summary Find pet by ID + * {@link /pet/:petId} + */ +export function getPetById(options: Options): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'GET', url: '/pet/{petId}', security: [{ type: 'apiKey', name: 'api_key', in: 'header' }, { type: 'oauth2' }], ...config }), config.throwOnError) as Promise> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/placeOrder.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/placeOrder.ts new file mode 100644 index 000000000..7f62226e0 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/placeOrder.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, UnwrappedResult } from '../.kubb/client' +import type { PlaceOrderOptions, PlaceOrderResponses } from '../types/PlaceOrder' +import { client, unwrapResult } from '../.kubb/client' + +/** + * @description Place a new order in the store + * @summary Place an order for a pet + * {@link /store/order} + */ +export function placeOrder(options: Options): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'POST', url: '/store/order', ...config }), config.throwOnError) as Promise> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/uploadFile.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/uploadFile.ts new file mode 100644 index 000000000..14074f38c --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/clients/uploadFile.ts @@ -0,0 +1,18 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Options, UnwrappedResult } from '../.kubb/client' +import type { UploadFileOptions, UploadFileResponses } from '../types/UploadFile' +import { client, unwrapResult } from '../.kubb/client' + +/** + * @summary uploads an image + * {@link /pet/:petId/uploadImage} + */ +export function uploadFile(options: Options): Promise> { + const { client: request = client, ...config } = options + + return unwrapResult(request({ method: 'POST', url: '/pet/{petId}/uploadImage', security: [{ type: 'oauth2' }], contentType: { request: 'application/octet-stream' }, ...config }), config.throwOnError) as Promise> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useAddPet.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useAddPet.ts new file mode 100644 index 000000000..e70aa8a1a --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useAddPet.ts @@ -0,0 +1,44 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { RequestConfig, ResponseErrorConfig } from '../.kubb/client' +import type { AddPetOptions, AddPetStatus200, AddPetStatus405 } from '../types/AddPet' +import type { UseMutationOptions, UseMutationResult, QueryClient } from '@tanstack/react-query' +import { addPet } from '../clients/addPet' +import { mutationOptions, useMutation } from '@tanstack/react-query' + +export const addPetMutationKey = () => [{ url: '/pet' }] as const + +export function addPetMutationOptions(config: Partial> & { contentType?: { request?: "application/json" | "application/xml" | "application/x-www-form-urlencoded"; response?: "application/json" | "application/xml" } } = {}) { + const mutationKey = addPetMutationKey() + return mutationOptions, AddPetOptions, TContext>({ + mutationKey, + mutationFn: async({ body }) => { + return await addPet({ ...config, body, throwOnError: true }) + }, + }) +} + +/** + * @description Add a new pet to the store + * @summary Add a new pet to the store + * {@link /pet} + */ +export function useAddPet(options: { + mutation?: UseMutationOptions, AddPetOptions, TContext> & { client?: QueryClient }, + client?: Partial> & { contentType?: { request?: "application/json" | "application/xml" | "application/x-www-form-urlencoded"; response?: "application/json" | "application/xml" } }, +} = {}) { + const { mutation = {}, client: config = {} } = options ?? {} + const { client: queryClient, ...mutationOptions } = mutation; + const mutationKey = mutationOptions.mutationKey ?? addPetMutationKey() + + const baseOptions = addPetMutationOptions(config) as UseMutationOptions, AddPetOptions, TContext> + + return useMutation, AddPetOptions, TContext>({ + ...baseOptions, + mutationKey, + ...mutationOptions, + }, queryClient) as UseMutationResult, AddPetOptions, TContext> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useDeletePet.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useDeletePet.ts new file mode 100644 index 000000000..2509e412e --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useDeletePet.ts @@ -0,0 +1,44 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { RequestConfig, ResponseErrorConfig } from '../.kubb/client' +import type { DeletePetOptions, DeletePetResponse, DeletePetStatus400 } from '../types/DeletePet' +import type { UseMutationOptions, UseMutationResult, QueryClient } from '@tanstack/react-query' +import { deletePet } from '../clients/deletePet' +import { mutationOptions, useMutation } from '@tanstack/react-query' + +export const deletePetMutationKey = () => [{ url: '/pet/:petId' }] as const + +export function deletePetMutationOptions(config: Partial> = {}) { + const mutationKey = deletePetMutationKey() + return mutationOptions, DeletePetOptions, TContext>({ + mutationKey, + mutationFn: async({ path, headers }) => { + return await deletePet({ ...config, path, headers, throwOnError: true }) + }, + }) +} + +/** + * @description delete a pet + * @summary Deletes a pet + * {@link /pet/:petId} + */ +export function useDeletePet(options: { + mutation?: UseMutationOptions, DeletePetOptions, TContext> & { client?: QueryClient }, + client?: Partial>, +} = {}) { + const { mutation = {}, client: config = {} } = options ?? {} + const { client: queryClient, ...mutationOptions } = mutation; + const mutationKey = mutationOptions.mutationKey ?? deletePetMutationKey() + + const baseOptions = deletePetMutationOptions(config) as UseMutationOptions, DeletePetOptions, TContext> + + return useMutation, DeletePetOptions, TContext>({ + ...baseOptions, + mutationKey, + ...mutationOptions, + }, queryClient) as UseMutationResult, DeletePetOptions, TContext> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useFindPetsByStatus.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useFindPetsByStatus.ts new file mode 100644 index 000000000..37d9b5810 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useFindPetsByStatus.ts @@ -0,0 +1,49 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { RequestConfig, ResponseErrorConfig } from '../.kubb/client' +import type { FindPetsByStatusOptions, FindPetsByStatusStatus200, FindPetsByStatusStatus400 } from '../types/FindPetsByStatus' +import type { QueryKey, QueryClient, QueryObserverOptions, UseQueryResult } from '@tanstack/react-query' +import { findPetsByStatus } from '../clients/findPetsByStatus' +import { queryOptions, useQuery } from '@tanstack/react-query' + +export const findPetsByStatusQueryKey = ({ query }: Omit = {}) => [{ url: '/pet/findByStatus' }, ...(query ? [query] : [])] as const + +type FindPetsByStatusQueryKey = ReturnType + +export function findPetsByStatusQueryOptions({ query }: FindPetsByStatusOptions = {}, config: Partial> = {}) { + const queryKey = findPetsByStatusQueryKey({ query }) + return queryOptions, FindPetsByStatusStatus200, typeof queryKey>({ + queryKey, + queryFn: async ({ signal }) => { + return await findPetsByStatus({ ...config, query, signal: config.signal ?? signal, throwOnError: true }) + }, + }) +} + +/** + * @description Multiple status values can be provided with comma separated strings + * @summary Finds Pets by status + * {@link /pet/findByStatus} + */ +export function useFindPetsByStatus({ query }: { query?: FindPetsByStatusOptions['query'] | (() => FindPetsByStatusOptions['query']) } = {}, options: { + query?: Partial, TData, TQueryData, TQueryKey>> & { client?: QueryClient }, + client?: Partial> +} = {}) { + const { query: queryConfig = {}, client: config = {} } = options ?? {} + const { client: queryClient, ...resolvedOptions } = queryConfig + const resolvedParams = { query: typeof query === 'function' ? query() : query } + const queryKey = resolvedOptions?.queryKey ?? findPetsByStatusQueryKey(resolvedParams) + + const queryResult = useQuery({ + ...findPetsByStatusQueryOptions(resolvedParams, config), + ...resolvedOptions, + queryKey, + } as unknown as QueryObserverOptions, queryClient) as UseQueryResult> & { queryKey: TQueryKey } + + queryResult.queryKey = queryKey as TQueryKey + + return queryResult +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useGetInventory.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useGetInventory.ts new file mode 100644 index 000000000..d8b4b0e72 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useGetInventory.ts @@ -0,0 +1,48 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { RequestConfig, ResponseErrorConfig } from '../.kubb/client' +import type { GetInventoryStatus200 } from '../types/GetInventory' +import type { QueryKey, QueryClient, QueryObserverOptions, UseQueryResult } from '@tanstack/react-query' +import { getInventory } from '../clients/getInventory' +import { queryOptions, useQuery } from '@tanstack/react-query' + +export const getInventoryQueryKey = () => [{ url: '/store/inventory' }] as const + +type GetInventoryQueryKey = ReturnType + +export function getInventoryQueryOptions(config: Partial> = {}) { + const queryKey = getInventoryQueryKey() + return queryOptions, GetInventoryStatus200, typeof queryKey>({ + queryKey, + queryFn: async ({ signal }) => { + return await getInventory({ ...config, signal: config.signal ?? signal, throwOnError: true }) + }, + }) +} + +/** + * @description Returns a map of status codes to quantities + * @summary Returns pet inventories by status + * {@link /store/inventory} + */ +export function useGetInventory(options: { + query?: Partial, TData, TQueryData, TQueryKey>> & { client?: QueryClient }, + client?: Partial> +} = {}) { + const { query: queryConfig = {}, client: config = {} } = options ?? {} + const { client: queryClient, ...resolvedOptions } = queryConfig + const queryKey = resolvedOptions?.queryKey ?? getInventoryQueryKey() + + const queryResult = useQuery({ + ...getInventoryQueryOptions(config), + ...resolvedOptions, + queryKey, + } as unknown as QueryObserverOptions, queryClient) as UseQueryResult> & { queryKey: TQueryKey } + + queryResult.queryKey = queryKey as TQueryKey + + return queryResult +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useGetPetById.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useGetPetById.ts new file mode 100644 index 000000000..91ab2f752 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useGetPetById.ts @@ -0,0 +1,49 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { RequestConfig, ResponseErrorConfig } from '../.kubb/client' +import type { GetPetByIdOptions, GetPetByIdStatus200, GetPetByIdStatus400, GetPetByIdStatus404 } from '../types/GetPetById' +import type { QueryKey, QueryClient, QueryObserverOptions, UseQueryResult } from '@tanstack/react-query' +import { getPetById } from '../clients/getPetById' +import { queryOptions, useQuery } from '@tanstack/react-query' + +export const getPetByIdQueryKey = ({ path }: Omit) => [{ url: '/pet/:petId', params: path }] as const + +type GetPetByIdQueryKey = ReturnType + +export function getPetByIdQueryOptions({ path }: GetPetByIdOptions, config: Partial> = {}) { + const queryKey = getPetByIdQueryKey({ path }) + return queryOptions, GetPetByIdStatus200, typeof queryKey>({ + queryKey, + queryFn: async ({ signal }) => { + return await getPetById({ ...config, path, signal: config.signal ?? signal, throwOnError: true }) + }, + }) +} + +/** + * @description Returns a single pet + * @summary Find pet by ID + * {@link /pet/:petId} + */ +export function useGetPetById({ path }: { path: GetPetByIdOptions['path'] | (() => GetPetByIdOptions['path']) }, options: { + query?: Partial, TData, TQueryData, TQueryKey>> & { client?: QueryClient }, + client?: Partial> +} = {}) { + const { query: queryConfig = {}, client: config = {} } = options ?? {} + const { client: queryClient, ...resolvedOptions } = queryConfig + const resolvedParams = { path: typeof path === 'function' ? path() : path } + const queryKey = resolvedOptions?.queryKey ?? getPetByIdQueryKey(resolvedParams) + + const queryResult = useQuery({ + ...getPetByIdQueryOptions(resolvedParams, config), + ...resolvedOptions, + queryKey, + } as unknown as QueryObserverOptions, queryClient) as UseQueryResult> & { queryKey: TQueryKey } + + queryResult.queryKey = queryKey as TQueryKey + + return queryResult +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/usePlaceOrder.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/usePlaceOrder.ts new file mode 100644 index 000000000..ef9ade17d --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/usePlaceOrder.ts @@ -0,0 +1,44 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { RequestConfig, ResponseErrorConfig } from '../.kubb/client' +import type { PlaceOrderOptions, PlaceOrderStatus200, PlaceOrderStatus405 } from '../types/PlaceOrder' +import type { UseMutationOptions, UseMutationResult, QueryClient } from '@tanstack/react-query' +import { placeOrder } from '../clients/placeOrder' +import { mutationOptions, useMutation } from '@tanstack/react-query' + +export const placeOrderMutationKey = () => [{ url: '/store/order' }] as const + +export function placeOrderMutationOptions(config: Partial> & { contentType?: { request?: "application/json" | "application/xml" | "application/x-www-form-urlencoded" } } = {}) { + const mutationKey = placeOrderMutationKey() + return mutationOptions, PlaceOrderOptions, TContext>({ + mutationKey, + mutationFn: async({ body }) => { + return await placeOrder({ ...config, body, throwOnError: true }) + }, + }) +} + +/** + * @description Place a new order in the store + * @summary Place an order for a pet + * {@link /store/order} + */ +export function usePlaceOrder(options: { + mutation?: UseMutationOptions, PlaceOrderOptions, TContext> & { client?: QueryClient }, + client?: Partial> & { contentType?: { request?: "application/json" | "application/xml" | "application/x-www-form-urlencoded" } }, +} = {}) { + const { mutation = {}, client: config = {} } = options ?? {} + const { client: queryClient, ...mutationOptions } = mutation; + const mutationKey = mutationOptions.mutationKey ?? placeOrderMutationKey() + + const baseOptions = placeOrderMutationOptions(config) as UseMutationOptions, PlaceOrderOptions, TContext> + + return useMutation, PlaceOrderOptions, TContext>({ + ...baseOptions, + mutationKey, + ...mutationOptions, + }, queryClient) as UseMutationResult, PlaceOrderOptions, TContext> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useUploadFile.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useUploadFile.ts new file mode 100644 index 000000000..b6e686477 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/hooks/useUploadFile.ts @@ -0,0 +1,43 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { RequestConfig, ResponseErrorConfig } from '../.kubb/client' +import type { UploadFileOptions, UploadFileStatus200 } from '../types/UploadFile' +import type { UseMutationOptions, UseMutationResult, QueryClient } from '@tanstack/react-query' +import { uploadFile } from '../clients/uploadFile' +import { mutationOptions, useMutation } from '@tanstack/react-query' + +export const uploadFileMutationKey = () => [{ url: '/pet/:petId/uploadImage' }] as const + +export function uploadFileMutationOptions(config: Partial> = {}) { + const mutationKey = uploadFileMutationKey() + return mutationOptions, UploadFileOptions, TContext>({ + mutationKey, + mutationFn: async({ path, query, body }) => { + return await uploadFile({ ...config, path, query, body, throwOnError: true }) + }, + }) +} + +/** + * @summary uploads an image + * {@link /pet/:petId/uploadImage} + */ +export function useUploadFile(options: { + mutation?: UseMutationOptions, UploadFileOptions, TContext> & { client?: QueryClient }, + client?: Partial>, +} = {}) { + const { mutation = {}, client: config = {} } = options ?? {} + const { client: queryClient, ...mutationOptions } = mutation; + const mutationKey = mutationOptions.mutationKey ?? uploadFileMutationKey() + + const baseOptions = uploadFileMutationOptions(config) as UseMutationOptions, UploadFileOptions, TContext> + + return useMutation, UploadFileOptions, TContext>({ + ...baseOptions, + mutationKey, + ...mutationOptions, + }, queryClient) as UseMutationResult, UploadFileOptions, TContext> +} diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPet.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPet.ts new file mode 100644 index 000000000..60a15d709 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPet.ts @@ -0,0 +1,58 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { AddPetRequest } from './AddPetRequest' +import type { Pet } from './Pet' + +export type AddPetStatus200Json = Pet; + +export type AddPetStatus200Xml = Pet; + +export type AddPetStatus200 = (AddPetStatus200Json | AddPetStatus200Xml); + +export type AddPetStatus405 = unknown; + +/** + * @description Create a new pet in the store + * @type object +*/ +export type AddPetBodyJson = AddPetRequest; + +/** + * @description Create a new pet in the store + * @type object +*/ +export type AddPetBodyXml = Pet; + +/** + * @description Create a new pet in the store + * @type object +*/ +export type AddPetBodyFormUrlEncoded = Pet; + +export type AddPetBody = (AddPetBodyJson | AddPetBodyXml | AddPetBodyFormUrlEncoded); + +export type AddPetOptions = { + body: AddPetBody; + path?: never; + query?: never; + headers?: never; +}; + +export type AddPetResponses = { + "200": ({ + contentType: "application/json"; + data: AddPetStatus200Json; + } | { + contentType: "application/xml"; + data: AddPetStatus200Xml; + }); + "405": AddPetStatus405; +}; + +/** + * @description Union of all possible responses +*/ +export type AddPetResponse = (AddPetStatus200 | AddPetStatus405); diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPetRequest.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPetRequest.ts new file mode 100644 index 000000000..45a4217ac --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPetRequest.ts @@ -0,0 +1,30 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { AddPetRequestStatusEnumKey } from './AddPetRequestStatusEnum' +import type { Category } from './Category' +import type { Tag } from './Tag' + +export type AddPetRequest = { + /** + * @description + * Format: `int64` + * @example 10 + * @type integer | undefined + */ + id?: bigint; + /** + * @example doggie + * @type string + */ + name: string; + category?: Category; + photoUrls: string[]; + tags?: Tag[]; + /** + * @description pet status in the store + */ + status?: AddPetRequestStatusEnumKey; +}; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPetRequestStatusEnum.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPetRequestStatusEnum.ts new file mode 100644 index 000000000..5be95333a --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/AddPetRequestStatusEnum.ts @@ -0,0 +1,12 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export const addPetRequestStatusEnum = { + available: "available", + pending: "pending", + sold: "sold" +} as const; + +export type AddPetRequestStatusEnumKey = (typeof addPetRequestStatusEnum)[keyof typeof addPetRequestStatusEnum]; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/ApiResponse.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/ApiResponse.ts new file mode 100644 index 000000000..221bf2ba3 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/ApiResponse.ts @@ -0,0 +1,15 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export type ApiResponse = { + /** + * @description + * Format: `int32` + * @type integer | undefined + */ + code?: number; + type?: string; + message?: string; +}; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Category.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Category.ts new file mode 100644 index 000000000..5af2ddfc9 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Category.ts @@ -0,0 +1,19 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export type Category = { + /** + * @description + * Format: `int64` + * @example 1 + * @type integer | undefined + */ + id?: bigint; + /** + * @example Dogs + * @type string | undefined + */ + name?: string; +}; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/DeletePet.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/DeletePet.ts new file mode 100644 index 000000000..d2a48a696 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/DeletePet.ts @@ -0,0 +1,36 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export type DeletePetPath = { + /** + * @description Pet id to delete + * + * Format: `int64` + * @type integer + */ + petId: bigint; +}; + +export type DeletePetHeaders = { + api_key?: string; +}; + +export type DeletePetStatus400 = unknown; + +export type DeletePetOptions = { + body?: never; + path: DeletePetPath; + query?: never; + headers?: DeletePetHeaders; +}; + +export type DeletePetResponses = { + "400": DeletePetStatus400; +}; + +/** + * @description Union of all possible responses +*/ +export type DeletePetResponse = DeletePetStatus400; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/FindPetsByStatus.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/FindPetsByStatus.ts new file mode 100644 index 000000000..aa12e1d84 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/FindPetsByStatus.ts @@ -0,0 +1,46 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { FindPetsByStatusStatusKey } from './FindPetsByStatusStatus' +import type { Pet } from './Pet' + +export type FindPetsByStatusQuery = { + /** + * @description Status values that need to be considered for filter + * @default available + */ + status?: FindPetsByStatusStatusKey; +}; + +export type FindPetsByStatusStatus200Json = Pet[]; + +export type FindPetsByStatusStatus200Xml = Pet[]; + +export type FindPetsByStatusStatus200 = (FindPetsByStatusStatus200Json | FindPetsByStatusStatus200Xml); + +export type FindPetsByStatusStatus400 = unknown; + +export type FindPetsByStatusOptions = { + body?: never; + path?: never; + query?: FindPetsByStatusQuery; + headers?: never; +}; + +export type FindPetsByStatusResponses = { + "200": ({ + contentType: "application/json"; + data: FindPetsByStatusStatus200Json; + } | { + contentType: "application/xml"; + data: FindPetsByStatusStatus200Xml; + }); + "400": FindPetsByStatusStatus400; +}; + +/** + * @description Union of all possible responses +*/ +export type FindPetsByStatusResponse = (FindPetsByStatusStatus200 | FindPetsByStatusStatus400); diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/FindPetsByStatusStatus.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/FindPetsByStatusStatus.ts new file mode 100644 index 000000000..8d9cab86e --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/FindPetsByStatusStatus.ts @@ -0,0 +1,12 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export const findPetsByStatusStatus = { + available: "available", + pending: "pending", + sold: "sold" +} as const; + +export type FindPetsByStatusStatusKey = (typeof findPetsByStatusStatus)[keyof typeof findPetsByStatusStatus]; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/GetInventory.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/GetInventory.ts new file mode 100644 index 000000000..5152eb568 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/GetInventory.ts @@ -0,0 +1,24 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export type GetInventoryStatus200 = { + [key: string]: number; +}; + +export type GetInventoryOptions = { + body?: never; + path?: never; + query?: never; + headers?: never; +}; + +export type GetInventoryResponses = { + "200": GetInventoryStatus200; +}; + +/** + * @description Union of all possible responses +*/ +export type GetInventoryResponse = GetInventoryStatus200; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/GetPetById.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/GetPetById.ts new file mode 100644 index 000000000..715be3930 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/GetPetById.ts @@ -0,0 +1,50 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Pet } from './Pet' + +export type GetPetByIdPath = { + /** + * @description ID of pet to return + * + * Format: `int64` + * @type integer + */ + petId: bigint; +}; + +export type GetPetByIdStatus200Json = Pet; + +export type GetPetByIdStatus200Xml = Pet; + +export type GetPetByIdStatus200 = (GetPetByIdStatus200Json | GetPetByIdStatus200Xml); + +export type GetPetByIdStatus400 = unknown; + +export type GetPetByIdStatus404 = unknown; + +export type GetPetByIdOptions = { + body?: never; + path: GetPetByIdPath; + query?: never; + headers?: never; +}; + +export type GetPetByIdResponses = { + "200": ({ + contentType: "application/json"; + data: GetPetByIdStatus200Json; + } | { + contentType: "application/xml"; + data: GetPetByIdStatus200Xml; + }); + "400": GetPetByIdStatus400; + "404": GetPetByIdStatus404; +}; + +/** + * @description Union of all possible responses +*/ +export type GetPetByIdResponse = (GetPetByIdStatus200 | GetPetByIdStatus400 | GetPetByIdStatus404); diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Order.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Order.ts new file mode 100644 index 000000000..3ad730496 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Order.ts @@ -0,0 +1,42 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { OrderStatusEnumKey } from './OrderStatusEnum' + +export type Order = { + /** + * @description + * Format: `int64` + * @example 10 + * @type integer | undefined + */ + id?: bigint; + /** + * @description + * Format: `int64` + * @example 198772 + * @type integer | undefined + */ + petId?: bigint; + /** + * @description + * Format: `int32` + * @example 7 + * @type integer | undefined + */ + quantity?: number; + /** + * @description + * Format: `date-time` + * @type string | undefined + */ + shipDate?: string; + /** + * @description Order Status + * @example approved + */ + status?: OrderStatusEnumKey; + complete?: boolean; +}; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/OrderStatusEnum.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/OrderStatusEnum.ts new file mode 100644 index 000000000..f9549ae8a --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/OrderStatusEnum.ts @@ -0,0 +1,12 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export const orderStatusEnum = { + placed: "placed", + approved: "approved", + delivered: "delivered" +} as const; + +export type OrderStatusEnumKey = (typeof orderStatusEnum)[keyof typeof orderStatusEnum]; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Pet.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Pet.ts new file mode 100644 index 000000000..a6bfb8a13 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Pet.ts @@ -0,0 +1,38 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Category } from './Category' +import type { PetStatusEnumKey } from './PetStatusEnum' +import type { Tag } from './Tag' + +export type Pet = { + /** + * @description + * Format: `int64` + * @example 10 + * @type integer | undefined + */ + id?: bigint; + /** + * @example doggie + * @type string + */ + name: string; + /** + * @minLength 2 + * @maxLength 42 + * @pattern ^[A-Za-z0-9()\[\]'"][-A-Za-z0-9_. \/()\[\]]{0,40}[A-Za-z0-9()\[\]'"]$ + * @example my_log_destination + * @type string | undefined + */ + log?: string; + category?: Category; + photoUrls: string[]; + tags?: Tag[]; + /** + * @description pet status in the store + */ + status?: PetStatusEnumKey; +}; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/PetStatusEnum.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/PetStatusEnum.ts new file mode 100644 index 000000000..225482744 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/PetStatusEnum.ts @@ -0,0 +1,12 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export const petStatusEnum = { + available: "available", + pending: "pending", + sold: "sold" +} as const; + +export type PetStatusEnumKey = (typeof petStatusEnum)[keyof typeof petStatusEnum]; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/PlaceOrder.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/PlaceOrder.ts new file mode 100644 index 000000000..ed84aebb1 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/PlaceOrder.ts @@ -0,0 +1,35 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { Order } from './Order' + +export type PlaceOrderStatus200 = Order; + +export type PlaceOrderStatus405 = unknown; + +export type PlaceOrderBodyJson = Order | undefined; + +export type PlaceOrderBodyXml = Order | undefined; + +export type PlaceOrderBodyFormUrlEncoded = Order | undefined; + +export type PlaceOrderBody = (PlaceOrderBodyJson | PlaceOrderBodyXml | PlaceOrderBodyFormUrlEncoded); + +export type PlaceOrderOptions = { + body: PlaceOrderBody; + path?: never; + query?: never; + headers?: never; +}; + +export type PlaceOrderResponses = { + "200": PlaceOrderStatus200; + "405": PlaceOrderStatus405; +}; + +/** + * @description Union of all possible responses +*/ +export type PlaceOrderResponse = (PlaceOrderStatus200 | PlaceOrderStatus405); diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Tag.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Tag.ts new file mode 100644 index 000000000..7878ce4f8 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/Tag.ts @@ -0,0 +1,14 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +export type Tag = { + /** + * @description + * Format: `int64` + * @type integer | undefined + */ + id?: bigint; + name?: string; +}; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/UploadFile.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/UploadFile.ts new file mode 100644 index 000000000..d5832e635 --- /dev/null +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/returnTypeData/types/UploadFile.ts @@ -0,0 +1,44 @@ +/** +* Generated by Kubb (https://kubb.dev/). +* Do not edit manually. +*/ + +import type { ApiResponse } from './ApiResponse' + +export type UploadFilePath = { + /** + * @description ID of pet to update + * + * Format: `int64` + * @type integer + */ + petId: bigint; +}; + +export type UploadFileQuery = { + /** + * @description Additional Metadata + * @type string | undefined + */ + additionalMetadata?: string; +}; + +export type UploadFileStatus200 = ApiResponse; + +export type UploadFileBody = Blob | undefined; + +export type UploadFileOptions = { + body: UploadFileBody; + path: UploadFilePath; + query?: UploadFileQuery; + headers?: never; +}; + +export type UploadFileResponses = { + "200": UploadFileStatus200; +}; + +/** + * @description Union of all possible responses +*/ +export type UploadFileResponse = UploadFileStatus200; diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/suspense/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/suspense/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/suspense/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/suspense/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginReactQuery/withClientPlugin/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginReactQuery/withClientPlugin/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginReactQuery/withClientPlugin/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginReactQuery/withClientPlugin/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/excludeByOperationId/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/excludeByOperationId/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/excludeByOperationId/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/excludeByOperationId/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/groupByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/groupByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/groupByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/groupByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/includeByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/includeByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/includeByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/includeByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/mutationDisabled/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/mutationDisabled/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/mutationDisabled/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/mutationDisabled/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/paramsCasing/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/paramsCasing/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/paramsCasing/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/paramsCasing/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/parserZod/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/parserZod/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/parserZod/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/parserZod/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/petStore/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/petStore/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/petStore/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/petStore/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/queryDisabled/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/queryDisabled/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/queryDisabled/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/queryDisabled/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/queryMethods/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/queryMethods/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/queryMethods/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/queryMethods/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginSwr/withClientPlugin/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginSwr/withClientPlugin/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginSwr/withClientPlugin/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginSwr/withClientPlugin/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/excludeByOperationId/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/excludeByOperationId/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/excludeByOperationId/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/excludeByOperationId/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/groupByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/groupByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/groupByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/groupByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/includeByTag/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/includeByTag/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/includeByTag/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/includeByTag/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/infinite/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/infinite/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/infinite/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/infinite/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/mutationDisabled/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/mutationDisabled/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/mutationDisabled/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/mutationDisabled/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/paramsCasing/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/paramsCasing/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/paramsCasing/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/paramsCasing/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/parserZod/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/parserZod/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/parserZod/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/parserZod/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/petStore/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/petStore/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/petStore/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/petStore/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/__snapshots__/pluginVueQuery/withClientPlugin/.kubb/client.ts b/tests/3.0.x/__snapshots__/pluginVueQuery/withClientPlugin/.kubb/client.ts index d928c18f3..e022ad571 100644 --- a/tests/3.0.x/__snapshots__/pluginVueQuery/withClientPlugin/.kubb/client.ts +++ b/tests/3.0.x/__snapshots__/pluginVueQuery/withClientPlugin/.kubb/client.ts @@ -84,6 +84,30 @@ export type RequestResult +/** + * The shape a generated operation returns when `returnType: 'data'` is set: the bare success body + * once `throwOnError` (on by default) narrows away the error branch, falling back to the full + * `RequestResult` when a call sets `throwOnError: false` and still needs `error` to discriminate a + * failed response. + */ +export type UnwrappedResult< + TResponses, + ThrowOnError extends boolean = true, + TRequest = AxiosRequestConfig, + TResponse = AxiosResponse, +> = ThrowOnError extends true ? RequestResult['data'] : RequestResult + +/** + * Narrows a resolved call down to its success body once `throwOnError` (on by default) rules out + * the error branch, the same default the runtime itself applies. Falls back to the full result for + * a call that sets `throwOnError: false`, since that path still needs `error` to discriminate a + * failed response. Backs `returnType: 'data'`, mirroring how `toEventStream` centralizes the + * post-processing for `text/event-stream` operations. + */ +export function unwrapResult(promise: Promise, throwOnError: boolean | undefined): Promise { + return promise.then((result) => ((throwOnError ?? true) ? result.data : result)) +} + /** * The data-shaped keys of the grouped options object, which `Options` re-adds typed per operation. */ diff --git a/tests/3.0.x/pluginReactQuery.test.ts b/tests/3.0.x/pluginReactQuery.test.ts index 345138b8a..660908c06 100644 --- a/tests/3.0.x/pluginReactQuery.test.ts +++ b/tests/3.0.x/pluginReactQuery.test.ts @@ -269,6 +269,27 @@ const configs: Array<{ name: string; config: BuildConfig }> = [ ], }, }, + + // ─── with the client plugin's returnType ──────────────────────────────── + { + name: 'returnTypeData', + config: { + root: __dirname, + input: '../../schemas/3.0.x/petStore.yaml', + output: { path: './gen', barrel: false }, + adapter: adapterOas({ validate: false, enums: 'root' }), + parsers: [parserTs()], + storage: fsStorage(), + plugins: [ + pluginTs({ output: { path: './types', barrel: false, mode: 'directory' } }), + pluginAxios({ output: { path: './clients', barrel: false, mode: 'directory' }, returnType: 'data' }), + pluginReactQuery({ + hooks: true, + output: { path: './hooks', barrel: false, mode: 'directory' }, + }), + ], + }, + }, ] describe(`plugin-react-query options ${version}`, () => {