Skip to content

Commit

Permalink
feat: Add SDK option to disable auto-creation of the client
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswiggins committed Jan 1, 2025
1 parent ec9c018 commit 73cce83
Show file tree
Hide file tree
Showing 5 changed files with 134 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/pink-seahorses-cheer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hey-api/openapi-ts': minor
---

Add SDK option to disable the auto-creation of the client
4 changes: 4 additions & 0 deletions packages/openapi-ts/src/generate/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const clientModulePath = ({
};

export const clientApi = {
Client: {
asType: true,
name: 'Client',
},
Options: {
asType: true,
name: 'Options',
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const defaultConfig: Plugin.Config<Config> = {
},
asClass: false,
auth: true,
autoCreateClient: true,
exportFromIndex: true,
name: '@hey-api/sdk',
operationId: true,
Expand Down
154 changes: 112 additions & 42 deletions packages/openapi-ts/src/plugins/@hey-api/sdk/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TypeScriptFile['identifier']>;
// TODO: refactor this so we don't need to import error type unless it's used here
Expand All @@ -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';
Expand Down Expand Up @@ -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: [
Expand All @@ -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',
Expand Down Expand Up @@ -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,
Expand All @@ -429,6 +448,7 @@ const generateClassSdk = ({
isRequired: hasOperationDataRequired(operation),
name: 'options',
type: operationOptionsType({
clientOption: !plugin.autoCreateClient ? 'omitted' : undefined,
context,
identifierData,
// identifierError,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -550,52 +608,64 @@ export const handler: Plugin.Handler<Config> = ({ context, plugin }) => {
id: sdkId,
path: plugin.output,
});

const sdkOutput = file.nameWithoutExtension();

// import required packages and core files
const clientModule = clientModulePath({
config: context.config,
sourceOutput: sdkOutput,
});
file.import({
module: clientModule,
name: 'createClient',
});
file.import({
module: clientModule,
name: 'createConfig',
});

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.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,
});
}

if (plugin.asClass) {
generateClassSdk({ context, plugin });
Expand Down
12 changes: 12 additions & 0 deletions packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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**
*
Expand Down

0 comments on commit 73cce83

Please sign in to comment.