diff --git a/.changeset/pink-seahorses-cheer.md b/.changeset/pink-seahorses-cheer.md new file mode 100644 index 000000000..ab7cf5c3d --- /dev/null +++ b/.changeset/pink-seahorses-cheer.md @@ -0,0 +1,5 @@ +--- +'@hey-api/openapi-ts': minor +--- + +Add SDK option to disable the auto-creation of the client diff --git a/packages/openapi-ts/src/compiler/index.ts b/packages/openapi-ts/src/compiler/index.ts index 56a85cdb9..0da263828 100644 --- a/packages/openapi-ts/src/compiler/index.ts +++ b/packages/openapi-ts/src/compiler/index.ts @@ -50,6 +50,7 @@ export const compiler = { propertyAccessExpression: types.createPropertyAccessExpression, propertyAccessExpressions: transform.createPropertyAccessExpressions, propertyAssignment: types.createPropertyAssignment, + propertyDeclaration: types.createPropertyDeclaration, regularExpressionLiteral: types.createRegularExpressionLiteral, returnFunctionCall: _return.createReturnFunctionCall, returnStatement: _return.createReturnStatement, diff --git a/packages/openapi-ts/src/compiler/types.ts b/packages/openapi-ts/src/compiler/types.ts index 872ccdafd..c9ebaa088 100644 --- a/packages/openapi-ts/src/compiler/types.ts +++ b/packages/openapi-ts/src/compiler/types.ts @@ -898,6 +898,42 @@ export const createPropertyAssignment = ({ name: string | ts.PropertyName; }) => ts.factory.createPropertyAssignment(name, initializer); +export const createPropertyDeclaration = ({ + accessLevel, + comment, + initializer, + isReadonly, + name, + type, +}: { + accessLevel?: AccessLevel; + comment?: Comments; + initializer?: ts.Expression; + isReadonly?: boolean; + name: string; + type: ts.TypeNode; +}) => { + const modifiers = toAccessLevelModifiers(accessLevel); + if (isReadonly) { + modifiers.push(ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword)); + } + + const node = ts.factory.createPropertyDeclaration( + modifiers, + createIdentifier({ text: name }), + undefined, + type, + initializer, + ); + + addLeadingComments({ + comments: comment, + node, + }); + + return node; +}; + export const createRegularExpressionLiteral = ({ flags = [], text, diff --git a/packages/openapi-ts/src/generate/client.ts b/packages/openapi-ts/src/generate/client.ts index 6b2f95ae3..6bb0ef727 100644 --- a/packages/openapi-ts/src/generate/client.ts +++ b/packages/openapi-ts/src/generate/client.ts @@ -28,6 +28,10 @@ export const clientModulePath = ({ }; export const clientApi = { + Client: { + asType: true, + name: 'Client', + }, Options: { asType: true, name: 'Options', diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts index c691627f5..bbe629174 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts @@ -30,6 +30,7 @@ export const defaultConfig: Plugin.Config = { }, asClass: false, auth: true, + autoCreateClient: true, exportFromIndex: true, name: '@hey-api/sdk', operationId: true, diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts index 48cd678b8..9d3350e62 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts @@ -28,9 +28,11 @@ import { serviceFunctionIdentifier } from './plugin-legacy'; import type { Config } from './types'; export const operationOptionsType = ({ + clientOption, identifierData, throwOnError, }: { + clientOption?: 'required' | 'omitted'; context: IR.Context; identifierData?: ReturnType; // TODO: refactor this so we don't need to import error type unless it's used here @@ -39,13 +41,24 @@ export const operationOptionsType = ({ }) => { const optionsName = clientApi.Options.name; + let optionsType = identifierData + ? `${optionsName}<${identifierData.name}>` + : optionsName; + // TODO: refactor this to be more generic, works for now if (throwOnError) { - return `${optionsName}<${identifierData?.name || 'unknown'}, ${throwOnError}>`; + optionsType = `${optionsName}<${identifierData?.name || 'unknown'}, ${throwOnError}>`; } - return identifierData - ? `${optionsName}<${identifierData.name}>` - : optionsName; + + if (clientOption === 'required') { + optionsType += ` & { client: Client }`; + } + + if (clientOption === 'omitted') { + optionsType = `Omit<${optionsType}, 'client'>`; + } + + return optionsType; }; const sdkId = 'sdk'; @@ -377,6 +390,12 @@ const operationStatements = ({ value: operation.path, }); + let clientCall = '(options?.client ?? client)'; + + if (!plugin.autoCreateClient) { + clientCall = plugin.asClass ? 'this._client' : 'options.client'; + } + return [ compiler.returnFunctionCall({ args: [ @@ -385,7 +404,7 @@ const operationStatements = ({ obj: requestOptions, }), ], - name: `(options?.client ?? client).${operation.method}`, + name: `${clientCall}.${operation.method}`, types: [ identifierResponse.name || 'unknown', identifierError.name || 'unknown', @@ -417,7 +436,7 @@ const generateClassSdk = ({ operation.summary && escapeComment(operation.summary), operation.description && escapeComment(operation.description), ], - isStatic: true, + isStatic: !!plugin.autoCreateClient, // if client is required, methods are not static name: serviceFunctionIdentifier({ config: context.config, handleIllegal: false, @@ -429,6 +448,7 @@ const generateClassSdk = ({ isRequired: hasOperationDataRequired(operation), name: 'options', type: operationOptionsType({ + clientOption: !plugin.autoCreateClient ? 'omitted' : undefined, context, identifierData, // identifierError, @@ -466,9 +486,45 @@ const generateClassSdk = ({ context.subscribe('after', () => { for (const [name, nodes] of sdks) { + const extraMembers: ts.ClassElement[] = []; + + // Add client property and constructor if autoCreateClient is false + if (!plugin.autoCreateClient) { + const clientType = 'Client'; + + const clientProperty = compiler.propertyDeclaration({ + accessLevel: 'private', + comment: ['Client Instance'], + name: '_client', + type: compiler.typeReferenceNode({ typeName: clientType }), + }); + + const constructor = compiler.constructorDeclaration({ + comment: ['@param client - Client Instance'], + parameters: [ + { + isRequired: true, + name: 'client', + type: clientType, + }, + ], + statements: [ + compiler.expressionToStatement({ + expression: compiler.binaryExpression({ + left: compiler.identifier({ text: 'this._client ' }), + operator: '=', + right: compiler.identifier({ text: 'client' }), + }), + }), + ], + }); + + extraMembers.push(clientProperty, constructor); + } + const node = compiler.classDeclaration({ decorator: undefined, - members: nodes, + members: [...extraMembers, ...nodes], name: transformServiceName({ config: context.config, name, @@ -503,9 +559,11 @@ const generateFlatSdk = ({ expression: compiler.arrowFunction({ parameters: [ { - isRequired: hasOperationDataRequired(operation), + isRequired: + hasOperationDataRequired(operation) || !plugin.autoCreateClient, name: 'options', type: operationOptionsType({ + clientOption: !plugin.autoCreateClient ? 'required' : undefined, context, identifierData, // identifierError, @@ -550,6 +608,7 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { id: sdkId, path: plugin.output, }); + const sdkOutput = file.nameWithoutExtension(); // import required packages and core files @@ -557,46 +616,57 @@ export const handler: Plugin.Handler = ({ context, plugin }) => { config: context.config, sourceOutput: sdkOutput, }); - file.import({ - module: clientModule, - name: 'createClient', - }); - file.import({ - module: clientModule, - name: 'createConfig', - }); + + if (plugin.autoCreateClient) { + file.import({ + module: clientModule, + name: 'createClient', + }); + file.import({ + module: clientModule, + name: 'createConfig', + }); + + // define client first + const statement = compiler.constVariable({ + exportConst: true, + expression: compiler.callExpression({ + functionName: 'createClient', + parameters: [ + compiler.callExpression({ + functionName: 'createConfig', + parameters: [ + plugin.throwOnError + ? compiler.objectExpression({ + obj: [ + { + key: 'throwOnError', + value: plugin.throwOnError, + }, + ], + }) + : undefined, + ], + }), + ], + }), + name: 'client', + }); + + file.add(statement); + } else { + // Bring in the client type + file.import({ + ...clientApi.Client, + module: clientModule, + }); + } + file.import({ ...clientApi.Options, module: clientModule, }); - // define client first - const statement = compiler.constVariable({ - exportConst: true, - expression: compiler.callExpression({ - functionName: 'createClient', - parameters: [ - compiler.callExpression({ - functionName: 'createConfig', - parameters: [ - plugin.throwOnError - ? compiler.objectExpression({ - obj: [ - { - key: 'throwOnError', - value: plugin.throwOnError, - }, - ], - }) - : undefined, - ], - }), - ], - }), - name: 'client', - }); - file.add(statement); - if (plugin.asClass) { generateClassSdk({ context, plugin }); } else { diff --git a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts index 84a8ccb8b..75ddf3d20 100644 --- a/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts +++ b/packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts @@ -25,6 +25,18 @@ export interface Config extends Plugin.Name<'@hey-api/sdk'> { * @default true */ auth?: boolean; + /** + * **This feature works only with the [experimental parser](https://heyapi.dev/openapi-ts/configuration#parser)** + * + * Should the generated SDK do a createClient call automatically? If this is + * set to false, the generated SDK will expect a client to be passed in during: + * + * - instantiation if asClass is set to true (and the client will be passed to the constructor. All methods will not be static either) + * - each method call if asClass is set to false + * + * @default true + */ + autoCreateClient?: boolean; /** * **This feature works only with the legacy parser** *