-
Notifications
You must be signed in to change notification settings - Fork 283
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
Changes from 5 commits
24d1f3d
9b3b60d
07b1f3a
cbd4944
dc59f99
1b9debe
ef67b52
a0f3fa7
55bfbc0
0074cb5
508f406
2857a53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,6 +53,8 @@ export type CustomerClient = { | |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. */ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,60 @@ | ||
import type {LoaderFunctionArgs} from '@remix-run/server-runtime'; | ||
import type {Storefront} from '../storefront'; | ||
|
||
type GraphiQLLoader = (args: LoaderFunctionArgs) => Promise<Response>; | ||
|
||
export const graphiqlLoader: GraphiQLLoader = async function graphiqlLoader({ | ||
context, | ||
request, | ||
context: {storefront, customerAccount}, | ||
}: LoaderFunctionArgs) { | ||
const storefront = context?.storefront as Storefront | undefined; | ||
const url = new URL(request.url); | ||
|
||
if (!storefront) { | ||
throw new Error( | ||
`GraphiQL: Hydrogen's storefront client must be injected in the loader context.`, | ||
); | ||
} | ||
|
||
const url = storefront.getApiUrl(); | ||
const accessToken = | ||
storefront.getPublicTokenHeaders()['X-Shopify-Storefront-Access-Token']; | ||
const schemas: { | ||
[key: string]: { | ||
name: string; | ||
value?: object; | ||
accessToken?: string; | ||
authHeader: string; | ||
apiUrl: string; | ||
}; | ||
} = {}; | ||
|
||
if (storefront) { | ||
const authHeader = 'X-Shopify-Storefront-Access-Token'; | ||
schemas.storefront = { | ||
name: 'Storefront API', | ||
authHeader, | ||
accessToken: storefront.getPublicTokenHeaders()[authHeader], | ||
apiUrl: storefront.getApiUrl(), | ||
}; | ||
} | ||
|
||
if (customerAccount) { | ||
// CustomerAccount API does not support introspection to the same URL. | ||
// Read it from a file using the asset server: | ||
Comment on lines
+40
to
+41
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @s-lee-kwong Can we add introspection to the graphql end point? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is something we can look at but right now our only introspection point is something like
but that api_key is from an app so I wouldn't use what I pasted exactly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GraphiQL can only work with 1 url for both introspection and queries. So if you want to add support for GraphiQL in CAAPI, the URL for queries should also provide introspection 🙏 Here we had to work around this issue by downloading the schema in a different way and providing it to GarphiQL in JSON format without doing introspection. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will add this to the on going Friction Doc for the future. Having the introspection and queries be at the same URL is a fairly standard way of implementation for GraphQL API. A lot of the tools work assuming this convention and it would be really good to have. |
||
const customerAccountSchema = await ( | ||
await fetch(url.origin + '/graphiql/customer-account.schema.json') | ||
).json(); | ||
|
||
// @ts-ignore This is recognized in editor but not at build time | ||
const accessToken = await customerAccount.getAccessToken(); | ||
|
||
if (customerAccountSchema) { | ||
schemas['customer-account'] = { | ||
name: 'Customer Account API', | ||
value: customerAccountSchema, | ||
authHeader: 'Authorization', | ||
accessToken, | ||
// @ts-ignore This is recognized in editor but not at build time | ||
apiUrl: customerAccount.getApiUrl(), | ||
}; | ||
} | ||
} | ||
|
||
// GraphiQL icon from their GitHub repo | ||
const favicon = `https://avatars.githubusercontent.com/u/12972006?s=48&v=4`; | ||
|
@@ -87,6 +126,8 @@ export const graphiqlLoader: GraphiQLLoader = async function graphiqlLoader({ | |
|
||
<script> | ||
const windowUrl = new URL(document.URL); | ||
const startingSchemaKey = | ||
windowUrl.searchParams.get('schema') || 'storefront'; | ||
|
||
let query = '{ shop { name } }'; | ||
if (windowUrl.searchParams.has('query')) { | ||
|
@@ -98,6 +139,10 @@ export const graphiqlLoader: GraphiQLLoader = async function graphiqlLoader({ | |
// Prettify query | ||
query = GraphiQL.GraphQL.print(GraphiQL.GraphQL.parse(query)); | ||
|
||
if (startingSchemaKey !== 'storefront') { | ||
query += ' #schema:' + startingSchemaKey; | ||
} | ||
|
||
let variables; | ||
if (windowUrl.searchParams.has('variables')) { | ||
variables = decodeURIComponent( | ||
|
@@ -110,23 +155,87 @@ export const graphiqlLoader: GraphiQLLoader = async function graphiqlLoader({ | |
variables = JSON.stringify(JSON.parse(variables), null, 2); | ||
} | ||
|
||
const schemas = ${JSON.stringify(schemas)}; | ||
let lastActiveTabIndex = -1; | ||
|
||
const root = ReactDOM.createRoot( | ||
document.getElementById('graphiql'), | ||
); | ||
root.render( | ||
React.createElement(GraphiQL, { | ||
|
||
root.render(React.createElement(RootWrapper)); | ||
|
||
function RootWrapper() { | ||
const [activeSchema, setActiveSchema] = | ||
React.useState(startingSchemaKey); | ||
|
||
const schema = schemas[activeSchema]; | ||
if (!schema) { | ||
throw new Error('No schema found for ' + activeSchema); | ||
} | ||
|
||
const keys = Object.keys(schemas); | ||
|
||
return React.createElement(GraphiQL, { | ||
fetcher: GraphiQL.createFetcher({ | ||
url: '${url}', | ||
headers: { | ||
'X-Shopify-Storefront-Access-Token': '${accessToken}', | ||
}, | ||
url: schema.apiUrl, | ||
headers: {[schema.authHeader]: schema.accessToken}, | ||
}), | ||
defaultEditorToolsVisibility: true, | ||
query, | ||
variables, | ||
schema: schema.value, | ||
plugins: [GraphiQLPluginExplorer.explorerPlugin()], | ||
}), | ||
); | ||
onTabChange: ({tabs, activeTabIndex}) => { | ||
if (activeTabIndex === lastActiveTabIndex) return; | ||
|
||
lastActiveTabIndex = activeTabIndex; | ||
|
||
const activeTab = tabs[activeTabIndex]; | ||
if (activeTab) { | ||
const nextSchema = | ||
activeTab.query.match(/#schema:([a-z-]+)/m)?.[1] || | ||
'storefront'; | ||
|
||
if (nextSchema !== activeSchema) { | ||
setActiveSchema(nextSchema); | ||
} | ||
} | ||
}, | ||
children: [ | ||
// React.createElement(GraphiQL.Toolbar, {}), | ||
React.createElement(GraphiQL.Logo, { | ||
key: 'Logo replacement', | ||
children: [ | ||
React.createElement('div', { | ||
key: 'Logo wrapper', | ||
style: {display: 'flex', alignItems: 'center'}, | ||
children: [ | ||
React.createElement('div', { | ||
key: 'api', | ||
className: 'graphiql-logo', | ||
style: { | ||
paddingRight: 0, | ||
whiteSpace: 'nowrap', | ||
cursor: 'pointer', | ||
}, | ||
onClick: () => { | ||
clicked = true; | ||
const activeKey = keys.indexOf(activeSchema); | ||
const nextKey = | ||
keys[(activeKey + 1) % keys.length]; | ||
|
||
setActiveSchema(nextKey); | ||
}, | ||
children: [schema.name], | ||
}), | ||
React.createElement(GraphiQL.Logo, {key: 'logo'}), | ||
], | ||
}), | ||
], | ||
}), | ||
], | ||
}); | ||
} | ||
</script> | ||
</body> | ||
</html> | ||
|
There was a problem hiding this comment.
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