Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Customer Account API in GraphiQL #1693

Merged
merged 12 commits into from
Jan 29, 2024
6 changes: 6 additions & 0 deletions .changeset/polite-bikes-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/hydrogen': patch
'@shopify/cli-hydrogen': patch
---

Add support for multiple schemas in GraphiQL. Fix links in Subrequest Profiler.
10 changes: 9 additions & 1 deletion packages/cli/src/lib/graphiql-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ export function getGraphiQLUrl({
graphql,
}: {
host?: string;
graphql?: {query: string; variables: string | Record<string, any>};
graphql?: {
query: string;
variables: string | Record<string, any>;
schema?: string;
};
}) {
let url = `${host.endsWith('/') ? host.slice(0, -1) : host}/graphiql`;

Expand All @@ -14,6 +18,10 @@ export function getGraphiQLUrl({
url += `?query=${encodeURIComponent(query)}${
variables ? `&variables=${encodeURIComponent(variables)}` : ''
}`;

if (graphql.schema) {
url += `&schema=${graphql.schema}`;
}
}

return url;
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/src/lib/mini-oxygen/assets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import {createRequire} from 'node:module';
import {
createServer,
type IncomingMessage,
Expand Down Expand Up @@ -39,7 +40,15 @@ export function createAssetsServer(buildPathClient: string) {
: pathname;

if (isValidAssetPath) {
const filePath = path.join(buildPathClient, relativeAssetPath);
let filePath = path.join(buildPathClient, relativeAssetPath);

// Request coming from /graphiql
if (relativeAssetPath === '/graphiql/customer-account.schema.json') {
const require = createRequire(import.meta.url);
filePath = require.resolve(
'@shopify/hydrogen/customer-account.schema.json',
);
}

// Ignore errors and just return 404
const file = await fs.open(filePath).catch(() => {});
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/lib/request-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ async function getRequestInfo(request: RequestKind) {
purpose: data.purpose === 'prefetch' ? '(prefetch)' : '',
cacheStatus: data.cacheStatus ?? '',
graphql: data.graphql
? (JSON.parse(data.graphql) as {query: string; variables: object})
? (JSON.parse(data.graphql) as {
query: string;
variables: object;
schema?: string;
})
: null,
};
}
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/virtual-routes/components/RequestDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,12 @@ export function RequestDetails({
<div className="grid-layout">
<DetailsRow rowName="Name" value={requestInfo.displayName} />
<DetailsRow rowName="Request URL" value={requestInfo.url} />
<DetailsRow
rowName="Status"
value={`${requestInfo.responseInit?.status} ${requestInfo.responseInit?.statusText}`}
/>
{requestInfo.responseInit ? (
<DetailsRow
rowName="Status"
value={`${requestInfo.responseInit?.status} ${requestInfo.responseInit?.statusText}`}
/>
) : null}
<DetailsRow
rowName="GraphiQL"
value={requestInfo.graphiqlLink}
Expand Down
16 changes: 12 additions & 4 deletions packages/hydrogen/src/customer/auth.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,37 @@ import {
CUSTOMER_API_CLIENT_ID,
CUSTOMER_ACCOUNT_SESSION_KEY,
} from './constants';
import type {H2OEvent} from '@shopify/remix-oxygen';

type H2OEvent = Parameters<NonNullable<typeof __H2O_LOG_EVENT>>[0];

export interface Locks {
refresh?: Promise<any>;
}

export const logFetchEvent =
export const logSubRequestEvent =
process.env.NODE_ENV === 'development'
? ({
url,
response,
startTime,
query,
variables,
...debugInfo
}: {
url: H2OEvent['url'];
response: Response;
startTime: H2OEvent['startTime'];
query?: string;
variables?: Record<string, any> | null;
} & Partial<H2OEvent>) => {
globalThis.__H2O_LOG_EVENT?.({
...debugInfo,
eventType: 'subrequest',
url,
startTime,
graphql: query
? JSON.stringify({query, variables, schema: 'customer-account'})
: undefined,
responseInit: {
status: response.status || 0,
statusText: response.statusText || '',
Expand Down Expand Up @@ -100,7 +108,7 @@ export async function refreshToken({
body: newBody,
});

logFetchEvent?.({
logSubRequestEvent?.({
displayName: 'Customer Account API: access token refresh',
url,
startTime,
Expand Down Expand Up @@ -268,7 +276,7 @@ export async function exchangeAccessToken(
body,
});

logFetchEvent?.({
logSubRequestEvent?.({
displayName: 'Customer Account API: access token exchange',
url,
startTime,
Expand Down
27 changes: 14 additions & 13 deletions packages/hydrogen/src/customer/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
getNonce,
redirect,
Locks,
logFetchEvent,
logSubRequestEvent,
} from './auth.helpers';
import {BadRequest} from './BadRequest';
import {generateNonce} from '../csp/nonce';
Expand Down Expand Up @@ -89,7 +89,7 @@ export function createCustomerAccountClient({
? requestUrl.origin.replace('http', 'https')
: requestUrl.origin;
const redirectUri = authUrl.startsWith('/') ? origin + authUrl : authUrl;

const customerAccountApiUrl = `${customerAccountUrl}/account/customer/api/${customerApiVersion}/graphql`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a bunch of similar changes in #1684

const locks: Locks = {};

async function fetchCustomerAPI<T>({
Expand All @@ -109,37 +109,36 @@ export function createCustomerAccountClient({
// Get stack trace before losing it with any async operation.
// Since this is an internal function that is always called from
// the public query/mutate wrappers, add 1 to the stack offset.
const stackInfo = getCallerStackLine?.(1);
const startTime = new Date().getTime();
const stackInfo = getCallerStackLine?.();

const url = `${customerAccountUrl}/account/customer/api/${customerApiVersion}/graphql`;
const graphqlData = JSON.stringify({query, variables});
const startTime = new Date().getTime();

const response = await fetch(url, {
const response = await fetch(customerAccountApiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': USER_AGENT,
Origin: origin,
Authorization: accessToken,
},
body: graphqlData,
body: JSON.stringify({query, variables}),
});

logFetchEvent?.({
url,
logSubRequestEvent?.({
url: customerAccountApiUrl,
startTime,
response,
waitUntil,
stackInfo,
graphql: graphqlData,
query,
variables,
...getDebugHeaders(request),
});

const body = await response.text();

const errorOptions: GraphQLErrorOptions<T> = {
url,
url: customerAccountApiUrl,
response,
type,
query,
Expand Down Expand Up @@ -280,6 +279,7 @@ export function createCustomerAccountClient({
isLoggedIn,
handleAuthStatus,
getAccessToken,
getApiUrl: () => customerAccountApiUrl,
mutate(mutation, options?) {
mutation = minifyQuery(mutation);
assertMutation(mutation, 'customer.mutate');
Expand Down Expand Up @@ -352,8 +352,9 @@ export function createCustomerAccountClient({
body,
});

logFetchEvent?.({
logSubRequestEvent?.({
url,
displayName: 'Customer Account API: authorize',
startTime,
response,
waitUntil,
Expand Down
4 changes: 4 additions & 0 deletions packages/hydrogen/src/customer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export type CustomerAccount = {
handleAuthStatus: () => void | DataFunctionValue;
/** Returns CustomerAccessToken if the customer is logged in. It also run a expiry check and does a token refresh if needed. */
getAccessToken: () => Promise<string | undefined>;
/** Creates the fully-qualified URL to your store's GraphQL endpoint.*/
getApiUrl: () => string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reminder to add this to the Doc type below as well

/** Logout the customer by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. The path app should redirect to after logout can be setup in Customer Account API settings in admin.*/
logout: () => Promise<Response>;
/** Execute a GraphQL query against the Customer Account API. This method execute `handleAuthStatus()` ahead of query. */
Expand Down Expand Up @@ -119,6 +121,8 @@ export type CustomerAccountForDocs = {
handleAuthStatus?: () => void | DataFunctionValue;
/** Returns CustomerAccessToken if the customer is logged in. It also run a expiry check and does a token refresh if needed. */
getAccessToken?: () => Promise<string | undefined>;
/** Creates the fully-qualified URL to your store's GraphQL endpoint.*/
getApiUrl: () => string;
/** Logout the customer by clearing the session and redirecting to the login domain. It should be called and returned from a Remix action. The path app should redirect to after logout can be setup in Customer Account API settings in admin.*/
logout?: () => Promise<Response>;
/** Execute a GraphQL query against the Customer Account API. This method execute `handleAuthStatus()` ahead of query. */
Expand Down
Loading
Loading