From 67b72959be499ff59f5f68bfdaa7e5568f5de02f Mon Sep 17 00:00:00 2001 From: Lubos Date: Tue, 7 Jan 2025 11:24:04 +0100 Subject: [PATCH] fix: detect pagination in composite schemas with null type --- .changeset/late-games-impress.md | 5 + .../src/openApi/2.0.x/parser/pagination.ts | 16 ++- .../src/openApi/3.0.x/parser/pagination.ts | 16 ++- .../src/openApi/3.1.x/parser/pagination.ts | 33 ++++- packages/openapi-ts/test/3.1.x.test.ts | 8 ++ .../@tanstack/react-query.gen.ts | 123 ++++++++++++++++++ .../3.1.x/pagination-ref-any-of/index.ts | 3 + .../3.1.x/pagination-ref-any-of/sdk.gen.ts | 17 +++ .../3.1.x/pagination-ref-any-of/types.gen.ts | 19 +++ packages/openapi-ts/test/openapi-ts.config.ts | 7 +- .../spec/3.1.x/pagination-ref-any-of.yaml | 27 ++++ 11 files changed, 252 insertions(+), 22 deletions(-) create mode 100644 .changeset/late-games-impress.md create mode 100644 packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/@tanstack/react-query.gen.ts create mode 100644 packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/index.ts create mode 100644 packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/sdk.gen.ts create mode 100644 packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/types.gen.ts create mode 100644 packages/openapi-ts/test/spec/3.1.x/pagination-ref-any-of.yaml diff --git a/.changeset/late-games-impress.md b/.changeset/late-games-impress.md new file mode 100644 index 000000000..bbba5da01 --- /dev/null +++ b/.changeset/late-games-impress.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': patch +--- + +fix: detect pagination in composite schemas with null type diff --git a/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts b/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts index 8680cdf9f..f6cec2827 100644 --- a/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts +++ b/packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts @@ -1,9 +1,18 @@ import { paginationKeywordsRegExp } from '../../../ir/pagination'; import type { IR } from '../../../ir/types'; +import type { SchemaType } from '../../shared/types/schema'; import type { ParameterObject, ReferenceObject } from '../types/spec'; import { type SchemaObject } from '../types/spec'; import { getSchemaType } from './schema'; +const isPaginationType = ( + schemaType: SchemaType | undefined, +): boolean => + schemaType === 'boolean' || + schemaType === 'integer' || + schemaType === 'number' || + schemaType === 'string'; + // We handle only simple values for now, up to 1 nested field export const paginationField = ({ context, @@ -83,12 +92,7 @@ export const paginationField = ({ const schemaType = getSchemaType({ schema: property }); // TODO: resolve deeper references - if ( - schemaType === 'boolean' || - schemaType === 'integer' || - schemaType === 'number' || - schemaType === 'string' - ) { + if (isPaginationType(schemaType)) { return name; } } diff --git a/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts b/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts index dfa3ae0fd..454b92f45 100644 --- a/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts +++ b/packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts @@ -1,5 +1,6 @@ import { paginationKeywordsRegExp } from '../../../ir/pagination'; import type { IR } from '../../../ir/types'; +import type { SchemaType } from '../../shared/types/schema'; import type { ParameterObject, ReferenceObject, @@ -9,6 +10,14 @@ import { type SchemaObject } from '../types/spec'; import { mediaTypeObject } from './mediaType'; import { getSchemaType } from './schema'; +const isPaginationType = ( + schemaType: SchemaType | undefined, +): boolean => + schemaType === 'boolean' || + schemaType === 'integer' || + schemaType === 'number' || + schemaType === 'string'; + // We handle only simple values for now, up to 1 nested field export const paginationField = ({ context, @@ -72,12 +81,7 @@ export const paginationField = ({ const schemaType = getSchemaType({ schema: property }); // TODO: resolve deeper references - if ( - schemaType === 'boolean' || - schemaType === 'integer' || - schemaType === 'number' || - schemaType === 'string' - ) { + if (isPaginationType(schemaType)) { return name; } } diff --git a/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts b/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts index f76535b53..f32bc2c4f 100644 --- a/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts +++ b/packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts @@ -1,10 +1,19 @@ import { paginationKeywordsRegExp } from '../../../ir/pagination'; import type { IR } from '../../../ir/types'; +import type { SchemaType } from '../../shared/types/schema'; import type { ParameterObject, RequestBodyObject } from '../types/spec'; import { type SchemaObject } from '../types/spec'; import { mediaTypeObject } from './mediaType'; import { getSchemaTypes } from './schema'; +const isPaginationType = ( + schemaTypes: ReadonlyArray>, +): boolean => + schemaTypes.includes('boolean') || + schemaTypes.includes('integer') || + schemaTypes.includes('number') || + schemaTypes.includes('string'); + // We handle only simple values for now, up to 1 nested field export const paginationField = ({ context, @@ -65,15 +74,25 @@ export const paginationField = ({ const property = schema.properties[name]!; if (typeof property !== 'boolean') { - const schemaTypes = getSchemaTypes({ schema: property }); // TODO: resolve deeper references + const schemaTypes = getSchemaTypes({ schema: property }); + + if (!schemaTypes.length) { + const compositionSchemas = property.anyOf ?? property.oneOf; + const nonNullCompositionSchemas = (compositionSchemas ?? []).filter( + (schema) => schema.type !== 'null', + ); + if (nonNullCompositionSchemas.length === 1) { + const schemaTypes = getSchemaTypes({ + schema: nonNullCompositionSchemas[0]!, + }); + if (isPaginationType(schemaTypes)) { + return name; + } + } + } - if ( - schemaTypes.includes('boolean') || - schemaTypes.includes('integer') || - schemaTypes.includes('number') || - schemaTypes.includes('string') - ) { + if (isPaginationType(schemaTypes)) { return name; } } diff --git a/packages/openapi-ts/test/3.1.x.test.ts b/packages/openapi-ts/test/3.1.x.test.ts index b102cb1cd..cf9eadde8 100644 --- a/packages/openapi-ts/test/3.1.x.test.ts +++ b/packages/openapi-ts/test/3.1.x.test.ts @@ -430,6 +430,14 @@ describe(`OpenAPI ${version}`, () => { }), description: 'handles empty response status codes', }, + { + config: createConfig({ + input: 'pagination-ref-any-of.yaml', + output: 'pagination-ref-any-of', + plugins: ['@tanstack/react-query'], + }), + description: 'detects pagination for composite types with null', + }, { config: createConfig({ input: 'parameter-explode-false.json', diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/@tanstack/react-query.gen.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/@tanstack/react-query.gen.ts new file mode 100644 index 000000000..6fafed9a1 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/@tanstack/react-query.gen.ts @@ -0,0 +1,123 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Options } from '@hey-api/client-fetch'; +import { queryOptions, infiniteQueryOptions, type InfiniteData, type DefaultError, type UseMutationOptions } from '@tanstack/react-query'; +import type { PostFooData } from '../types.gen'; +import { postFoo, client } from '../sdk.gen'; + +type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + } +]; + +const createQueryKey = (id: string, options?: TOptions, infinite?: boolean): QueryKey[0] => { + const params: QueryKey[0] = { _id: id, baseUrl: (options?.client ?? client).getConfig().baseUrl } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return params; +}; + +export const postFooQueryKey = (options: Options) => [ + createQueryKey('postFoo', options) +]; + +export const postFooOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await postFoo({ + ...options, + ...queryKey[0], + signal, + throwOnError: true + }); + return data; + }, + queryKey: postFooQueryKey(options) + }); +}; + +const createInfiniteParams = [0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey, page: K) => { + const params = queryKey[0]; + if (page.body) { + params.body = { + ...queryKey[0].body as any, + ...page.body as any + }; + } + if (page.headers) { + params.headers = { + ...queryKey[0].headers, + ...page.headers + }; + } + if (page.path) { + params.path = { + ...queryKey[0].path as any, + ...page.path as any + }; + } + if (page.query) { + params.query = { + ...queryKey[0].query as any, + ...page.query as any + }; + } + return params as unknown as typeof page; +}; + +export const postFooInfiniteQueryKey = (options: Options): QueryKey> => [ + createQueryKey('postFoo', options, true) +]; + +export const postFooInfiniteOptions = (options: Options) => { + return infiniteQueryOptions, QueryKey>, number | null | Pick>[0], 'body' | 'headers' | 'path' | 'query'>>( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { + body: { + page: pageParam + } + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await postFoo({ + ...options, + ...params, + signal, + throwOnError: true + }); + return data; + }, + queryKey: postFooInfiniteQueryKey(options) + }); +}; + +export const postFooMutation = (options?: Partial>) => { + const mutationOptions: UseMutationOptions> = { + mutationFn: async (localOptions) => { + const { data } = await postFoo({ + ...options, + ...localOptions, + throwOnError: true + }); + return data; + } + }; + return mutationOptions; +}; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/index.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/index.ts new file mode 100644 index 000000000..e64537d21 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from './types.gen'; +export * from './sdk.gen'; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/sdk.gen.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/sdk.gen.ts new file mode 100644 index 000000000..b846bab70 --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/sdk.gen.ts @@ -0,0 +1,17 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createClient, createConfig, type Options } from '@hey-api/client-fetch'; +import type { PostFooData } from './types.gen'; + +export const client = createClient(createConfig()); + +export const postFoo = (options: Options) => { + return (options?.client ?? client).post({ + ...options, + headers: { + 'Content-Type': 'application/json', + ...options?.headers + }, + url: '/foo' + }); +}; \ No newline at end of file diff --git a/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/types.gen.ts b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/types.gen.ts new file mode 100644 index 000000000..98a7949ca --- /dev/null +++ b/packages/openapi-ts/test/__snapshots__/3.1.x/pagination-ref-any-of/types.gen.ts @@ -0,0 +1,19 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type Foo = { + page?: number | null; +}; + +export type PostFooData = { + body: Foo; + path?: never; + query?: never; + url: '/foo'; +}; + +export type PostFooResponses = { + /** + * OK + */ + 200: unknown; +}; \ No newline at end of file diff --git a/packages/openapi-ts/test/openapi-ts.config.ts b/packages/openapi-ts/test/openapi-ts.config.ts index 9146775d0..6adfe768a 100644 --- a/packages/openapi-ts/test/openapi-ts.config.ts +++ b/packages/openapi-ts/test/openapi-ts.config.ts @@ -12,7 +12,7 @@ export default defineConfig({ // exclude: '^#/components/schemas/ModelWithCircularReference$', // include: // '^(#/components/schemas/import|#/paths/api/v{api-version}/simple/options)$', - path: './packages/openapi-ts/test/spec/2.0.x/type-format.yaml', + path: './packages/openapi-ts/test/spec/3.0.x/pagination-ref-any-of.yaml', // path: './test/spec/v3-transforms.json', // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', // path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', @@ -49,8 +49,9 @@ export default defineConfig({ }, // @ts-ignore { + bigInt: true, dates: true, - name: '@hey-api/transformers', + // name: '@hey-api/transformers', }, // @ts-ignore { @@ -69,7 +70,7 @@ export default defineConfig({ }, // @ts-ignore { - // name: '@tanstack/react-query', + name: '@tanstack/react-query', }, // @ts-ignore { diff --git a/packages/openapi-ts/test/spec/3.1.x/pagination-ref-any-of.yaml b/packages/openapi-ts/test/spec/3.1.x/pagination-ref-any-of.yaml new file mode 100644 index 000000000..b168f9a5f --- /dev/null +++ b/packages/openapi-ts/test/spec/3.1.x/pagination-ref-any-of.yaml @@ -0,0 +1,27 @@ +openapi: 3.1.1 +info: + title: OpenAPI 3.1.1 pagination ref any of example + version: 1 +paths: + /foo: + post: + requestBody: + content: + 'application/json': + schema: + $ref: '#/components/schemas/Foo' + required: true + responses: + '200': + description: OK +components: + schemas: + Foo: + properties: + page: + anyOf: + - type: integer + minimum: 1.0 + - type: 'null' + default: 1 + type: object