Skip to content
This repository has been archived by the owner on Apr 11, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1039 from Shopify/ml-graphql-client-add-utils
Browse files Browse the repository at this point in the history
[GraphQL Client] Add utility factories to generate common API client functions
  • Loading branch information
melissaluu authored Nov 7, 2023
2 parents 80d7995 + b830e57 commit dfedb43
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changeset/thin-avocados-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopify/graphql-client": minor
---

Add API client utility factories for generating the `getHeaders()` and `getGQLClientParams()` functions
1 change: 1 addition & 0 deletions packages/graphql-client/src/api-client-utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { validateRetries, getErrorMessage } from "../graphql-client/utilities";

export * from "./validations";
export * from "./api-versions";
export * from "./utilities";
export type * from "./types";
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { generateGetHeaders, generateGetGQLClientParams } from "../utilities";

describe("generateGetHeaders()", () => {
const config = {
storeDomain: "https://test.shopify.io",
apiUrl: "https://test.shopify.io/api/2023-10/graphql.json",
apiVersion: "2023-10",
headers: {
"X-Shopify-Storefront-Access-Token": "public-access-token",
},
};

let getHeader: ReturnType<typeof generateGetHeaders>;

beforeEach(() => {
getHeader = generateGetHeaders(config);
});

it("returns a function ", () => {
expect(getHeader).toEqual(expect.any(Function));
});

describe("returned function", () => {
it("returns the config headers if no custom headers were passed in", () => {
expect(getHeader()).toEqual(config.headers);
});

it("returns a set of headers that includes both the provided custom headers and the config headers", () => {
const customHeaders = {
"Shopify-Storefront-Id": "shop-id",
};

expect(getHeader(customHeaders)).toEqual({
...customHeaders,
...config.headers,
});
});

it("returns a set of headers where the client config headers cannot be overwritten with the custom headers", () => {
const customHeaders = {
"Shopify-Storefront-Id": "shop-id",
"X-Shopify-Storefront-Access-Token": "",
};

const headers = getHeader(customHeaders);
expect(headers["X-Shopify-Storefront-Access-Token"]).toEqual(
config.headers["X-Shopify-Storefront-Access-Token"]
);
});
});
});

describe("generateGetGQLClientParams()", () => {
const mockHeaders = {
"X-Shopify-Storefront-Access-Token": "public-access-token",
};
const mockApiUrl = "https://test.shopify.io/api/unstable/graphql.json";
const operation = `
query products{
products(first: 1) {
nodes {
id
title
}
}
}
`;

let getHeaderMock: jest.Mock;
let getApiUrlMock: jest.Mock;
let getGQLClientParams: ReturnType<typeof generateGetGQLClientParams>;

beforeEach(() => {
getHeaderMock = jest.fn().mockReturnValue(mockHeaders);
getApiUrlMock = jest.fn().mockReturnValue(mockApiUrl);
getGQLClientParams = generateGetGQLClientParams({
getHeaders: getHeaderMock,
getApiUrl: getApiUrlMock,
});
});

afterEach(() => {
jest.resetAllMocks();
});

it("returns a function", () => {
expect(getGQLClientParams).toEqual(expect.any(Function));
});

describe("returned function", () => {
it("returns an array with only the operation string if no additional options were passed into the function", () => {
const params = getGQLClientParams(operation);

expect(params).toHaveLength(1);
expect(params[0]).toBe(operation);
});

it("returns an array with only the operation string if an empty options object was passed into the function", () => {
const params = getGQLClientParams(operation, {});

expect(params).toHaveLength(1);
expect(params[0]).toBe(operation);
});

it("returns an array with the operation string and an option with variables when variables were provided", () => {
const variables = { first: 10 };
const params = getGQLClientParams(operation, { variables });

expect(params).toHaveLength(2);

expect(params[0]).toBe(operation);
expect(params[1]).toEqual({ variables });
});

it("returns an array with the operation string and an option with headers when custom headers were provided", () => {
const customHeaders = { "Shopify-Storefront-Id": "shop-id" };
const params = getGQLClientParams(operation, { customHeaders });

expect(params).toHaveLength(2);

expect(params[0]).toBe(operation);
expect(getHeaderMock).toHaveBeenCalledWith(customHeaders);
expect(params[1]).toEqual({ headers: mockHeaders });
});

it("returns an array with the operation string and an option with url when an api version was provided", () => {
const apiVersion = "unstable";
const params = getGQLClientParams(operation, { apiVersion });

expect(params).toHaveLength(2);

expect(params[0]).toBe(operation);
expect(getApiUrlMock).toHaveBeenCalledWith(apiVersion);
expect(params[1]).toEqual({ url: mockApiUrl });
});

it("returns an array with the operation string and an option with retries when a retries value was provided", () => {
const retries = 2;
const params = getGQLClientParams(operation, { retries });

expect(params).toHaveLength(2);

expect(params[0]).toBe(operation);
expect(params[1]).toEqual({ retries });
});
});
});
16 changes: 9 additions & 7 deletions packages/graphql-client/src/api-client-utilities/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ export type ApiClientLogger<TLogContentTypes = ApiClientLogContentTypes> =
BaseLogger<TLogContentTypes>;

export interface ApiClientConfig {
readonly storeDomain: string;
readonly apiVersion: string;
readonly headers: Headers;
readonly apiUrl: string;
readonly retries?: number;
storeDomain: string;
apiVersion: string;
headers: Headers;
apiUrl: string;
retries?: number;
}

export interface ApiClientRequestOptions {
Expand All @@ -43,8 +43,10 @@ export type ApiClientRequestParams = [
options?: ApiClientRequestOptions
];

export interface ApiClient<TClientConfig extends ApiClientConfig> {
readonly config: TClientConfig;
export interface ApiClient<
TClientConfig extends ApiClientConfig = ApiClientConfig
> {
readonly config: Readonly<TClientConfig>;
getHeaders: (customHeaders?: Headers) => Headers;
getApiUrl: (apiVersion?: string) => string;
fetch: (
Expand Down
44 changes: 44 additions & 0 deletions packages/graphql-client/src/api-client-utilities/utilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { RequestParams } from "../graphql-client/types";

import { ApiClient, ApiClientConfig, ApiClientRequestOptions } from "./types";

export function generateGetHeaders(
config: ApiClientConfig
): ApiClient["getHeaders"] {
return (customHeaders) => {
return { ...(customHeaders ?? {}), ...config.headers };
};
}

export function generateGetGQLClientParams({
getHeaders,
getApiUrl,
}: {
getHeaders: ApiClient["getHeaders"];
getApiUrl: ApiClient["getApiUrl"];
}) {
return (
operation: string,
options?: ApiClientRequestOptions
): RequestParams => {
const props: RequestParams = [operation];

if (options && Object.keys(options).length > 0) {
const {
variables,
apiVersion: propApiVersion,
customHeaders,
retries,
} = options;

props.push({
...(variables ? { variables } : {}),
...(customHeaders ? { headers: getHeaders(customHeaders) } : {}),
...(propApiVersion ? { url: getApiUrl(propApiVersion) } : {}),
...(retries ? { retries } : {}),
});
}

return props;
};
}

0 comments on commit dfedb43

Please sign in to comment.