Skip to content

Commit

Permalink
Merge pull request #442 from Shopify/add-storefront-client
Browse files Browse the repository at this point in the history
Add storefront client
  • Loading branch information
byrichardpowell authored Sep 22, 2023
2 parents a3a26b1 + 6ac6832 commit 3351e73
Show file tree
Hide file tree
Showing 40 changed files with 2,052 additions and 1,810 deletions.
46 changes: 46 additions & 0 deletions .changeset/thirty-comics-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
'@shopify/shopify-app-remix': minor
---

Added the storefront GraphQL client.

The storefront API client can be accessed in two ways

<details>
<summary>App Proxy</summary>

```ts
import {json} from '@remix-run/node';
import {authenticate} from '~/shopify.server';

export async function loader({request}) {
const {storefront} = await authenticate.public.appProxy(request);
const response = await storefront.graphql('{blogs(first: 10) {nodes{id}}}');

return json(await response.json());
}
```

</details>

<details>
<summary>Unauthenticated Storefront</summary>

```ts
import {json} from '@remix-run/node';
import {unauthenticated} from '~/shopify.server';
import {customAuthenticateRequest} from '~/helpers';

export async function loader({request}) {
await customAuthenticateRequest(request);

const {storefront} = await unauthenticated.storefront(
'my-shop.myshopify.com',
);
const response = await storefront.graphql('{blogs(first: 10) {nodes{id}}}');

return json(await response.json());
}
```

</details>
2,957 changes: 1,381 additions & 1,576 deletions packages/shopify-app-remix/docs/generated/generated_docs_data.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Session} from '@shopify/shopify-api';

import {LATEST_API_VERSION} from '..';
import {AdminApiContext} from '../config-types';
import type {AdminApiContext} from '../clients';

import {mockExternalRequest} from './request-mock';
import {TEST_SHOP} from './const';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {Session} from '@shopify/shopify-api';

import {LATEST_API_VERSION} from '..';
import type {StorefrontContext} from '../clients';

import {mockExternalRequest} from './request-mock';
import {TEST_SHOP} from './const';

export function expectStorefrontApiClient(
factory: () => Promise<{
storefront: StorefrontContext;
expectedSession: Session;
actualSession: Session;
}>,
) {
it('Storefront client can perform GraphQL Requests', async () => {
// GIVEN
const {storefront, actualSession} = await factory();
const apiResponse = {blogs: {nodes: [{id: 1}]}};
await mockExternalRequest({
request: new Request(
`https://${TEST_SHOP}/api/${LATEST_API_VERSION}/graphql.json`,
{
method: 'POST',
headers: {
'Shopify-Storefront-Private-Token': actualSession.accessToken!,
},
},
),
response: new Response(JSON.stringify(apiResponse)),
});

// WHEN
const response = await storefront.graphql(
'blogs(first: 1) { nodes { id }}',
);

// THEN
expect(response.status).toEqual(200);
expect(await response.json()).toEqual(apiResponse);
});

it('Storefront client uses the correct session', async () => {
// GIVEN
const {expectedSession, actualSession} = await factory();

// THEN
expect(expectedSession).toEqual(actualSession);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './expect-begin-auth-redirect';
export * from './expect-document-request-headers';
export * from './expect-exit-iframe';
export * from './expect-login-redirect';
export * from './expect-storefront-api-client';
export * from './get-hmac';
export * from './get-jwt';
export * from './get-thrown-response';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,9 @@ import {
ShopifyRestResources,
} from '@shopify/shopify-api';

import {adminClientFactory} from '../../clients/admin';
import {AdminApiContext, adminClientFactory} from '../../clients/admin';
import type {BasicParams} from '../../types';
import type {
AdminApiContext,
AppConfig,
AppConfigArg,
} from '../../config-types';
import type {AppConfig, AppConfigArg} from '../../config-types';
import {
getSessionTokenHeader,
validateSessionToken,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {JwtPayload, Session, ShopifyRestResources} from '@shopify/shopify-api';

import {EnsureCORSFunction} from '../helpers/ensure-cors-headers';
import type {AdminApiContext, AppConfigArg} from '../../config-types';
import type {AppConfigArg} from '../../config-types';
import type {AdminApiContext} from '../../clients';

import type {BillingContext} from './billing/types';
import {RedirectFunction} from './helpers/redirect';
Expand Down Expand Up @@ -191,3 +192,8 @@ export type AdminContext<
> = Config['isEmbeddedApp'] extends false
? NonEmbeddedAdminContext<Config, Resources>
: EmbeddedAdminContext<Config, Resources>;

export type AuthenticateAdmin<
Config extends AppConfigArg,
Resources extends ShopifyRestResources = ShopifyRestResources,
> = (request: Request) => Promise<AdminContext<Config, Resources>>;
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import {HashFormat, createSHA256HMAC} from '@shopify/shopify-api/runtime';

import {shopifyApp} from '../../../..';
import {LATEST_API_VERSION, shopifyApp} from '../../../..';
import {
API_SECRET_KEY,
APP_URL,
TEST_SHOP,
expectAdminApiClient,
expectStorefrontApiClient,
getThrownResponse,
mockExternalRequest,
setUpValidSession,
testConfig,
} from '../../../../__test-helpers';
Expand Down Expand Up @@ -98,13 +100,8 @@ describe('authenticating app proxy requests', () => {
const shopify = shopifyApp(config);

// WHEN
const url = new URL(APP_URL);
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', secondsInPast(1));
url.searchParams.set('signature', await createAppProxyHmac(url));

const {liquid} = await shopify.authenticate.public.appProxy(
new Request(url.toString()),
await getValidRequest(),
);
const response = liquid('Liquid template {{shop.name}}');

Expand All @@ -120,13 +117,8 @@ describe('authenticating app proxy requests', () => {
const shopify = shopifyApp(config);

// WHEN
const url = new URL(APP_URL);
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', secondsInPast(1));
url.searchParams.set('signature', await createAppProxyHmac(url));

const {liquid} = await shopify.authenticate.public.appProxy(
new Request(url.toString()),
await getValidRequest(),
);
const response = liquid('Liquid template {{shop.name}}', 400);

Expand All @@ -140,13 +132,8 @@ describe('authenticating app proxy requests', () => {
const shopify = shopifyApp(config);

// WHEN
const url = new URL(APP_URL);
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', secondsInPast(1));
url.searchParams.set('signature', await createAppProxyHmac(url));

const {liquid} = await shopify.authenticate.public.appProxy(
new Request(url.toString()),
await getValidRequest(),
);
const response = liquid('Liquid template {{shop.name}}', {
headers: {
Expand All @@ -169,13 +156,8 @@ describe('authenticating app proxy requests', () => {
const shopify = shopifyApp(config);

// WHEN
const url = new URL(APP_URL);
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', secondsInPast(1));
url.searchParams.set('signature', await createAppProxyHmac(url));

const {liquid} = await shopify.authenticate.public.appProxy(
new Request(url.toString()),
await getValidRequest(),
);
const response = liquid('Liquid template {{shop.name}}', {
layout: false,
Expand All @@ -193,13 +175,8 @@ describe('authenticating app proxy requests', () => {
const shopify = shopifyApp(config);

// WHEN
const url = new URL(APP_URL);
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', secondsInPast(1));
url.searchParams.set('signature', await createAppProxyHmac(url));

const {liquid} = await shopify.authenticate.public.appProxy(
new Request(url.toString()),
await getValidRequest(),
);
const response = liquid('Liquid template {{shop.name}}', {
status: 400,
Expand All @@ -221,19 +198,15 @@ describe('authenticating app proxy requests', () => {
const shopify = shopifyApp(config);

// WHEN
const url = new URL(APP_URL);
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', secondsInPast(1));
url.searchParams.set('signature', await createAppProxyHmac(url));

const context = await shopify.authenticate.public.appProxy(
new Request(url.toString()),
await getValidRequest(),
);

// THEN
expect(context).toStrictEqual({
session: undefined,
admin: undefined,
storefront: undefined,
liquid: expect.any(Function),
});
});
Expand All @@ -246,14 +219,47 @@ describe('authenticating app proxy requests', () => {
shopify.sessionStorage,
false,
);

const {admin, session: actualSession} =
await shopify.unauthenticated.admin(TEST_SHOP);
await shopify.authenticate.public.appProxy(await getValidRequest());

if (!admin) {
throw new Error('No admin client');
}

return {admin, expectedSession, actualSession};
});
});

describe('Valid requests with a session return a Storefront API client', () => {
expectStorefrontApiClient(async () => {
const shopify = shopifyApp(testConfig());
const expectedSession = await setUpValidSession(
shopify.sessionStorage,
false,
);

const {storefront, session: actualSession} =
await shopify.authenticate.public.appProxy(await getValidRequest());

if (!storefront) {
throw new Error('No storefront client');
}

return {storefront, expectedSession, actualSession};
});
});
});

async function getValidRequest(): Promise<Request> {
const url = new URL(APP_URL);
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', secondsInPast(1));
url.searchParams.set('signature', await createAppProxyHmac(url));

return new Request(url.toString());
}

async function createAppProxyHmac(url: URL): Promise<string> {
const params = Object.fromEntries(url.searchParams.entries());
const string = Object.entries(params)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,19 @@ const data: ReferenceEntityTemplateSchema = {
description: 'Authenticates requests coming from Shopify app proxies.',
type: 'AuthenticateAppProxy',
},
{
title: 'AppProxyContext',
description: 'Object returned by `authenticate.public.appProxy`.',
type: 'AppProxyContextWithSession',
},
],
jsDocTypeExamples: ['AppProxyContextWithSession'],
related: [
{
name: 'API context',
name: 'Admin API context',
subtitle: 'Interact with the Admin API.',
url: '/docs/api/shopify-app-remix/apis/admin-api',
},
{
name: 'Storefront API context',
subtitle: 'Interact with the Storefront API.',
url: '/docs/api/shopify-app-remix/apis/storefront-api',
},
],
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {ShopifyRestResources} from '@shopify/shopify-api';

import {adminClientFactory} from '../../../clients/admin';
import {adminClientFactory, storefrontClientFactory} from '../../../clients';
import {BasicParams} from '../../../types';

import {
Expand Down Expand Up @@ -50,6 +50,7 @@ export function authenticateAppProxyFactory<
liquid,
session: undefined,
admin: undefined,
storefront: undefined,
};

return context;
Expand All @@ -59,6 +60,7 @@ export function authenticateAppProxyFactory<
liquid,
session,
admin: adminClientFactory({params, session}),
storefront: storefrontClientFactory({params, session}),
};

return context;
Expand Down
Loading

0 comments on commit 3351e73

Please sign in to comment.