Skip to content

Commit

Permalink
Merged in feature/MI-3_provide-sdk-as-a-class (pull request #466)
Browse files Browse the repository at this point in the history
MI-3: Use Object.assign to provide BigCommerce SDK as a class instead of a factory

Approved-by: Brett Cutting
  • Loading branch information
tvhees committed Oct 24, 2024
2 parents 5b0c1a7 + 8de2302 commit 8886a07
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 52 deletions.
4 changes: 2 additions & 2 deletions packages/modules/bigcommerce/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ export class SomeService {
Fetching the sdk in a resolver:

```typescript
import { BigCommerceSdk } from '../../providers';
import { BigCommerceGraphQlClient } from '../../providers/clients';
import { Sdk } from '@aligent/bigcommerce-operations';

export const someResolver: QueryResolvers['a-resolver-type'] = {
resolve: async (_root, _args, context, _info) => {
const sdk: Sdk = context.injector.get(BigCommerceSdk);
const sdk: Sdk = context.injector.get(BigCommerceGraphQlClient);

const response = sdk.login({
email,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { DocumentNode, Kind } from 'graphql';
import * as xray from 'aws-xray-sdk';
import axios, { AxiosRequestConfig } from 'axios';
import { print } from 'graphql';
import { ExecutionContext, Inject, Injectable, forwardRef } from 'graphql-modules';
import { ModuleConfig } from '..';
import { BigCommerceModuleConfig, retrieveCustomerImpersonationTokenFromCache } from '../';
import { getSdk } from '@aligent/bigcommerce-operations';
import { logAndThrowError } from '@aligent/utils';

/* eslint-disable-next-line @typescript-eslint/no-empty-interface, @typescript-eslint/no-unsafe-declaration-merging --
* This interface tells typescript that the class BigCommerceGraphQlClient will have all the methods
* and properties of the object returned by getSdk.
*
* Technically this is unsafe because Typescript won't check that we actually initialise those properties, but
* it's the only way I could find to get Typescript to understand the shape of the class with the SDK methods
* dynamically added to it.
*/
export interface BigCommerceGraphQlClient extends ReturnType<typeof buildBigCommerceAxiosSdk> {}

/**
* Class for making BigCommerce GraphQL requests
*
* This abstracts the axios and xray specific implementation details, and also
* automatically injects the customerImpersonationToken from the request context
*
* @example Basic use for making a GraphQL Request
* ```ts
* import { BigCommerceGraphQlClient } from '../../clients/big-commerce-graphql-client';
*
* const bigCommerce = context.injector.get(BigCommerceGraphQlClient);
* const response = await bigCommerce.getBrands(variables);
* ```
*
* @example Conditionally including the Customer Id for operations that may require it
* ```ts
* import { BigCommerceGraphQlClient } from '../../clients/big-commerce-graphql-client';
*
* const bigCommerce = context.injector.get(BigCommerceGraphQlClient);
* const bcCustomerId = getBcCustomerId(context);
* const response = await bigCommerce.checkout(variables, { headers: { ...(bcCustomerId && { 'x-bc-customer-id': bcCustomerId }) }});
* ```
*/
@Injectable({ global: true })
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
export class BigCommerceGraphQlClient {
@ExecutionContext() private context: ExecutionContext;

constructor(@Inject(forwardRef(() => ModuleConfig)) private config: BigCommerceModuleConfig) {
const sdk = buildBigCommerceAxiosSdk(config, this.context);
Object.assign(this, sdk);
}
}

/**
* This function encapsulates the details of constructing an axios-based
* SDK for use by the class exported in this file.
*
* @returns Object with fully typed request functions generated from
* graphql operations defined in this module
*/
function buildBigCommerceAxiosSdk(config: BigCommerceModuleConfig, context: ExecutionContext) {
const timeout = {
milliseconds: 10_000,
message: 'BigCommerce GraphQL request timed out',
};
// The connection timeout should be slightly longer to
// allow servers to respond with a timeout failure before
// we directly cancel a connection
const connectionTimeout = timeout.milliseconds + 50;

const client = axios.create({
baseURL: config.graphqlEndpoint,
headers: {
accept: 'application/json',
},
timeout: timeout.milliseconds,
timeoutErrorMessage: timeout.message,
});

const requester = async <R, V>(
doc: DocumentNode,
variables: V,
options?: AxiosRequestConfig
): Promise<R> => {
const operationName =
doc.definitions.find((d) => d.kind === Kind.OPERATION_DEFINITION)?.name?.value ||
'Unknown Operation';

try {
const customerImpersonationToken =
await retrieveCustomerImpersonationTokenFromCache(context);

const data = {
query: print(doc),
variables,
};

const requestOptions = {
signal: AbortSignal.timeout(connectionTimeout),
...options,
headers: {
Authorization: `Bearer ${customerImpersonationToken}`,
...options?.headers,
},
};

const response = await xray.captureAsyncFunc('BigCommerceGraphQl', async (segment) => {
// Add query annotation to axios request
segment?.addAnnotation('query', data.query);
const response = await client.post<R>('', data, requestOptions);
segment?.close();
return response;
});

return response.data;
} catch (error: unknown) {
return logAndThrowError(error, operationName);
}
};

return getSdk(requester);
}
1 change: 1 addition & 0 deletions packages/modules/bigcommerce/src/clients/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './big-commerce-graphql-client';
3 changes: 2 additions & 1 deletion packages/modules/bigcommerce/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ export * from './apis/rest';
export * from './utils';

// Export Globally accessible DI Tokens so other modules can use them
export { ModuleConfig, BigCommerceSdk } from './providers';
export { ModuleConfig } from './providers';
export { BigCommerceGraphQlClient } from './clients/';
51 changes: 2 additions & 49 deletions packages/modules/bigcommerce/src/providers/index.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,12 @@
import { InjectionToken, Provider, Scope } from 'graphql-modules';
import { BigCommerceModuleConfig } from '../index';
import { getSdk } from '@aligent/bigcommerce-operations';
import { logAndThrowError, requesterFactory } from '@aligent/utils';
import { Get, Paths } from 'type-fest';
import { BigCommerceGraphQlClient } from '../clients';

export const ModuleConfig = new InjectionToken<BigCommerceModuleConfig>(
'Configuration for the BigCommerce GraphQL Module'
);

/**
* Generates a BigCommerce GraphQl SDK based on operations
* and configuration defined in this module
*/
const sdkFactory = (config: BigCommerceModuleConfig) =>
getSdk(
requesterFactory({
name: 'bcGraphQlRequest',
graphqlEndpoint: config.graphqlEndpoint,
timeout: {
milliseconds: 10_000,
message: 'BigCommerce GraphQL request timed out',
},
onError: logAndThrowError,
})
);

/**
* Types for the injectable BigCommerce GraphQl SDK
*
* Available calls, variables, and return types are derived from
* GraphQl operations defined in this module
*/
export type BcSdk = ReturnType<typeof sdkFactory>;

/**
* Injection token for the BigCommerce GraphQl SDK
*
* Available calls, variables, and return types are derived from
* GraphQl operations defined in this module
*
* @example
* import { BigCommerceSdk } from '../../providers';
*
* const bcSdk = context.injector.get(BigCommerceSdk);
* response = await bcSdk.getBrands(variables, headers);
*/
export const BigCommerceSdk = new InjectionToken<BcSdk>(
'Sdk for making GraphQL calls to BigCommerce'
);

/**
* Utility type to assist with getting a deeply nested type by path
*
Expand Down Expand Up @@ -79,11 +37,6 @@ export const getProviders = (config: BigCommerceModuleConfig): Array<Provider> =
scope: Scope.Singleton,
global: true,
},
{
provide: BigCommerceSdk,
useFactory: sdkFactory,
deps: [ModuleConfig],
global: true,
},
BigCommerceGraphQlClient,
];
};

0 comments on commit 8886a07

Please sign in to comment.