# App proxy [App proxies](https://shopify.dev/docs/apps/online-store/app-proxies) take requests to Shopify links, and redirect them to external links. The `authenticate.public.appProxy` function validates requests made to app proxies, and returns a context to enable querying Shopify APIs. > Note: If the store has not installed the app, store-related properties such as `admin` or `storefront` will be `undefined` ```typescript import type {LoaderFunctionArgs} from '@remix-run/node'; import {authenticate} from '../shopify.server'; export const loader = async ({request}: LoaderFunctionArgs) => { const {storefront, liquid} = await authenticate.public.appProxy(request); if (!storefront) { return new Response(); } const response = await storefront.graphql( `#graphql query productTitle { products(first: 1) { nodes { title } } }`, ); const body = await response.json(); const title = body.data.products.nodes[0].title; return liquid(`Found product ${title} from {{shop.name}}`); }; ``` ## authenticate.public.appProxy Authenticates requests coming to the app from Shopify app proxies. ### AuthenticateAppProxy #### Returns: Promise #### Params: - request: Request export type AuthenticateAppProxy = ( request: Request, ) => Promise; ### AppProxyContext ### session value: `undefined` No session is available for the shop that made this request. This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. ### admin value: `undefined` No session is available for the shop that made this request. Therefore no methods for interacting with the GraphQL / REST Admin APIs are available. ### storefront value: `undefined` No session is available for the shop that made this request. Therefore no method for interacting with the Storefront API is available. ### liquid value: `LiquidResponseFunction` - LiquidResponseFunction: export type LiquidResponseFunction = ( body: string, initAndOptions?: number | (ResponseInit & Options), ) => Response; A utility for creating a Liquid Response. ### LiquidResponseFunction #### Returns: Response #### Params: - body: string - initAndOptions: number | (ResponseInit & Options) export type LiquidResponseFunction = ( body: string, initAndOptions?: number | (ResponseInit & Options), ) => Response; ### Options ### layout value: `boolean` Whether to use the shop's theme layout around the Liquid content. ### AppProxyContextWithSession ### session value: `Session` - Session: export class Session { public static fromPropertyArray( entries: [string, string | number | boolean][], returnUserData = false, ): Session { if (!Array.isArray(entries)) { throw new InvalidSession( 'The parameter is not an array: a Session cannot be created from this object.', ); } const obj = Object.fromEntries( entries .filter(([_key, value]) => value !== null && value !== undefined) // Sanitize keys .map(([key, value]) => { switch (key.toLowerCase()) { case 'isonline': return ['isOnline', value]; case 'accesstoken': return ['accessToken', value]; case 'onlineaccessinfo': return ['onlineAccessInfo', value]; case 'userid': return ['userId', value]; case 'firstname': return ['firstName', value]; case 'lastname': return ['lastName', value]; case 'accountowner': return ['accountOwner', value]; case 'emailverified': return ['emailVerified', value]; default: return [key.toLowerCase(), value]; } }), ); const sessionData = {} as SessionParams; const onlineAccessInfo = { associated_user: {}, } as OnlineAccessInfo; Object.entries(obj).forEach(([key, value]) => { switch (key) { case 'isOnline': if (typeof value === 'string') { sessionData[key] = value.toString().toLowerCase() === 'true'; } else if (typeof value === 'number') { sessionData[key] = Boolean(value); } else { sessionData[key] = value; } break; case 'scope': sessionData[key] = value.toString(); break; case 'expires': sessionData[key] = value ? new Date(Number(value)) : undefined; break; case 'onlineAccessInfo': onlineAccessInfo.associated_user.id = Number(value); break; case 'userId': if (returnUserData) { onlineAccessInfo.associated_user.id = Number(value); break; } case 'firstName': if (returnUserData) { onlineAccessInfo.associated_user.first_name = String(value); break; } case 'lastName': if (returnUserData) { onlineAccessInfo.associated_user.last_name = String(value); break; } case 'email': if (returnUserData) { onlineAccessInfo.associated_user.email = String(value); break; } case 'accountOwner': if (returnUserData) { onlineAccessInfo.associated_user.account_owner = Boolean(value); break; } case 'locale': if (returnUserData) { onlineAccessInfo.associated_user.locale = String(value); break; } case 'collaborator': if (returnUserData) { onlineAccessInfo.associated_user.collaborator = Boolean(value); break; } case 'emailVerified': if (returnUserData) { onlineAccessInfo.associated_user.email_verified = Boolean(value); break; } // Return any user keys as passed in default: sessionData[key] = value; } }); if (sessionData.isOnline) { sessionData.onlineAccessInfo = onlineAccessInfo; } const session = new Session(sessionData); return session; } /** * The unique identifier for the session. */ readonly id: string; /** * The Shopify shop domain, such as `example.myshopify.com`. */ public shop: string; /** * The state of the session. Used for the OAuth authentication code flow. */ public state: string; /** * Whether the access token in the session is online or offline. */ public isOnline: boolean; /** * The desired scopes for the access token, at the time the session was created. */ public scope?: string; /** * The date the access token expires. */ public expires?: Date; /** * The access token for the session. */ public accessToken?: string; /** * Information on the user for the session. Only present for online sessions. */ public onlineAccessInfo?: OnlineAccessInfo; constructor(params: SessionParams) { Object.assign(this, params); } /** * Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. */ public isActive(scopes: AuthScopes | string | string[]): boolean { return ( !this.isScopeChanged(scopes) && Boolean(this.accessToken) && !this.isExpired() ); } /** * Whether the access token has the given scopes. */ public isScopeChanged(scopes: AuthScopes | string | string[]): boolean { const scopesObject = scopes instanceof AuthScopes ? scopes : new AuthScopes(scopes); return !scopesObject.equals(this.scope); } /** * Whether the access token is expired. */ public isExpired(withinMillisecondsOfExpiry = 0): boolean { return Boolean( this.expires && this.expires.getTime() - withinMillisecondsOfExpiry < Date.now(), ); } /** * Converts an object with data into a Session. */ public toObject(): SessionParams { const object: SessionParams = { id: this.id, shop: this.shop, state: this.state, isOnline: this.isOnline, }; if (this.scope) { object.scope = this.scope; } if (this.expires) { object.expires = this.expires; } if (this.accessToken) { object.accessToken = this.accessToken; } if (this.onlineAccessInfo) { object.onlineAccessInfo = this.onlineAccessInfo; } return object; } /** * Checks whether the given session is equal to this session. */ public equals(other: Session | undefined): boolean { if (!other) return false; const mandatoryPropsMatch = this.id === other.id && this.shop === other.shop && this.state === other.state && this.isOnline === other.isOnline; if (!mandatoryPropsMatch) return false; const copyA = this.toPropertyArray(true); copyA.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); const copyB = other.toPropertyArray(true); copyB.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); return JSON.stringify(copyA) === JSON.stringify(copyB); } /** * Converts the session into an array of key-value pairs. */ public toPropertyArray( returnUserData = false, ): [string, string | number | boolean][] { return ( Object.entries(this) .filter( ([key, value]) => propertiesToSave.includes(key) && value !== undefined && value !== null, ) // Prepare values for db storage .flatMap(([key, value]): [string, string | number | boolean][] => { switch (key) { case 'expires': return [[key, value ? value.getTime() : undefined]]; case 'onlineAccessInfo': // eslint-disable-next-line no-negated-condition if (!returnUserData) { return [[key, value.associated_user.id]]; } else { return [ ['userId', value?.associated_user?.id], ['firstName', value?.associated_user?.first_name], ['lastName', value?.associated_user?.last_name], ['email', value?.associated_user?.email], ['locale', value?.associated_user?.locale], ['emailVerified', value?.associated_user?.email_verified], ['accountOwner', value?.associated_user?.account_owner], ['collaborator', value?.associated_user?.collaborator], ]; } default: return [[key, value]]; } }) // Filter out tuples with undefined values .filter(([_key, value]) => value !== undefined) ); } } The session for the shop that made the request. This comes from the session storage which `shopifyApp` uses to store sessions in your database of choice. Use this to get shop or user-specific data. ### admin value: `AdminApiContext` - AdminApiContext: export interface AdminApiContext< Resources extends ShopifyRestResources = ShopifyRestResources, > { /** * Methods for interacting with the Shopify Admin REST API * * There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. * * {@link https://shopify.dev/docs/api/admin-rest} * * @example * Using REST resources. * Getting the number of orders in a store using REST resources. Visit the [Admin REST API references](https://shopify.dev/docs/api/admin-rest) for examples on using each resource. * * ```ts * // /app/routes/**\/*.ts * import { LoaderFunctionArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const loader = async ({ request }: LoaderFunctionArgs) => { * const { * admin, * session, * } = await authenticate.admin(request); * * return json( * admin.rest.resources.Order.count({ session }), * ); * }; * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-07"; * * const shopify = shopifyApp({ * restResources, * // ...etc * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * * @example * Performing a GET request to the REST API. * Use `admin.rest.get` to make custom requests to make a request to to the `customer/count` endpoint * * ```ts * // /app/routes/**\/*.ts * import { LoaderFunctionArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const loader = async ({ request }: LoaderFunctionArgs) => { * const { * admin, * session, * } = await authenticate.admin(request); * * const response = await admin.rest.get({ * path: "/customers/count.json", * }); * const customers = await response.json(); * * return json({ customers }); * }; * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; * * const shopify = shopifyApp({ * restResources, * // ...etc * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * * @example * Performing a POST request to the REST API. * Use `admin.rest.post` to make custom requests to make a request to to the `customers.json` endpoint to send a welcome email * ```ts * // /app/routes/**\/*.ts * import { LoaderFunctionArgs, json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const loader = async ({ request }: LoaderFunctionArgs) => { * const { * admin, * session, * } = await authenticate.admin(request); * * const response = admin.rest.post({ * path: "customers/7392136888625/send_invite.json", * body: { * customer_invite: { * to: "new_test_email@shopify.com", * from: "j.limited@example.com", * bcc: ["j.limited@example.com"], * subject: "Welcome to my new shop", * custom_message: "My awesome new store", * }, * }, * }); * * const customerInvite = await response.json(); * return json({ customerInvite }); * }; * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * import { restResources } from "@shopify/shopify-api/rest/admin/2023-04"; * * const shopify = shopifyApp({ * restResources, * // ...etc * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ rest: RestClientWithResources; /** * Methods for interacting with the Shopify Admin GraphQL API * * {@link https://shopify.dev/docs/api/admin-graphql} * {@link https://github.com/Shopify/shopify-app-js/blob/main/packages/apps/shopify-api/docs/reference/clients/Graphql.md} * * @example * Querying the GraphQL API. * Use `admin.graphql` to make query / mutation requests. * ```ts * // /app/routes/**\/*.ts * import { ActionFunctionArgs } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const action = async ({ request }: ActionFunctionArgs) => { * const { admin } = await authenticate.admin(request); * * const response = await admin.graphql( * `#graphql * mutation populateProduct($input: ProductInput!) { * productCreate(input: $input) { * product { * id * } * } * }`, * { * variables: { * input: { title: "Product Name" }, * }, * }, * ); * * const productData = await response.json(); * return json({ * productId: productData.data?.productCreate?.product?.id, * }); * } * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * * const shopify = shopifyApp({ * // ... * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` * * @example * Handling GraphQL errors. * Catch `GraphqlQueryError` errors to see error messages from the API. * ```ts * // /app/routes/**\/*.ts * import { ActionFunctionArgs } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const action = async ({ request }: ActionFunctionArgs) => { * const { admin } = await authenticate.admin(request); * * try { * const response = await admin.graphql( * `#graphql * query incorrectQuery { * products(first: 10) { * nodes { * not_a_field * } * } * }`, * ); * * return json({ data: await response.json() }); * } catch (error) { * if (error instanceof GraphqlQueryError) { * // error.body.errors: * // { graphQLErrors: [ * // { message: "Field 'not_a_field' doesn't exist on type 'Product'" } * // ] } * return json({ errors: error.body?.errors }, { status: 500 }); * } * return json({ message: "An error occurred" }, { status: 500 }); * } * } * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * * const shopify = shopifyApp({ * // ... * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ graphql: GraphQLClient; } Methods for interacting with the GraphQL / REST Admin APIs for the store that made the request. ### storefront value: `StorefrontContext` - StorefrontContext: export interface StorefrontContext { /** * Method for interacting with the Shopify Storefront GraphQL API * * If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints). * * {@link https://shopify.dev/docs/api/storefront} * * @example * Querying the GraphQL API. * Use `storefront.graphql` to make query / mutation requests. * ```ts * // app/routes/**\/.ts * import { json } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export async function action({ request }: ActionFunctionArgs) { * const { storefront } = await authenticate.public.appProxy(request); * * const response = await storefront.graphql(`{blogs(first: 10) { edges { node { id } } } }`); * * return json(await response.json()); * } * ``` * * @example * Handling GraphQL errors. * Catch `GraphqlQueryError` errors to see error messages from the API. * ```ts * // /app/routes/**\/*.ts * import { ActionFunctionArgs } from "@remix-run/node"; * import { authenticate } from "../shopify.server"; * * export const action = async ({ request }: ActionFunctionArgs) => { * const { storefront } = await authenticate.public.appProxy(request); * * try { * const response = await storefront.graphql( * `#graphql * query incorrectQuery { * products(first: 10) { * nodes { * not_a_field * } * } * }`, * ); * * return json({ data: await response.json() }); * } catch (error) { * if (error instanceof GraphqlQueryError) { * // { errors: { graphQLErrors: [ * // { message: "Field 'not_a_field' doesn't exist on type 'Product'" } * // ] } } * return json({ errors: error.body?.errors }, { status: 500 }); * } * return json({ message: "An error occurred" }, { status: 500 }); * } * } * ``` * * ```ts * // /app/shopify.server.ts * import { shopifyApp } from "@shopify/shopify-app-remix/server"; * * const shopify = shopifyApp({ * // ... * }); * export default shopify; * export const authenticate = shopify.authenticate; * ``` */ graphql: GraphQLClient; } Method for interacting with the Shopify Storefront Graphql API for the store that made the request. ### liquid value: `LiquidResponseFunction` - LiquidResponseFunction: export type LiquidResponseFunction = ( body: string, initAndOptions?: number | (ResponseInit & Options), ) => Response; A utility for creating a Liquid Response. ### Session Stores App information from logged in merchants so they can make authenticated requests to the Admin API. ### id value: `string` The unique identifier for the session. ### shop value: `string` The Shopify shop domain, such as `example.myshopify.com`. ### state value: `string` The state of the session. Used for the OAuth authentication code flow. ### isOnline value: `boolean` Whether the access token in the session is online or offline. ### scope value: `string` The desired scopes for the access token, at the time the session was created. ### expires value: `Date` The date the access token expires. ### accessToken value: `string` The access token for the session. ### onlineAccessInfo value: `OnlineAccessInfo` - OnlineAccessInfo: export interface OnlineAccessInfo { /** * How long the access token is valid for, in seconds. */ expires_in: number; /** * The effective set of scopes for the session. */ associated_user_scope: string; /** * The user associated with the access token. */ associated_user: OnlineAccessUser; } Information on the user for the session. Only present for online sessions. ### isActive value: `(scopes: string | string[] | AuthScopes) => boolean` - AuthScopes: class AuthScopes { public static SCOPE_DELIMITER = ','; private compressedScopes: Set; private expandedScopes: Set; constructor(scopes: string | string[] | AuthScopes | undefined) { let scopesArray: string[] = []; if (typeof scopes === 'string') { scopesArray = scopes.split( new RegExp(`${AuthScopes.SCOPE_DELIMITER}\\s*`), ); } else if (Array.isArray(scopes)) { scopesArray = scopes; } else if (scopes) { scopesArray = Array.from(scopes.expandedScopes); } scopesArray = scopesArray .map((scope) => scope.trim()) .filter((scope) => scope.length); const impliedScopes = this.getImpliedScopes(scopesArray); const scopeSet = new Set(scopesArray); const impliedSet = new Set(impliedScopes); this.compressedScopes = new Set( [...scopeSet].filter((x) => !impliedSet.has(x)), ); this.expandedScopes = new Set([...scopeSet, ...impliedSet]); } /** * Checks whether the current set of scopes includes the given one. */ public has(scope: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (scope instanceof AuthScopes) { other = scope; } else { other = new AuthScopes(scope); } return ( other.toArray().filter((x) => !this.expandedScopes.has(x)).length === 0 ); } /** * Checks whether the current set of scopes equals the given one. */ public equals(otherScopes: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (otherScopes instanceof AuthScopes) { other = otherScopes; } else { other = new AuthScopes(otherScopes); } return ( this.compressedScopes.size === other.compressedScopes.size && this.toArray().filter((x) => !other.has(x)).length === 0 ); } /** * Returns a comma-separated string with the current set of scopes. */ public toString() { return this.toArray().join(AuthScopes.SCOPE_DELIMITER); } /** * Returns an array with the current set of scopes. */ public toArray() { return [...this.compressedScopes]; } private getImpliedScopes(scopesArray: string[]): string[] { return scopesArray.reduce((array: string[], current: string) => { const matches = current.match(/^(unauthenticated_)?write_(.*)$/); if (matches) { array.push(`${matches[1] ? matches[1] : ''}read_${matches[2]}`); } return array; }, []); } } Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. ### isScopeChanged value: `(scopes: string | string[] | AuthScopes) => boolean` - AuthScopes: class AuthScopes { public static SCOPE_DELIMITER = ','; private compressedScopes: Set; private expandedScopes: Set; constructor(scopes: string | string[] | AuthScopes | undefined) { let scopesArray: string[] = []; if (typeof scopes === 'string') { scopesArray = scopes.split( new RegExp(`${AuthScopes.SCOPE_DELIMITER}\\s*`), ); } else if (Array.isArray(scopes)) { scopesArray = scopes; } else if (scopes) { scopesArray = Array.from(scopes.expandedScopes); } scopesArray = scopesArray .map((scope) => scope.trim()) .filter((scope) => scope.length); const impliedScopes = this.getImpliedScopes(scopesArray); const scopeSet = new Set(scopesArray); const impliedSet = new Set(impliedScopes); this.compressedScopes = new Set( [...scopeSet].filter((x) => !impliedSet.has(x)), ); this.expandedScopes = new Set([...scopeSet, ...impliedSet]); } /** * Checks whether the current set of scopes includes the given one. */ public has(scope: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (scope instanceof AuthScopes) { other = scope; } else { other = new AuthScopes(scope); } return ( other.toArray().filter((x) => !this.expandedScopes.has(x)).length === 0 ); } /** * Checks whether the current set of scopes equals the given one. */ public equals(otherScopes: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (otherScopes instanceof AuthScopes) { other = otherScopes; } else { other = new AuthScopes(otherScopes); } return ( this.compressedScopes.size === other.compressedScopes.size && this.toArray().filter((x) => !other.has(x)).length === 0 ); } /** * Returns a comma-separated string with the current set of scopes. */ public toString() { return this.toArray().join(AuthScopes.SCOPE_DELIMITER); } /** * Returns an array with the current set of scopes. */ public toArray() { return [...this.compressedScopes]; } private getImpliedScopes(scopesArray: string[]): string[] { return scopesArray.reduce((array: string[], current: string) => { const matches = current.match(/^(unauthenticated_)?write_(.*)$/); if (matches) { array.push(`${matches[1] ? matches[1] : ''}read_${matches[2]}`); } return array; }, []); } } Whether the access token has the given scopes. ### isExpired value: `(withinMillisecondsOfExpiry?: number) => boolean` Whether the access token is expired. ### toObject value: `() => SessionParams` - Session: export class Session { public static fromPropertyArray( entries: [string, string | number | boolean][], returnUserData = false, ): Session { if (!Array.isArray(entries)) { throw new InvalidSession( 'The parameter is not an array: a Session cannot be created from this object.', ); } const obj = Object.fromEntries( entries .filter(([_key, value]) => value !== null && value !== undefined) // Sanitize keys .map(([key, value]) => { switch (key.toLowerCase()) { case 'isonline': return ['isOnline', value]; case 'accesstoken': return ['accessToken', value]; case 'onlineaccessinfo': return ['onlineAccessInfo', value]; case 'userid': return ['userId', value]; case 'firstname': return ['firstName', value]; case 'lastname': return ['lastName', value]; case 'accountowner': return ['accountOwner', value]; case 'emailverified': return ['emailVerified', value]; default: return [key.toLowerCase(), value]; } }), ); const sessionData = {} as SessionParams; const onlineAccessInfo = { associated_user: {}, } as OnlineAccessInfo; Object.entries(obj).forEach(([key, value]) => { switch (key) { case 'isOnline': if (typeof value === 'string') { sessionData[key] = value.toString().toLowerCase() === 'true'; } else if (typeof value === 'number') { sessionData[key] = Boolean(value); } else { sessionData[key] = value; } break; case 'scope': sessionData[key] = value.toString(); break; case 'expires': sessionData[key] = value ? new Date(Number(value)) : undefined; break; case 'onlineAccessInfo': onlineAccessInfo.associated_user.id = Number(value); break; case 'userId': if (returnUserData) { onlineAccessInfo.associated_user.id = Number(value); break; } case 'firstName': if (returnUserData) { onlineAccessInfo.associated_user.first_name = String(value); break; } case 'lastName': if (returnUserData) { onlineAccessInfo.associated_user.last_name = String(value); break; } case 'email': if (returnUserData) { onlineAccessInfo.associated_user.email = String(value); break; } case 'accountOwner': if (returnUserData) { onlineAccessInfo.associated_user.account_owner = Boolean(value); break; } case 'locale': if (returnUserData) { onlineAccessInfo.associated_user.locale = String(value); break; } case 'collaborator': if (returnUserData) { onlineAccessInfo.associated_user.collaborator = Boolean(value); break; } case 'emailVerified': if (returnUserData) { onlineAccessInfo.associated_user.email_verified = Boolean(value); break; } // Return any user keys as passed in default: sessionData[key] = value; } }); if (sessionData.isOnline) { sessionData.onlineAccessInfo = onlineAccessInfo; } const session = new Session(sessionData); return session; } /** * The unique identifier for the session. */ readonly id: string; /** * The Shopify shop domain, such as `example.myshopify.com`. */ public shop: string; /** * The state of the session. Used for the OAuth authentication code flow. */ public state: string; /** * Whether the access token in the session is online or offline. */ public isOnline: boolean; /** * The desired scopes for the access token, at the time the session was created. */ public scope?: string; /** * The date the access token expires. */ public expires?: Date; /** * The access token for the session. */ public accessToken?: string; /** * Information on the user for the session. Only present for online sessions. */ public onlineAccessInfo?: OnlineAccessInfo; constructor(params: SessionParams) { Object.assign(this, params); } /** * Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. */ public isActive(scopes: AuthScopes | string | string[]): boolean { return ( !this.isScopeChanged(scopes) && Boolean(this.accessToken) && !this.isExpired() ); } /** * Whether the access token has the given scopes. */ public isScopeChanged(scopes: AuthScopes | string | string[]): boolean { const scopesObject = scopes instanceof AuthScopes ? scopes : new AuthScopes(scopes); return !scopesObject.equals(this.scope); } /** * Whether the access token is expired. */ public isExpired(withinMillisecondsOfExpiry = 0): boolean { return Boolean( this.expires && this.expires.getTime() - withinMillisecondsOfExpiry < Date.now(), ); } /** * Converts an object with data into a Session. */ public toObject(): SessionParams { const object: SessionParams = { id: this.id, shop: this.shop, state: this.state, isOnline: this.isOnline, }; if (this.scope) { object.scope = this.scope; } if (this.expires) { object.expires = this.expires; } if (this.accessToken) { object.accessToken = this.accessToken; } if (this.onlineAccessInfo) { object.onlineAccessInfo = this.onlineAccessInfo; } return object; } /** * Checks whether the given session is equal to this session. */ public equals(other: Session | undefined): boolean { if (!other) return false; const mandatoryPropsMatch = this.id === other.id && this.shop === other.shop && this.state === other.state && this.isOnline === other.isOnline; if (!mandatoryPropsMatch) return false; const copyA = this.toPropertyArray(true); copyA.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); const copyB = other.toPropertyArray(true); copyB.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); return JSON.stringify(copyA) === JSON.stringify(copyB); } /** * Converts the session into an array of key-value pairs. */ public toPropertyArray( returnUserData = false, ): [string, string | number | boolean][] { return ( Object.entries(this) .filter( ([key, value]) => propertiesToSave.includes(key) && value !== undefined && value !== null, ) // Prepare values for db storage .flatMap(([key, value]): [string, string | number | boolean][] => { switch (key) { case 'expires': return [[key, value ? value.getTime() : undefined]]; case 'onlineAccessInfo': // eslint-disable-next-line no-negated-condition if (!returnUserData) { return [[key, value.associated_user.id]]; } else { return [ ['userId', value?.associated_user?.id], ['firstName', value?.associated_user?.first_name], ['lastName', value?.associated_user?.last_name], ['email', value?.associated_user?.email], ['locale', value?.associated_user?.locale], ['emailVerified', value?.associated_user?.email_verified], ['accountOwner', value?.associated_user?.account_owner], ['collaborator', value?.associated_user?.collaborator], ]; } default: return [[key, value]]; } }) // Filter out tuples with undefined values .filter(([_key, value]) => value !== undefined) ); } } - SessionParams: export interface SessionParams { /** * The unique identifier for the session. */ readonly id: string; /** * The Shopify shop domain. */ shop: string; /** * The state of the session. Used for the OAuth authentication code flow. */ state: string; /** * Whether the access token in the session is online or offline. */ isOnline: boolean; /** * The scopes for the access token. */ scope?: string; /** * The date the access token expires. */ expires?: Date; /** * The access token for the session. */ accessToken?: string; /** * Information on the user for the session. Only present for online sessions. */ onlineAccessInfo?: OnlineAccessInfo | StoredOnlineAccessInfo; /** * Additional properties of the session allowing for extension */ [key: string]: any; } Converts an object with data into a Session. ### equals value: `(other: Session) => boolean` - Session: export class Session { public static fromPropertyArray( entries: [string, string | number | boolean][], returnUserData = false, ): Session { if (!Array.isArray(entries)) { throw new InvalidSession( 'The parameter is not an array: a Session cannot be created from this object.', ); } const obj = Object.fromEntries( entries .filter(([_key, value]) => value !== null && value !== undefined) // Sanitize keys .map(([key, value]) => { switch (key.toLowerCase()) { case 'isonline': return ['isOnline', value]; case 'accesstoken': return ['accessToken', value]; case 'onlineaccessinfo': return ['onlineAccessInfo', value]; case 'userid': return ['userId', value]; case 'firstname': return ['firstName', value]; case 'lastname': return ['lastName', value]; case 'accountowner': return ['accountOwner', value]; case 'emailverified': return ['emailVerified', value]; default: return [key.toLowerCase(), value]; } }), ); const sessionData = {} as SessionParams; const onlineAccessInfo = { associated_user: {}, } as OnlineAccessInfo; Object.entries(obj).forEach(([key, value]) => { switch (key) { case 'isOnline': if (typeof value === 'string') { sessionData[key] = value.toString().toLowerCase() === 'true'; } else if (typeof value === 'number') { sessionData[key] = Boolean(value); } else { sessionData[key] = value; } break; case 'scope': sessionData[key] = value.toString(); break; case 'expires': sessionData[key] = value ? new Date(Number(value)) : undefined; break; case 'onlineAccessInfo': onlineAccessInfo.associated_user.id = Number(value); break; case 'userId': if (returnUserData) { onlineAccessInfo.associated_user.id = Number(value); break; } case 'firstName': if (returnUserData) { onlineAccessInfo.associated_user.first_name = String(value); break; } case 'lastName': if (returnUserData) { onlineAccessInfo.associated_user.last_name = String(value); break; } case 'email': if (returnUserData) { onlineAccessInfo.associated_user.email = String(value); break; } case 'accountOwner': if (returnUserData) { onlineAccessInfo.associated_user.account_owner = Boolean(value); break; } case 'locale': if (returnUserData) { onlineAccessInfo.associated_user.locale = String(value); break; } case 'collaborator': if (returnUserData) { onlineAccessInfo.associated_user.collaborator = Boolean(value); break; } case 'emailVerified': if (returnUserData) { onlineAccessInfo.associated_user.email_verified = Boolean(value); break; } // Return any user keys as passed in default: sessionData[key] = value; } }); if (sessionData.isOnline) { sessionData.onlineAccessInfo = onlineAccessInfo; } const session = new Session(sessionData); return session; } /** * The unique identifier for the session. */ readonly id: string; /** * The Shopify shop domain, such as `example.myshopify.com`. */ public shop: string; /** * The state of the session. Used for the OAuth authentication code flow. */ public state: string; /** * Whether the access token in the session is online or offline. */ public isOnline: boolean; /** * The desired scopes for the access token, at the time the session was created. */ public scope?: string; /** * The date the access token expires. */ public expires?: Date; /** * The access token for the session. */ public accessToken?: string; /** * Information on the user for the session. Only present for online sessions. */ public onlineAccessInfo?: OnlineAccessInfo; constructor(params: SessionParams) { Object.assign(this, params); } /** * Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. */ public isActive(scopes: AuthScopes | string | string[]): boolean { return ( !this.isScopeChanged(scopes) && Boolean(this.accessToken) && !this.isExpired() ); } /** * Whether the access token has the given scopes. */ public isScopeChanged(scopes: AuthScopes | string | string[]): boolean { const scopesObject = scopes instanceof AuthScopes ? scopes : new AuthScopes(scopes); return !scopesObject.equals(this.scope); } /** * Whether the access token is expired. */ public isExpired(withinMillisecondsOfExpiry = 0): boolean { return Boolean( this.expires && this.expires.getTime() - withinMillisecondsOfExpiry < Date.now(), ); } /** * Converts an object with data into a Session. */ public toObject(): SessionParams { const object: SessionParams = { id: this.id, shop: this.shop, state: this.state, isOnline: this.isOnline, }; if (this.scope) { object.scope = this.scope; } if (this.expires) { object.expires = this.expires; } if (this.accessToken) { object.accessToken = this.accessToken; } if (this.onlineAccessInfo) { object.onlineAccessInfo = this.onlineAccessInfo; } return object; } /** * Checks whether the given session is equal to this session. */ public equals(other: Session | undefined): boolean { if (!other) return false; const mandatoryPropsMatch = this.id === other.id && this.shop === other.shop && this.state === other.state && this.isOnline === other.isOnline; if (!mandatoryPropsMatch) return false; const copyA = this.toPropertyArray(true); copyA.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); const copyB = other.toPropertyArray(true); copyB.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); return JSON.stringify(copyA) === JSON.stringify(copyB); } /** * Converts the session into an array of key-value pairs. */ public toPropertyArray( returnUserData = false, ): [string, string | number | boolean][] { return ( Object.entries(this) .filter( ([key, value]) => propertiesToSave.includes(key) && value !== undefined && value !== null, ) // Prepare values for db storage .flatMap(([key, value]): [string, string | number | boolean][] => { switch (key) { case 'expires': return [[key, value ? value.getTime() : undefined]]; case 'onlineAccessInfo': // eslint-disable-next-line no-negated-condition if (!returnUserData) { return [[key, value.associated_user.id]]; } else { return [ ['userId', value?.associated_user?.id], ['firstName', value?.associated_user?.first_name], ['lastName', value?.associated_user?.last_name], ['email', value?.associated_user?.email], ['locale', value?.associated_user?.locale], ['emailVerified', value?.associated_user?.email_verified], ['accountOwner', value?.associated_user?.account_owner], ['collaborator', value?.associated_user?.collaborator], ]; } default: return [[key, value]]; } }) // Filter out tuples with undefined values .filter(([_key, value]) => value !== undefined) ); } } Checks whether the given session is equal to this session. ### toPropertyArray value: `(returnUserData?: boolean) => [string, string | number | boolean][]` Converts the session into an array of key-value pairs. ### OnlineAccessInfo ### expires_in value: `number` How long the access token is valid for, in seconds. ### associated_user_scope value: `string` The effective set of scopes for the session. ### associated_user value: `OnlineAccessUser` - OnlineAccessUser: export interface OnlineAccessUser { /** * The user's ID. */ id: number; /** * The user's first name. */ first_name: string; /** * The user's last name. */ last_name: string; /** * The user's email address. */ email: string; /** * Whether the user has verified their email address. */ email_verified: boolean; /** * Whether the user is the account owner. */ account_owner: boolean; /** * The user's locale. */ locale: string; /** * Whether the user is a collaborator. */ collaborator: boolean; } The user associated with the access token. ### OnlineAccessUser ### id value: `number` The user's ID. ### first_name value: `string` The user's first name. ### last_name value: `string` The user's last name. ### email value: `string` The user's email address. ### email_verified value: `boolean` Whether the user has verified their email address. ### account_owner value: `boolean` Whether the user is the account owner. ### locale value: `string` The user's locale. ### collaborator value: `boolean` Whether the user is a collaborator. ### AuthScopes A class that represents a set of access token scopes. ### has value: `(scope: string | string[] | AuthScopes) => boolean` - AuthScopes: class AuthScopes { public static SCOPE_DELIMITER = ','; private compressedScopes: Set; private expandedScopes: Set; constructor(scopes: string | string[] | AuthScopes | undefined) { let scopesArray: string[] = []; if (typeof scopes === 'string') { scopesArray = scopes.split( new RegExp(`${AuthScopes.SCOPE_DELIMITER}\\s*`), ); } else if (Array.isArray(scopes)) { scopesArray = scopes; } else if (scopes) { scopesArray = Array.from(scopes.expandedScopes); } scopesArray = scopesArray .map((scope) => scope.trim()) .filter((scope) => scope.length); const impliedScopes = this.getImpliedScopes(scopesArray); const scopeSet = new Set(scopesArray); const impliedSet = new Set(impliedScopes); this.compressedScopes = new Set( [...scopeSet].filter((x) => !impliedSet.has(x)), ); this.expandedScopes = new Set([...scopeSet, ...impliedSet]); } /** * Checks whether the current set of scopes includes the given one. */ public has(scope: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (scope instanceof AuthScopes) { other = scope; } else { other = new AuthScopes(scope); } return ( other.toArray().filter((x) => !this.expandedScopes.has(x)).length === 0 ); } /** * Checks whether the current set of scopes equals the given one. */ public equals(otherScopes: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (otherScopes instanceof AuthScopes) { other = otherScopes; } else { other = new AuthScopes(otherScopes); } return ( this.compressedScopes.size === other.compressedScopes.size && this.toArray().filter((x) => !other.has(x)).length === 0 ); } /** * Returns a comma-separated string with the current set of scopes. */ public toString() { return this.toArray().join(AuthScopes.SCOPE_DELIMITER); } /** * Returns an array with the current set of scopes. */ public toArray() { return [...this.compressedScopes]; } private getImpliedScopes(scopesArray: string[]): string[] { return scopesArray.reduce((array: string[], current: string) => { const matches = current.match(/^(unauthenticated_)?write_(.*)$/); if (matches) { array.push(`${matches[1] ? matches[1] : ''}read_${matches[2]}`); } return array; }, []); } } Checks whether the current set of scopes includes the given one. ### equals value: `(otherScopes: string | string[] | AuthScopes) => boolean` - AuthScopes: class AuthScopes { public static SCOPE_DELIMITER = ','; private compressedScopes: Set; private expandedScopes: Set; constructor(scopes: string | string[] | AuthScopes | undefined) { let scopesArray: string[] = []; if (typeof scopes === 'string') { scopesArray = scopes.split( new RegExp(`${AuthScopes.SCOPE_DELIMITER}\\s*`), ); } else if (Array.isArray(scopes)) { scopesArray = scopes; } else if (scopes) { scopesArray = Array.from(scopes.expandedScopes); } scopesArray = scopesArray .map((scope) => scope.trim()) .filter((scope) => scope.length); const impliedScopes = this.getImpliedScopes(scopesArray); const scopeSet = new Set(scopesArray); const impliedSet = new Set(impliedScopes); this.compressedScopes = new Set( [...scopeSet].filter((x) => !impliedSet.has(x)), ); this.expandedScopes = new Set([...scopeSet, ...impliedSet]); } /** * Checks whether the current set of scopes includes the given one. */ public has(scope: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (scope instanceof AuthScopes) { other = scope; } else { other = new AuthScopes(scope); } return ( other.toArray().filter((x) => !this.expandedScopes.has(x)).length === 0 ); } /** * Checks whether the current set of scopes equals the given one. */ public equals(otherScopes: string | string[] | AuthScopes | undefined) { let other: AuthScopes; if (otherScopes instanceof AuthScopes) { other = otherScopes; } else { other = new AuthScopes(otherScopes); } return ( this.compressedScopes.size === other.compressedScopes.size && this.toArray().filter((x) => !other.has(x)).length === 0 ); } /** * Returns a comma-separated string with the current set of scopes. */ public toString() { return this.toArray().join(AuthScopes.SCOPE_DELIMITER); } /** * Returns an array with the current set of scopes. */ public toArray() { return [...this.compressedScopes]; } private getImpliedScopes(scopesArray: string[]): string[] { return scopesArray.reduce((array: string[], current: string) => { const matches = current.match(/^(unauthenticated_)?write_(.*)$/); if (matches) { array.push(`${matches[1] ? matches[1] : ''}read_${matches[2]}`); } return array; }, []); } } Checks whether the current set of scopes equals the given one. ### toString value: `() => string` Returns a comma-separated string with the current set of scopes. ### toArray value: `() => any[]` Returns an array with the current set of scopes. ### SessionParams ### [key: string] value: `any` ### id value: `string` The unique identifier for the session. ### shop value: `string` The Shopify shop domain. ### state value: `string` The state of the session. Used for the OAuth authentication code flow. ### isOnline value: `boolean` Whether the access token in the session is online or offline. ### scope value: `string` The scopes for the access token. ### expires value: `Date` The date the access token expires. ### accessToken value: `string` The access token for the session. ### onlineAccessInfo value: `OnlineAccessInfo | StoredOnlineAccessInfo` - OnlineAccessInfo: export interface OnlineAccessInfo { /** * How long the access token is valid for, in seconds. */ expires_in: number; /** * The effective set of scopes for the session. */ associated_user_scope: string; /** * The user associated with the access token. */ associated_user: OnlineAccessUser; } - StoredOnlineAccessInfo: Omit & { associated_user: Partial; } Information on the user for the session. Only present for online sessions. ### AdminApiContext ### rest value: `RestClientWithResources` - RestClientWithResources: RemixRestClient & {resources: Resources} Methods for interacting with the Shopify Admin REST API There are methods for interacting with individual REST resources. You can also make `GET`, `POST`, `PUT` and `DELETE` requests should the REST resources not meet your needs. ### graphql value: `GraphQLClient` - GraphQLClient: export type GraphQLClient = < Operation extends keyof Operations, >( query: Operation, options?: GraphQLQueryOptions, ) => Promise>; Methods for interacting with the Shopify Admin GraphQL API ### RemixRestClient ### session value: `Session` - Session: export class Session { public static fromPropertyArray( entries: [string, string | number | boolean][], returnUserData = false, ): Session { if (!Array.isArray(entries)) { throw new InvalidSession( 'The parameter is not an array: a Session cannot be created from this object.', ); } const obj = Object.fromEntries( entries .filter(([_key, value]) => value !== null && value !== undefined) // Sanitize keys .map(([key, value]) => { switch (key.toLowerCase()) { case 'isonline': return ['isOnline', value]; case 'accesstoken': return ['accessToken', value]; case 'onlineaccessinfo': return ['onlineAccessInfo', value]; case 'userid': return ['userId', value]; case 'firstname': return ['firstName', value]; case 'lastname': return ['lastName', value]; case 'accountowner': return ['accountOwner', value]; case 'emailverified': return ['emailVerified', value]; default: return [key.toLowerCase(), value]; } }), ); const sessionData = {} as SessionParams; const onlineAccessInfo = { associated_user: {}, } as OnlineAccessInfo; Object.entries(obj).forEach(([key, value]) => { switch (key) { case 'isOnline': if (typeof value === 'string') { sessionData[key] = value.toString().toLowerCase() === 'true'; } else if (typeof value === 'number') { sessionData[key] = Boolean(value); } else { sessionData[key] = value; } break; case 'scope': sessionData[key] = value.toString(); break; case 'expires': sessionData[key] = value ? new Date(Number(value)) : undefined; break; case 'onlineAccessInfo': onlineAccessInfo.associated_user.id = Number(value); break; case 'userId': if (returnUserData) { onlineAccessInfo.associated_user.id = Number(value); break; } case 'firstName': if (returnUserData) { onlineAccessInfo.associated_user.first_name = String(value); break; } case 'lastName': if (returnUserData) { onlineAccessInfo.associated_user.last_name = String(value); break; } case 'email': if (returnUserData) { onlineAccessInfo.associated_user.email = String(value); break; } case 'accountOwner': if (returnUserData) { onlineAccessInfo.associated_user.account_owner = Boolean(value); break; } case 'locale': if (returnUserData) { onlineAccessInfo.associated_user.locale = String(value); break; } case 'collaborator': if (returnUserData) { onlineAccessInfo.associated_user.collaborator = Boolean(value); break; } case 'emailVerified': if (returnUserData) { onlineAccessInfo.associated_user.email_verified = Boolean(value); break; } // Return any user keys as passed in default: sessionData[key] = value; } }); if (sessionData.isOnline) { sessionData.onlineAccessInfo = onlineAccessInfo; } const session = new Session(sessionData); return session; } /** * The unique identifier for the session. */ readonly id: string; /** * The Shopify shop domain, such as `example.myshopify.com`. */ public shop: string; /** * The state of the session. Used for the OAuth authentication code flow. */ public state: string; /** * Whether the access token in the session is online or offline. */ public isOnline: boolean; /** * The desired scopes for the access token, at the time the session was created. */ public scope?: string; /** * The date the access token expires. */ public expires?: Date; /** * The access token for the session. */ public accessToken?: string; /** * Information on the user for the session. Only present for online sessions. */ public onlineAccessInfo?: OnlineAccessInfo; constructor(params: SessionParams) { Object.assign(this, params); } /** * Whether the session is active. Active sessions have an access token that is not expired, and has the given scopes. */ public isActive(scopes: AuthScopes | string | string[]): boolean { return ( !this.isScopeChanged(scopes) && Boolean(this.accessToken) && !this.isExpired() ); } /** * Whether the access token has the given scopes. */ public isScopeChanged(scopes: AuthScopes | string | string[]): boolean { const scopesObject = scopes instanceof AuthScopes ? scopes : new AuthScopes(scopes); return !scopesObject.equals(this.scope); } /** * Whether the access token is expired. */ public isExpired(withinMillisecondsOfExpiry = 0): boolean { return Boolean( this.expires && this.expires.getTime() - withinMillisecondsOfExpiry < Date.now(), ); } /** * Converts an object with data into a Session. */ public toObject(): SessionParams { const object: SessionParams = { id: this.id, shop: this.shop, state: this.state, isOnline: this.isOnline, }; if (this.scope) { object.scope = this.scope; } if (this.expires) { object.expires = this.expires; } if (this.accessToken) { object.accessToken = this.accessToken; } if (this.onlineAccessInfo) { object.onlineAccessInfo = this.onlineAccessInfo; } return object; } /** * Checks whether the given session is equal to this session. */ public equals(other: Session | undefined): boolean { if (!other) return false; const mandatoryPropsMatch = this.id === other.id && this.shop === other.shop && this.state === other.state && this.isOnline === other.isOnline; if (!mandatoryPropsMatch) return false; const copyA = this.toPropertyArray(true); copyA.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); const copyB = other.toPropertyArray(true); copyB.sort(([k1], [k2]) => (k1 < k2 ? -1 : 1)); return JSON.stringify(copyA) === JSON.stringify(copyB); } /** * Converts the session into an array of key-value pairs. */ public toPropertyArray( returnUserData = false, ): [string, string | number | boolean][] { return ( Object.entries(this) .filter( ([key, value]) => propertiesToSave.includes(key) && value !== undefined && value !== null, ) // Prepare values for db storage .flatMap(([key, value]): [string, string | number | boolean][] => { switch (key) { case 'expires': return [[key, value ? value.getTime() : undefined]]; case 'onlineAccessInfo': // eslint-disable-next-line no-negated-condition if (!returnUserData) { return [[key, value.associated_user.id]]; } else { return [ ['userId', value?.associated_user?.id], ['firstName', value?.associated_user?.first_name], ['lastName', value?.associated_user?.last_name], ['email', value?.associated_user?.email], ['locale', value?.associated_user?.locale], ['emailVerified', value?.associated_user?.email_verified], ['accountOwner', value?.associated_user?.account_owner], ['collaborator', value?.associated_user?.collaborator], ]; } default: return [[key, value]]; } }) // Filter out tuples with undefined values .filter(([_key, value]) => value !== undefined) ); } } ### get value: `(params: GetRequestParams) => Promise` - GetRequestParams: export interface GetRequestParams { /** * The path to the resource, relative to the API version root. */ path: string; /** * The type of data expected in the response. */ type?: DataType; /** * The request body. */ data?: Record | string; /** * Query parameters to be sent with the request. */ query?: SearchParams; /** * Additional headers to be sent with the request. */ extraHeaders?: HeaderParams; /** * The maximum number of times the request can be made if it fails with a throttling or server error. */ tries?: number; } Performs a GET request on the given path. ### post value: `(params: PostRequestParams) => Promise` - PostRequestParams: GetRequestParams & { data: Record | string; } Performs a POST request on the given path. ### put value: `(params: PostRequestParams) => Promise` - PostRequestParams: GetRequestParams & { data: Record | string; } Performs a PUT request on the given path. ### delete value: `(params: GetRequestParams) => Promise` - GetRequestParams: export interface GetRequestParams { /** * The path to the resource, relative to the API version root. */ path: string; /** * The type of data expected in the response. */ type?: DataType; /** * The request body. */ data?: Record | string; /** * Query parameters to be sent with the request. */ query?: SearchParams; /** * Additional headers to be sent with the request. */ extraHeaders?: HeaderParams; /** * The maximum number of times the request can be made if it fails with a throttling or server error. */ tries?: number; } Performs a DELETE request on the given path. ### GetRequestParams ### path value: `string` The path to the resource, relative to the API version root. ### type value: `DataType` - DataType: export enum DataType { JSON = 'application/json', GraphQL = 'application/graphql', URLEncoded = 'application/x-www-form-urlencoded', } The type of data expected in the response. ### data value: `string | Record` The request body. ### query value: `SearchParams` Query parameters to be sent with the request. ### extraHeaders value: `HeaderParams` - HeaderParams: Record Additional headers to be sent with the request. ### tries value: `number` The maximum number of times the request can be made if it fails with a throttling or server error. ### DataType ### JSON value: `application/json` ### GraphQL value: `application/graphql` ### URLEncoded value: `application/x-www-form-urlencoded` ### GraphQLClient #### Returns: interface Promise { /** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of which ever callback is executed. */ then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; /** * Attaches a callback for only the rejection of the Promise. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of the callback. */ catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; }, interface Promise {}, Promise: PromiseConstructor, interface Promise { readonly [Symbol.toStringTag]: string; }, interface Promise { /** * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The * resolved value cannot be modified from the callback. * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). * @returns A Promise for the completion of the callback. */ finally(onfinally?: (() => void) | undefined | null): Promise; } #### Params: - query: Operation extends keyof Operations - options: GraphQLQueryOptions export type GraphQLClient = < Operation extends keyof Operations, >( query: Operation, options?: GraphQLQueryOptions, ) => Promise>; ### GraphQLQueryOptions ### variables value: `ApiClientRequestOptions["variables"]` - Options: interface Options { /** * Whether to use the shop's theme layout around the Liquid content. */ layout?: boolean; } The variables to pass to the operation. ### apiVersion value: `ApiVersion` - ApiVersion: export enum ApiVersion { October22 = '2022-10', January23 = '2023-01', April23 = '2023-04', July23 = '2023-07', October23 = '2023-10', January24 = '2024-01', April24 = '2024-04', Unstable = 'unstable', } The version of the API to use for the request. ### headers value: `Record` Additional headers to include in the request. ### tries value: `number` The total number of times to try the request if it fails. ### ApiVersion ### October22 value: `2022-10` ### January23 value: `2023-01` ### April23 value: `2023-04` ### July23 value: `2023-07` ### October23 value: `2023-10` ### January24 value: `2024-01` ### April24 value: `2024-04` ### Unstable value: `unstable` ### StorefrontContext ### graphql value: `GraphQLClient` - GraphQLClient: export type GraphQLClient = < Operation extends keyof Operations, >( query: Operation, options?: GraphQLQueryOptions, ) => Promise>; Method for interacting with the Shopify Storefront GraphQL API If you're getting incorrect type hints in the Shopify template, follow [these instructions](https://github.com/Shopify/shopify-app-template-remix/tree/main#incorrect-graphql-hints). ## Related - [Admin API context](https://shopify.dev/docs/api/shopify-app-remix/apis/admin-api) - [Storefront API context](https://shopify.dev/docs/api/shopify-app-remix/apis/storefront-api) - [Liquid reference](https://shopify.dev/docs/api/liquid) ## Examples [App proxies](https://shopify.dev/docs/apps/online-store/app-proxies) take requests to Shopify links, and redirect them to external links. The `authenticate.public.appProxy` function validates requests made to app proxies, and returns a context to enable querying Shopify APIs. > Note: If the store has not installed the app, store-related properties such as `admin` or `storefront` will be `undefined` ### session Get the session for the shop that initiated the request to the app proxy.```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import { getMyAppModelData } from "~/db/model.server"; export const loader = async ({ request }) => { // Get the session for the shop that initiated the request to the app proxy. const { session } = await authenticate.public.appProxy(request); // Use the session data to make to queries to your database or additional requests. return json( await getMyAppModelData({shop: session.shop}) ); }; ``` ### admin Use the `admin` object to interact with the REST or GraphQL APIs.```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export async function action({ request }: ActionFunctionArgs) { const { admin } = await authenticate.public.appProxy(request); const response = await admin.graphql( `#graphql mutation populateProduct($input: ProductInput!) { productCreate(input: $input) { product { id } } }`, { variables: { input: { title: "Product Name" } } } ); const productData = await response.json(); return json({ data: productData.data }); } ``` ### storefront Use the `storefront` object to interact with the GraphQL API.```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export async function action({ request }: ActionFunctionArgs) { const { storefront } = await authenticate.public.appProxy(request); const response = await storefront.graphql( `#graphql query blogIds { blogs(first: 10) { edges { node { id } } } }` ); return json(await response.json()); } ``` ### liquid Use the `liquid` helper to render a `Response` with Liquid content using the shop's theme. See the [Liquid reference](https://shopify.dev/docs/api/liquid) for all the features it enables.```typescript import {authenticate} from "~/shopify.server" export async function loader({ request }) { const {liquid} = await authenticate.public.appProxy(request); return liquid("Hello {{shop.name}}"); } ``` Set the `layout` option to `false` to render the Liquid content without a theme.```typescript import {authenticate} from "~/shopify.server" export async function loader({ request }) { const {liquid} = await authenticate.public.appProxy(request); return liquid( "Hello {{shop.name}}", { layout: false } ); } ``` Handle form submissions through an app proxy.```typescript import { redirect } from "@remix-run/node"; import { authenticate } from "~/shopify.server"; export async function loader({ request }) { const { liquid } = await authenticate.public.appProxy(request); return liquid(`
`); } export async function action({ request }) { await authenticate.public.appProxy(request); const formData = await request.formData(); const field = formData.get("field")?.toString(); // Perform actions here if (field) { console.log("Field:", field); } // Return to the form page return redirect("/apps/proxy/my-action"); } ``` ## Examples [App proxies](https://shopify.dev/docs/apps/online-store/app-proxies) take requests to Shopify links, and redirect them to external links. The `authenticate.public.appProxy` function validates requests made to app proxies, and returns a context to enable querying Shopify APIs. > Note: If the store has not installed the app, store-related properties such as `admin` or `storefront` will be `undefined` ### session Get the session for the shop that initiated the request to the app proxy.```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import { getMyAppModelData } from "~/db/model.server"; export const loader = async ({ request }) => { // Get the session for the shop that initiated the request to the app proxy. const { session } = await authenticate.public.appProxy(request); // Use the session data to make to queries to your database or additional requests. return json( await getMyAppModelData({shop: session.shop}) ); }; ``` ### admin Use the `admin` object to interact with the REST or GraphQL APIs.```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export async function action({ request }: ActionFunctionArgs) { const { admin } = await authenticate.public.appProxy(request); const response = await admin.graphql( `#graphql mutation populateProduct($input: ProductInput!) { productCreate(input: $input) { product { id } } }`, { variables: { input: { title: "Product Name" } } } ); const productData = await response.json(); return json({ data: productData.data }); } ``` ### storefront Use the `storefront` object to interact with the GraphQL API.```typescript import { json } from "@remix-run/node"; import { authenticate } from "../shopify.server"; export async function action({ request }: ActionFunctionArgs) { const { storefront } = await authenticate.public.appProxy(request); const response = await storefront.graphql( `#graphql query blogIds { blogs(first: 10) { edges { node { id } } } }` ); return json(await response.json()); } ``` ### liquid Use the `liquid` helper to render a `Response` with Liquid content using the shop's theme. See the [Liquid reference](https://shopify.dev/docs/api/liquid) for all the features it enables.```typescript import {authenticate} from "~/shopify.server" export async function loader({ request }) { const {liquid} = await authenticate.public.appProxy(request); return liquid("Hello {{shop.name}}"); } ``` Set the `layout` option to `false` to render the Liquid content without a theme.```typescript import {authenticate} from "~/shopify.server" export async function loader({ request }) { const {liquid} = await authenticate.public.appProxy(request); return liquid( "Hello {{shop.name}}", { layout: false } ); } ``` Handle form submissions through an app proxy.```typescript import { redirect } from "@remix-run/node"; import { authenticate } from "~/shopify.server"; export async function loader({ request }) { const { liquid } = await authenticate.public.appProxy(request); return liquid(`
`); } export async function action({ request }) { await authenticate.public.appProxy(request); const formData = await request.formData(); const field = formData.get("field")?.toString(); // Perform actions here if (field) { console.log("Field:", field); } // Return to the form page return redirect("/apps/proxy/my-action"); } ```