Skip to content

Commit

Permalink
Merge pull request #1530 from hey-api/fix/pagination-ref-composition
Browse files Browse the repository at this point in the history
fix: detect pagination in composite schemas with null type
  • Loading branch information
mrlubos authored Jan 7, 2025
2 parents ec0080d + 67b7295 commit 53969d6
Show file tree
Hide file tree
Showing 11 changed files with 252 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-games-impress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': patch
---

fix: detect pagination in composite schemas with null type
16 changes: 10 additions & 6 deletions packages/openapi-ts/src/openApi/2.0.x/parser/pagination.ts
Original file line number Diff line number Diff line change
@@ -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<SchemaObject> | 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,
Expand Down Expand Up @@ -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;
}
}
Expand Down
16 changes: 10 additions & 6 deletions packages/openapi-ts/src/openApi/3.0.x/parser/pagination.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -9,6 +10,14 @@ import { type SchemaObject } from '../types/spec';
import { mediaTypeObject } from './mediaType';
import { getSchemaType } from './schema';

const isPaginationType = (
schemaType: SchemaType<SchemaObject> | 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,
Expand Down Expand Up @@ -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;
}
}
Expand Down
33 changes: 26 additions & 7 deletions packages/openapi-ts/src/openApi/3.1.x/parser/pagination.ts
Original file line number Diff line number Diff line change
@@ -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<SchemaType<SchemaObject>>,
): 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,
Expand Down Expand Up @@ -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;
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/openapi-ts/test/3.1.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TOptions extends Options> = [
Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & {
_id: string;
_infinite?: boolean;
}
];

const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions, infinite?: boolean): QueryKey<TOptions>[0] => {
const params: QueryKey<TOptions>[0] = { _id: id, baseUrl: (options?.client ?? client).getConfig().baseUrl } as QueryKey<TOptions>[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<PostFooData>) => [
createQueryKey('postFoo', options)
];

export const postFooOptions = (options: Options<PostFooData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await postFoo({
...options,
...queryKey[0],
signal,
throwOnError: true
});
return data;
},
queryKey: postFooQueryKey(options)
});
};

const createInfiniteParams = <K extends Pick<QueryKey<Options>[0], 'body' | 'headers' | 'path' | 'query'>>(queryKey: QueryKey<Options>, 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<PostFooData>): QueryKey<Options<PostFooData>> => [
createQueryKey('postFoo', options, true)
];

export const postFooInfiniteOptions = (options: Options<PostFooData>) => {
return infiniteQueryOptions<unknown, DefaultError, InfiniteData<unknown>, QueryKey<Options<PostFooData>>, number | null | Pick<QueryKey<Options<PostFooData>>[0], 'body' | 'headers' | 'path' | 'query'>>(
// @ts-ignore
{
queryFn: async ({ pageParam, queryKey, signal }) => {
// @ts-ignore
const page: Pick<QueryKey<Options<PostFooData>>[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<Options<PostFooData>>) => {
const mutationOptions: UseMutationOptions<unknown, DefaultError, Options<PostFooData>> = {
mutationFn: async (localOptions) => {
const { data } = await postFoo({
...options,
...localOptions,
throwOnError: true
});
return data;
}
};
return mutationOptions;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts
export * from './types.gen';
export * from './sdk.gen';
Original file line number Diff line number Diff line change
@@ -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 = <ThrowOnError extends boolean = false>(options: Options<PostFooData, ThrowOnError>) => {
return (options?.client ?? client).post<unknown, unknown, ThrowOnError>({
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers
},
url: '/foo'
});
};
Original file line number Diff line number Diff line change
@@ -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;
};
7 changes: 4 additions & 3 deletions packages/openapi-ts/test/openapi-ts.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -49,8 +49,9 @@ export default defineConfig({
},
// @ts-ignore
{
bigInt: true,
dates: true,
name: '@hey-api/transformers',
// name: '@hey-api/transformers',
},
// @ts-ignore
{
Expand All @@ -69,7 +70,7 @@ export default defineConfig({
},
// @ts-ignore
{
// name: '@tanstack/react-query',
name: '@tanstack/react-query',
},
// @ts-ignore
{
Expand Down
27 changes: 27 additions & 0 deletions packages/openapi-ts/test/spec/3.1.x/pagination-ref-any-of.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 53969d6

Please sign in to comment.