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 #1045 from Shopify/ml-sfapi-client-use-factory-utils
Browse files Browse the repository at this point in the history
[Storefront API Client] Update client to use common API client utils
  • Loading branch information
melissaluu authored Nov 8, 2023
2 parents c61e32a + 5d24536 commit 1b6e032
Show file tree
Hide file tree
Showing 11 changed files with 134 additions and 166 deletions.
2 changes: 2 additions & 0 deletions .changeset/tricky-sheep-itch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"cSpell.words": ["isbot"]
"cSpell.words": [
"isbot"
]
}
4 changes: 2 additions & 2 deletions packages/storefront-api-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ const client = createStorefrontApiClient({
| -------------- | ------------------------ | ---------------------------------------------------- |
| storeDomain | `string` | The secure store domain |
| apiVersion | `string` | The Storefront API version to use in the API request |
| publicAccessToken | `string \| null` | The provided public access token |
| privateAccessToken | `string \| null` | The provided private access token |
| publicAccessToken | `string \| never` | The provided public access token. If `privateAccessToken` was provided, `publicAccessToken` will not be available. |
| privateAccessToken | `string \| never` | The provided private access token. If `publicAccessToken` was provided, `privateAccessToken` will not be available. |
| headers | `{[key: string]: string}` | The headers generated by the client during initialization |
| apiUrl | `string` | The API URL generated from the provided store domain and api version |
| clientName? | `string` | The provided client name |
Expand Down
16 changes: 15 additions & 1 deletion packages/storefront-api-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,21 @@
"release": "yarn build && changeset publish"
},
"jest": {
"testEnvironment": "jsdom",
"projects": [
{
"displayName": {
"name": "test:browser",
"color": "blue"
},
"testEnvironment": "jsdom"
},
{
"displayName": {
"name": "test:server",
"color": "yellow"
}
}
],
"setupFilesAfterEnv": [
"./src/tests/setupTests.ts"
],
Expand Down
3 changes: 1 addition & 2 deletions packages/storefront-api-client/rollup.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ export function getPlugins({ tsconfig, minify } = {}) {
}

const packageName = pkg.name.substring(1);
const repositoryName = pkg.repository.url.split(":")[1].split(".")[0];
export const bannerConfig = {
banner: `/*! ${packageName} -- Copyright (c) 2023-present, Shopify Inc. -- license (MIT): https://github.com/${repositoryName}/blob/main/LICENSE */`,
banner: `/*! ${packageName}@${pkg.version} -- Copyright (c) 2023-present, Shopify Inc. -- license (MIT): https://github.com/Shopify/shopify-api-js/blob/main/LICENSE.md */`,
};

const config = [
Expand Down
55 changes: 6 additions & 49 deletions packages/storefront-api-client/src/storefront-api-client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
createGraphQLClient,
RequestParams as GQLClientRequestParams,
getCurrentSupportedApiVersions,
validateDomainAndGetStoreUrl,
validateApiVersion,
generateGetGQLClientParams,
generateGetHeaders,
ApiClientRequestParams,
ApiClientRequestOptions,
} from "@shopify/graphql-client";

import {
Expand Down Expand Up @@ -92,22 +92,20 @@ export function createStorefrontApiClient({
logger,
});

const getHeaders = generateGetHeader(config);
const getHeaders = generateGetHeaders(config);
const getApiUrl = generateGetApiUrl(config, apiUrlFormatter);

const getGQLClientRequestProps = generateGetGQLClientProps({
const getGQLClientParams = generateGetGQLClientParams({
getHeaders,
getApiUrl,
});

const fetch = (...props: ApiClientRequestParams) => {
const requestProps = getGQLClientRequestProps(...props);
return graphqlClient.fetch(...requestProps);
return graphqlClient.fetch(...getGQLClientParams(...props));
};

const request = <TData>(...props: ApiClientRequestParams) => {
const requestProps = getGQLClientRequestProps(...props);
return graphqlClient.request<TData>(...requestProps);
return graphqlClient.request<TData>(...getGQLClientParams(...props));
};

const client: StorefrontApiClient = {
Expand Down Expand Up @@ -143,14 +141,6 @@ function generateApiUrlFormatter(
};
}

function generateGetHeader(
config: StorefrontApiClientConfig
): StorefrontApiClient["getHeaders"] {
return (customHeaders) => {
return { ...(customHeaders ?? {}), ...config.headers };
};
}

function generateGetApiUrl(
config: StorefrontApiClientConfig,
apiUrlFormatter: (version?: string) => string
Expand All @@ -159,36 +149,3 @@ function generateGetApiUrl(
return propApiVersion ? apiUrlFormatter(propApiVersion) : config.apiUrl;
};
}

function generateGetGQLClientProps({
getHeaders,
getApiUrl,
}: {
getHeaders: StorefrontApiClient["getHeaders"];
getApiUrl: StorefrontApiClient["getApiUrl"];
}) {
return (
operation: string,
options?: ApiClientRequestOptions
): GQLClientRequestParams => {
const props: GQLClientRequestParams = [operation];

if (options) {
const {
variables,
apiVersion: propApiVersion,
customHeaders,
retries,
} = options;

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

return props;
};
}
134 changes: 66 additions & 68 deletions packages/storefront-api-client/src/tests/storefront-api-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ describe("Storefront API Client", () => {

it("console warns when a unsupported api version is provided", () => {
const consoleWarnSpy = jest
.spyOn(window.console, "warn")
.spyOn(global.console, "warn")
.mockImplementation(jest.fn());

createStorefrontApiClient({
Expand Down Expand Up @@ -238,38 +238,40 @@ describe("Storefront API Client", () => {
);
});

it("throws an error when a private access token is provided in a browser environment (window is defined)", () => {
expect(() =>
createStorefrontApiClient({
...config,
publicAccessToken: undefined as any,
privateAccessToken: "private-access-token",
})
).toThrow(
new Error(
"Storefront API Client: private access tokens and headers should only be used in a server-to-server implementation. Use the public API access token in nonserver environments."
)
);
});

it("throws an error when both public and private access tokens are provided in a server environment (window is undefined)", () => {
const windowSpy = jest
.spyOn(window, "window", "get")
.mockImplementation(() => undefined as any);

expect(() =>
createStorefrontApiClient({
...config,
privateAccessToken: "private-token",
} as any)
).toThrow(
new Error(
`Storefront API Client: only provide either a public or private access token`
)
);
if (typeof window === "object") {
describe("browser environment", () => {
it("throws an error when a private access token is provided in a browser environment", () => {
expect(() =>
createStorefrontApiClient({
...config,
publicAccessToken: undefined as any,
privateAccessToken: "private-access-token",
})
).toThrow(
new Error(
"Storefront API Client: private access tokens and headers should only be used in a server-to-server implementation. Use the public API access token in nonserver environments."
)
);
});
});
}

windowSpy.mockRestore();
});
if (typeof window === "undefined") {
describe("server enviornment", () => {
it("throws an error when both public and private access tokens are provided in a server environment", () => {
expect(() =>
createStorefrontApiClient({
...config,
privateAccessToken: "private-token",
} as any)
).toThrow(
new Error(
`Storefront API Client: only provide either a public or private access token`
)
);
});
});
}
});
});

Expand All @@ -285,23 +287,21 @@ describe("Storefront API Client", () => {
expect(client.config.privateAccessToken).toBeUndefined();
});

it("returns a config object that includes the provided private access token and not a public access token when in a server environment (window is undefined)", () => {
const windowSpy = jest
.spyOn(window, "window", "get")
.mockImplementation(() => undefined as any);

const privateAccessToken = "private-token";
if (typeof window === "undefined") {
describe("server environment", () => {
it("returns a config object that includes the provided private access token and not a public access token when in a server environment", () => {
const privateAccessToken = "private-token";

const client = createStorefrontApiClient({
...config,
publicAccessToken: undefined,
privateAccessToken,
const client = createStorefrontApiClient({
...config,
publicAccessToken: undefined,
privateAccessToken,
});
expect(client.config.privateAccessToken).toBe(privateAccessToken);
expect(client.config.publicAccessToken).toBeUndefined();
});
});
expect(client.config.privateAccessToken).toBe(privateAccessToken);
expect(client.config.publicAccessToken).toBeUndefined();

windowSpy.mockRestore();
});
}

it("returns a config object that includes the provided client name", () => {
const clientName = "test-client";
Expand Down Expand Up @@ -382,25 +382,23 @@ describe("Storefront API Client", () => {
);
});

it("returns a header object that includes the private headers when a private access token is provided", () => {
const windowSpy = jest
.spyOn(window, "window", "get")
.mockImplementation(() => undefined as any);
if (typeof window === "undefined") {
describe("server environment", () => {
it("returns a header object that includes the private headers when a private access token is provided", () => {
const privateAccessToken = "private-token";

const privateAccessToken = "private-token";
const client = createStorefrontApiClient({
...config,
publicAccessToken: undefined,
privateAccessToken,
});

const client = createStorefrontApiClient({
...config,
publicAccessToken: undefined,
privateAccessToken,
expect(
client.config.headers[PRIVATE_ACCESS_TOKEN_HEADER]
).toEqual(privateAccessToken);
});
});

expect(client.config.headers[PRIVATE_ACCESS_TOKEN_HEADER]).toEqual(
privateAccessToken
);

windowSpy.mockRestore();
});
}

it("returns a header object that includes the SDK variant source header when client name is provided", () => {
const clientName = "test-client";
Expand Down Expand Up @@ -474,7 +472,7 @@ describe("Storefront API Client", () => {

it("console warns when a unsupported api version is provided", () => {
const consoleWarnSpy = jest
.spyOn(window.console, "warn")
.spyOn(global.console, "warn")
.mockImplementation(jest.fn());

const version = "2021-01";
Expand Down Expand Up @@ -514,8 +512,8 @@ describe("Storefront API Client", () => {
});

describe("parameters", () => {
it("calls the graphql client fetch() with just the operation string when there is no provided options", async () => {
const test = await client.fetch(operation);
it("calls the graphql client fetch() with just the operation string when there are no options provided", async () => {
await client.fetch(operation);

expect(graphqlClientMock.fetch).toHaveBeenCalledWith(operation);
});
Expand Down Expand Up @@ -598,8 +596,8 @@ describe("Storefront API Client", () => {
});

describe("parameters", () => {
it("calls the graphql client request() with just the operation string when there is no provided options", async () => {
const test = await client.request(operation);
it("calls the graphql client request() with just the operation string when there are no options provided", async () => {
await client.request(operation);

expect(graphqlClientMock.request).toHaveBeenCalledWith(operation);
});
Expand Down
44 changes: 21 additions & 23 deletions packages/storefront-api-client/src/tests/validations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,25 @@ describe("validateRequiredAccessToken()", () => {
});

describe("validatePrivateAccessTokenUsage()", () => {
it("throws an error when private token is provided within a browser environment (window is defined)", () => {
const privateAccessToken = "private-token";

expect(() => validatePrivateAccessTokenUsage(privateAccessToken)).toThrow(
new Error(
"Storefront API Client: private access tokens and headers should only be used in a server-to-server implementation. Use the public API access token in nonserver environments."
)
);
});

it("does not throw an error when only the private token is provided when within a server environment (window is undefined)", () => {
const windowSpy = jest
.spyOn(window, "window", "get")
.mockImplementation(() => undefined as any);

const privateAccessToken = "private-token";

expect(() =>
validatePrivateAccessTokenUsage(privateAccessToken)
).not.toThrow();

windowSpy.mockRestore();
});
if (typeof window === "object") {
it("throws an error when a private token is provided within a browser environment (window is defined)", () => {
const privateAccessToken = "private-token";

expect(() => validatePrivateAccessTokenUsage(privateAccessToken)).toThrow(
new Error(
"Storefront API Client: private access tokens and headers should only be used in a server-to-server implementation. Use the public API access token in nonserver environments."
)
);
});
}

if (typeof window === "undefined") {
it("does not throw an error when only the private token is provided when within a server environment", () => {
const privateAccessToken = "private-token";

expect(() =>
validatePrivateAccessTokenUsage(privateAccessToken)
).not.toThrow();
});
}
});
Loading

0 comments on commit 1b6e032

Please sign in to comment.