From 44fa13163b39848e1c4b31adae93671b1ec5260b Mon Sep 17 00:00:00 2001 From: Lubos Date: Thu, 13 Feb 2025 07:14:54 +0000 Subject: [PATCH] fix: allow providing custom query key to TanStack Query --- .../openapi-ts-next/src/client/sdk.gen.ts | 5 + packages/openapi-ts/src/generate/files.ts | 15 +- .../angular-query-experimental/types.d.ts | 11 +- .../query-core/infiniteQueryOptions.ts | 25 +- .../plugins/@tanstack/query-core/queryKey.ts | 370 ++++++++++-------- .../plugins/@tanstack/query-core/types.d.ts | 19 + .../plugins/@tanstack/react-query/types.d.ts | 12 +- .../plugins/@tanstack/solid-query/types.d.ts | 12 +- .../plugins/@tanstack/svelte-query/types.d.ts | 12 +- .../plugins/@tanstack/vue-query/types.d.ts | 12 +- packages/openapi-ts/test/hey-api.ts | 70 ++++ packages/openapi-ts/test/openapi-ts.config.ts | 1 + 12 files changed, 339 insertions(+), 225 deletions(-) create mode 100644 packages/openapi-ts/test/hey-api.ts diff --git a/examples/openapi-ts-next/src/client/sdk.gen.ts b/examples/openapi-ts-next/src/client/sdk.gen.ts index 3a2de1490..28a52d180 100644 --- a/examples/openapi-ts-next/src/client/sdk.gen.ts +++ b/examples/openapi-ts-next/src/client/sdk.gen.ts @@ -52,6 +52,11 @@ export type Options< * custom client. */ client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; }; /** diff --git a/packages/openapi-ts/src/generate/files.ts b/packages/openapi-ts/src/generate/files.ts index ebdfbfdf0..74b1680fe 100644 --- a/packages/openapi-ts/src/generate/files.ts +++ b/packages/openapi-ts/src/generate/files.ts @@ -202,15 +202,22 @@ export class TypeScriptFile { }): string { let filePath = ''; - if (!id.startsWith('.')) { + // relative file path + if (id.startsWith('.')) { + let configFileParts: Array = []; + // if providing a custom configuration file, relative paths must resolve + // relative to the configuration file. + if (context.config.configFile) { + const cfgParts = context.config.configFile.split('/'); + configFileParts = cfgParts.slice(0, cfgParts.length - 1); + } + filePath = path.resolve(process.cwd(), ...configFileParts, id); + } else { const file = context.file({ id }); if (!file) { throw new Error(`File with id ${id} does not exist`); } - filePath = file._path; - } else { - filePath = path.resolve(process.cwd(), id); } const thisPathParts = this._path.split(path.sep); diff --git a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts index ab31bf12b..fa0c52c50 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/angular-query-experimental/types.d.ts @@ -1,14 +1,9 @@ import type { Plugin } from '../../types'; +import type { TanStackQuery } from '../query-core/types'; export interface Config - extends Plugin.Name<'@tanstack/angular-query-experimental'> { - /** - * Should the exports from the generated files be re-exported in the index - * barrel file? - * - * @default false - */ - exportFromIndex?: boolean; + extends Plugin.Name<'@tanstack/angular-query-experimental'>, + TanStackQuery.Config { /** * Generate {@link https://tanstack.com/query/v5/docs/framework/angular/reference/infiniteQueryOptions `infiniteQueryOptions()`} helpers? These will be generated from GET and POST requests where a pagination parameter is detected. * diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts index 4cd743403..00fc5617f 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/infiniteQueryOptions.ts @@ -32,6 +32,17 @@ const createInfiniteParamsFunction = ({ }) => { const file = context.file({ id: plugin.name })!; + if (plugin.runtimeConfigPath) { + file.import({ + module: file.relativePathToFile({ + context, + id: plugin.runtimeConfigPath, + }), + name: createInfiniteParamsFn, + }); + return; + } + const fn = compiler.constVariable({ expression: compiler.arrowFunction({ multiLine: true, @@ -185,15 +196,15 @@ const createInfiniteParamsFunction = ({ }), }), compiler.returnVariable({ - expression: ts.factory.createAsExpression( - ts.factory.createAsExpression( - compiler.identifier({ text: 'params' }), - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), - ), - ts.factory.createTypeQueryNode( + expression: compiler.asExpression({ + expression: compiler.asExpression({ + expression: compiler.identifier({ text: 'params' }), + type: compiler.keywordTypeNode({ keyword: 'unknown' }), + }), + type: ts.factory.createTypeQueryNode( compiler.identifier({ text: 'page' }), ), - ), + }), }), ], types: [ diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts index 5673798fe..29a4c6bf7 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/queryKey.ts @@ -11,6 +11,9 @@ const createQueryKeyFn = 'createQueryKey'; export const queryKeyName = 'QueryKey'; const TOptionsType = 'TOptions'; +const infiniteIdentifier = compiler.identifier({ text: 'infinite' }); +const optionsIdentifier = compiler.identifier({ text: 'options' }); + export const createQueryKeyFunction = ({ context, plugin, @@ -20,191 +23,202 @@ export const createQueryKeyFunction = ({ }) => { const file = context.file({ id: plugin.name })!; - const returnType = compiler.indexedAccessTypeNode({ - indexType: compiler.literalTypeNode({ - literal: compiler.ots.number(0), - }), - objectType: compiler.typeReferenceNode({ - typeArguments: [compiler.typeReferenceNode({ typeName: TOptionsType })], - typeName: queryKeyName, - }), - }); - - const infiniteIdentifier = compiler.identifier({ text: 'infinite' }); - const identifierCreateQueryKey = file.identifier({ $ref: `#/ir/${createQueryKeyFn}`, create: true, namespace: 'value', }); - const fn = compiler.constVariable({ - expression: compiler.arrowFunction({ - multiLine: true, - parameters: [ - { - name: 'id', - type: compiler.typeReferenceNode({ typeName: 'string' }), - }, - { - isRequired: false, - name: 'options', - type: compiler.typeReferenceNode({ typeName: TOptionsType }), - }, - { - isRequired: false, - name: 'infinite', - type: compiler.typeReferenceNode({ typeName: 'boolean' }), - }, - ], - returnType, - statements: [ - compiler.constVariable({ - assertion: returnType, - expression: compiler.objectExpression({ - multiLine: false, - obj: [ - { - key: '_id', - value: compiler.identifier({ text: 'id' }), - }, - { - key: getClientBaseUrlKey(context.config), - value: compiler.identifier({ - text: `(options?.client ?? _heyApiClient).getConfig().${getClientBaseUrlKey(context.config)}`, - }), - }, - ], - }), - name: 'params', - typeName: returnType, + if (identifierCreateQueryKey.name) { + if (plugin.runtimeConfigPath) { + file.import({ + module: file.relativePathToFile({ + context, + id: plugin.runtimeConfigPath, }), - compiler.ifStatement({ - expression: infiniteIdentifier, - thenStatement: compiler.block({ - statements: [ - compiler.expressionToStatement({ - expression: compiler.binaryExpression({ - left: compiler.propertyAccessExpression({ - expression: 'params', - name: '_infinite', + name: identifierCreateQueryKey.name, + }); + return; + } + + const returnType = compiler.indexedAccessTypeNode({ + indexType: compiler.literalTypeNode({ + literal: compiler.ots.number(0), + }), + objectType: compiler.typeReferenceNode({ + typeArguments: [compiler.typeReferenceNode({ typeName: TOptionsType })], + typeName: queryKeyName, + }), + }); + + const fn = compiler.constVariable({ + expression: compiler.arrowFunction({ + multiLine: true, + parameters: [ + { + name: 'id', + type: compiler.typeReferenceNode({ typeName: 'string' }), + }, + { + isRequired: false, + name: 'options', + type: compiler.typeReferenceNode({ typeName: TOptionsType }), + }, + { + isRequired: false, + name: 'infinite', + type: compiler.typeReferenceNode({ typeName: 'boolean' }), + }, + ], + returnType, + statements: [ + compiler.constVariable({ + assertion: returnType, + expression: compiler.objectExpression({ + multiLine: false, + obj: [ + { + key: '_id', + value: compiler.identifier({ text: 'id' }), + }, + { + key: getClientBaseUrlKey(context.config), + value: compiler.identifier({ + text: `(options?.client ?? _heyApiClient).getConfig().${getClientBaseUrlKey(context.config)}`, }), - right: infiniteIdentifier, - }), - }), - ], - }), - }), - compiler.ifStatement({ - expression: compiler.propertyAccessExpression({ - expression: compiler.identifier({ text: 'options' }), - isOptional: true, - name: compiler.identifier({ text: 'body' }), + }, + ], + }), + name: 'params', + typeName: returnType, }), - thenStatement: compiler.block({ - statements: [ - compiler.expressionToStatement({ - expression: compiler.binaryExpression({ - left: compiler.propertyAccessExpression({ - expression: 'params', - name: 'body', - }), - right: compiler.propertyAccessExpression({ - expression: 'options', - name: 'body', + compiler.ifStatement({ + expression: infiniteIdentifier, + thenStatement: compiler.block({ + statements: [ + compiler.expressionToStatement({ + expression: compiler.binaryExpression({ + left: compiler.propertyAccessExpression({ + expression: 'params', + name: '_infinite', + }), + right: infiniteIdentifier, }), }), - }), - ], - }), - }), - compiler.ifStatement({ - expression: compiler.propertyAccessExpression({ - expression: compiler.identifier({ text: 'options' }), - isOptional: true, - name: compiler.identifier({ text: 'headers' }), + ], + }), }), - thenStatement: compiler.block({ - statements: [ - compiler.expressionToStatement({ - expression: compiler.binaryExpression({ - left: compiler.propertyAccessExpression({ - expression: 'params', - name: 'headers', - }), - right: compiler.propertyAccessExpression({ - expression: 'options', - name: 'headers', + compiler.ifStatement({ + expression: compiler.propertyAccessExpression({ + expression: optionsIdentifier, + isOptional: true, + name: compiler.identifier({ text: 'body' }), + }), + thenStatement: compiler.block({ + statements: [ + compiler.expressionToStatement({ + expression: compiler.binaryExpression({ + left: compiler.propertyAccessExpression({ + expression: 'params', + name: 'body', + }), + right: compiler.propertyAccessExpression({ + expression: 'options', + name: 'body', + }), }), }), - }), - ], - }), - }), - compiler.ifStatement({ - expression: compiler.propertyAccessExpression({ - expression: compiler.identifier({ text: 'options' }), - isOptional: true, - name: compiler.identifier({ text: 'path' }), + ], + }), }), - thenStatement: compiler.block({ - statements: [ - compiler.expressionToStatement({ - expression: compiler.binaryExpression({ - left: compiler.propertyAccessExpression({ - expression: 'params', - name: 'path', - }), - right: compiler.propertyAccessExpression({ - expression: 'options', - name: 'path', + compiler.ifStatement({ + expression: compiler.propertyAccessExpression({ + expression: optionsIdentifier, + isOptional: true, + name: compiler.identifier({ text: 'headers' }), + }), + thenStatement: compiler.block({ + statements: [ + compiler.expressionToStatement({ + expression: compiler.binaryExpression({ + left: compiler.propertyAccessExpression({ + expression: 'params', + name: 'headers', + }), + right: compiler.propertyAccessExpression({ + expression: 'options', + name: 'headers', + }), }), }), - }), - ], - }), - }), - compiler.ifStatement({ - expression: compiler.propertyAccessExpression({ - expression: compiler.identifier({ text: 'options' }), - isOptional: true, - name: compiler.identifier({ text: 'query' }), + ], + }), }), - thenStatement: compiler.block({ - statements: [ - compiler.expressionToStatement({ - expression: compiler.binaryExpression({ - left: compiler.propertyAccessExpression({ - expression: 'params', - name: 'query', - }), - right: compiler.propertyAccessExpression({ - expression: 'options', - name: 'query', + compiler.ifStatement({ + expression: compiler.propertyAccessExpression({ + expression: optionsIdentifier, + isOptional: true, + name: compiler.identifier({ text: 'path' }), + }), + thenStatement: compiler.block({ + statements: [ + compiler.expressionToStatement({ + expression: compiler.binaryExpression({ + left: compiler.propertyAccessExpression({ + expression: 'params', + name: 'path', + }), + right: compiler.propertyAccessExpression({ + expression: 'options', + name: 'path', + }), }), }), - }), - ], + ], + }), }), - }), - compiler.returnVariable({ - expression: 'params', - }), - ], - types: [ - { - extends: compiler.typeReferenceNode({ - typeName: compiler.identifier({ - text: clientApi.Options.name, + compiler.ifStatement({ + expression: compiler.propertyAccessExpression({ + expression: optionsIdentifier, + isOptional: true, + name: compiler.identifier({ text: 'query' }), + }), + thenStatement: compiler.block({ + statements: [ + compiler.expressionToStatement({ + expression: compiler.binaryExpression({ + left: compiler.propertyAccessExpression({ + expression: 'params', + name: 'query', + }), + right: compiler.propertyAccessExpression({ + expression: 'options', + name: 'query', + }), + }), + }), + ], }), }), - name: TOptionsType, - }, - ], - }), - name: identifierCreateQueryKey.name || '', - }); - file.add(fn); + compiler.returnVariable({ + expression: 'params', + }), + ], + types: [ + { + extends: compiler.typeReferenceNode({ + typeName: compiler.identifier({ + text: clientApi.Options.name, + }), + }), + name: TOptionsType, + }, + ], + }), + name: identifierCreateQueryKey.name, + }); + file.add(fn); + } }; const createQueryKeyLiteral = ({ @@ -223,20 +237,15 @@ const createQueryKeyLiteral = ({ $ref: `#/ir/${createQueryKeyFn}`, namespace: 'value', }); - const queryKeyLiteral = compiler.arrayLiteralExpression({ - elements: [ - compiler.callExpression({ - functionName: identifierCreateQueryKey.name || '', - parameters: [ - compiler.ots.string(id), - 'options', - isInfinite ? compiler.ots.boolean(true) : undefined, - ], - }), + const createQueryKeyCallExpression = compiler.callExpression({ + functionName: identifierCreateQueryKey.name || '', + parameters: [ + compiler.ots.string(id), + 'options', + isInfinite ? compiler.ots.boolean(true) : undefined, ], - multiLine: false, }); - return queryKeyLiteral; + return createQueryKeyCallExpression; }; export const createQueryKeyType = ({ @@ -248,6 +257,18 @@ export const createQueryKeyType = ({ }) => { const file = context.file({ id: plugin.name })!; + if (plugin.runtimeConfigPath) { + file.import({ + asType: true, + module: file.relativePathToFile({ + context, + id: plugin.runtimeConfigPath, + }), + name: queryKeyName, + }); + return; + } + const properties: Property[] = [ { name: '_id', @@ -265,6 +286,7 @@ export const createQueryKeyType = ({ ]; const queryKeyType = compiler.typeAliasDeclaration({ + exportType: true, name: queryKeyName, type: compiler.typeTupleNode({ types: [ diff --git a/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts index a4cfffc4f..9ee819f05 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/query-core/types.d.ts @@ -31,3 +31,22 @@ export interface PluginState { hasUsedQueryFn: boolean; typeInfiniteData: ImportExportItem; } + +/** + * Public TanStack Query API. + */ +export namespace TanStackQuery { + export type Config = { + /** + * Should the exports from the generated files be re-exported in the index + * barrel file? + * + * @default false + */ + exportFromIndex?: boolean; + /** + * TODO + */ + runtimeConfigPath?: string; + }; +} diff --git a/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts index f582b7fb0..5981a8935 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/react-query/types.d.ts @@ -1,13 +1,9 @@ import type { Plugin } from '../../types'; +import type { TanStackQuery } from '../query-core/types'; -export interface Config extends Plugin.Name<'@tanstack/react-query'> { - /** - * Should the exports from the generated files be re-exported in the index - * barrel file? - * - * @default false - */ - exportFromIndex?: boolean; +export interface Config + extends Plugin.Name<'@tanstack/react-query'>, + TanStackQuery.Config { /** * Generate {@link https://tanstack.com/query/v5/docs/framework/react/reference/infiniteQueryOptions `infiniteQueryOptions()`} helpers? These will be generated from GET and POST requests where a pagination parameter is detected. * diff --git a/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts index c1f96217d..5f6876ab8 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/solid-query/types.d.ts @@ -1,13 +1,9 @@ import type { Plugin } from '../../types'; +import type { TanStackQuery } from '../query-core/types'; -export interface Config extends Plugin.Name<'@tanstack/solid-query'> { - /** - * Should the exports from the generated files be re-exported in the index - * barrel file? - * - * @default false - */ - exportFromIndex?: boolean; +export interface Config + extends Plugin.Name<'@tanstack/solid-query'>, + TanStackQuery.Config { /** * Generate `createInfiniteQuery()` helpers? These will be generated from GET and POST requests where a pagination parameter is detected. * diff --git a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts index 364b37873..0deca0faa 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/svelte-query/types.d.ts @@ -1,13 +1,9 @@ import type { Plugin } from '../../types'; +import type { TanStackQuery } from '../query-core/types'; -export interface Config extends Plugin.Name<'@tanstack/svelte-query'> { - /** - * Should the exports from the generated files be re-exported in the index - * barrel file? - * - * @default false - */ - exportFromIndex?: boolean; +export interface Config + extends Plugin.Name<'@tanstack/svelte-query'>, + TanStackQuery.Config { /** * Generate `createInfiniteQuery()` helpers? These will be generated from GET and POST requests where a pagination parameter is detected. * diff --git a/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts b/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts index 6e4a1c062..81f556bb8 100644 --- a/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts +++ b/packages/openapi-ts/src/plugins/@tanstack/vue-query/types.d.ts @@ -1,13 +1,9 @@ import type { Plugin } from '../../types'; +import type { TanStackQuery } from '../query-core/types'; -export interface Config extends Plugin.Name<'@tanstack/vue-query'> { - /** - * Should the exports from the generated files be re-exported in the index - * barrel file? - * - * @default false - */ - exportFromIndex?: boolean; +export interface Config + extends Plugin.Name<'@tanstack/vue-query'>, + TanStackQuery.Config { /** * Generate {@link https://tanstack.com/query/v5/docs/framework/vue/reference/infiniteQueryOptions `infiniteQueryOptions()`} helpers? These will be generated from GET and POST requests where a pagination parameter is detected. * diff --git a/packages/openapi-ts/test/hey-api.ts b/packages/openapi-ts/test/hey-api.ts new file mode 100644 index 000000000..2af13839c --- /dev/null +++ b/packages/openapi-ts/test/hey-api.ts @@ -0,0 +1,70 @@ +import { client as _heyApiClient } from './generated/sample/client.gen'; +import type { Options } from './generated/sample/sdk.gen'; + +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + }, +]; + +export const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: (options?.client ?? _heyApiClient).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 createInfiniteParams = < + K extends Pick[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; +}; diff --git a/packages/openapi-ts/test/openapi-ts.config.ts b/packages/openapi-ts/test/openapi-ts.config.ts index 096e06f39..9dd08ba3f 100644 --- a/packages/openapi-ts/test/openapi-ts.config.ts +++ b/packages/openapi-ts/test/openapi-ts.config.ts @@ -78,6 +78,7 @@ export default defineConfig({ // @ts-ignore { name: '@tanstack/react-query', + runtimeConfigPath: './hey-api.ts', }, // @ts-ignore {