├── README.md
├── example
├── account
│ └── use-customer.tsx
├── api
│ ├── cart
│ │ ├── handlers
│ │ │ ├── add-item.ts
│ │ │ ├── get-cart.ts
│ │ │ ├── remove-item.ts
│ │ │ └── update-item.ts
│ │ └── index.ts
│ ├── catalog
│ │ ├── handlers
│ │ │ └── get-products.ts
│ │ └── products.ts
│ ├── checkout.ts
│ ├── customers
│ │ ├── handlers
│ │ │ ├── get-logged-in-customer.ts
│ │ │ ├── login.ts
│ │ │ ├── logout.ts
│ │ │ └── signup.ts
│ │ ├── index.ts
│ │ ├── login.ts
│ │ ├── logout.ts
│ │ └── signup.ts
│ ├── definitions
│ │ ├── catalog.ts
│ │ ├── store-content.ts
│ │ └── wishlist.ts
│ ├── fragments
│ │ ├── category-tree.ts
│ │ └── product.ts
│ ├── index.ts
│ ├── operations
│ │ ├── get-all-pages.ts
│ │ ├── get-all-product-paths.ts
│ │ ├── get-all-products.ts
│ │ ├── get-customer-id.ts
│ │ ├── get-customer-wishlist.ts
│ │ ├── get-page.ts
│ │ ├── get-product.ts
│ │ ├── get-site-info.ts
│ │ └── login.ts
│ ├── utils
│ │ ├── concat-cookie.ts
│ │ ├── create-api-handler.ts
│ │ ├── errors.ts
│ │ ├── fetch-graphql-api.ts
│ │ ├── fetch-store-api.ts
│ │ ├── fetch.ts
│ │ ├── filter-edges.ts
│ │ ├── get-cart-cookie.ts
│ │ ├── is-allowed-method.ts
│ │ ├── parse-item.ts
│ │ ├── set-product-locale-meta.ts
│ │ └── types.ts
│ └── wishlist
│ │ ├── handlers
│ │ ├── add-item.ts
│ │ ├── get-wishlist.ts
│ │ └── remove-item.ts
│ │ └── index.ts
├── auth
│ ├── use-login.tsx
│ ├── use-logout.tsx
│ └── use-signup.tsx
├── cart
│ ├── use-add-item.tsx
│ ├── use-cart-actions.tsx
│ ├── use-cart.tsx
│ ├── use-remove-item.tsx
│ └── use-update-item.tsx
├── index.tsx
├── products
│ ├── use-price.tsx
│ └── use-search.tsx
├── schema.d.ts
├── schema.graphql
├── scripts
│ └── generate-definitions.js
├── src
│ ├── api
│ │ └── index.ts
│ ├── cart
│ │ ├── use-add-item.tsx
│ │ ├── use-cart-actions.tsx
│ │ ├── use-cart.tsx
│ │ ├── use-remove-item.tsx
│ │ └── use-update-item.tsx
│ ├── index.tsx
│ ├── products
│ │ └── use-search.tsx
│ ├── use-customer.tsx
│ ├── use-login.tsx
│ ├── use-logout.tsx
│ ├── use-price.tsx
│ ├── use-signup.tsx
│ ├── utils
│ │ ├── errors.ts
│ │ ├── types.ts
│ │ ├── use-action.tsx
│ │ └── use-data.tsx
│ └── wishlist
│ │ ├── use-add-item.tsx
│ │ ├── use-remove-item.tsx
│ │ └── use-wishlist.tsx
└── wishlist
│ ├── use-add-item.tsx
│ ├── use-remove-item.tsx
│ ├── use-wishlist-actions.tsx
│ └── use-wishlist.tsx
├── package.json
├── src
└── commerce
│ ├── api
│ └── index.ts
│ ├── cart
│ ├── use-add-item.tsx
│ ├── use-cart-actions.tsx
│ ├── use-cart.tsx
│ ├── use-remove-item.tsx
│ └── use-update-item.tsx
│ ├── index.tsx
│ ├── products
│ └── use-search.tsx
│ ├── use-customer.tsx
│ ├── use-login.tsx
│ ├── use-logout.tsx
│ ├── use-price.tsx
│ ├── use-signup.tsx
│ ├── utils
│ ├── errors.ts
│ ├── types.ts
│ ├── use-action.tsx
│ └── use-data.tsx
│ └── wishlist
│ ├── use-add-item.tsx
│ ├── use-remove-item.tsx
│ └── use-wishlist.tsx
└── yarn.lock
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js Commerce Framework
2 |
3 | This repository serves as a model to build your own hooks for [Next.js Commerce](https://nextjs.org/commerce). We encourage contributors and teams to build their own hooks under a separate package with the corresponding configuration. An example of using this repository as a seed are the hooks we built with [BigCommerce](https://github.com/bigcommerce/storefront-data-hooks).
4 |
5 | ## Folder Structure
6 |
7 | The root folder contains an `index.tsx` file with the CommerceProvider to set up the right commerce context. Additional components needed to run the framework need to be there.
8 |
9 | `src` contains all the handlers used by the whole framework. The rest of the folders in the root file are named by functionality, e.g cart, auth, wishlist and are the API of the framework. Therefore, most of the work should be done in the `src` folder. Otherwise, it could affect the usage of the framework altering its functionality and possibly adding unexpected output.
10 |
11 | ## Commerce API
12 |
13 | ### CommerceProvider
14 |
15 | The main configuration provider that creates the Commerce context. Used by all Commerce hooks. Every provider may define its own defaults.
16 |
17 | ```jsx
18 | import { CommerceProvider } from "@commerce-framework";
19 |
20 | const App = ({ locale = "en-US", children }) => (
21 | {children}
22 | );
23 | ```
24 |
25 | - `locale:` Locale to use for i18n. This is **required**.
26 | - `cartCookie:` Name of the cookie that saves the cart id. This is **optional**.
27 | - `fetcher:` Global fetcher function that will be used for every API call to a GraphQL or REST endpoint in the browser, it's in charge of error management on network errors. Usage of this config is **optional**.
28 |
29 | > **CommerceProvider** should not re-render unless a prop changes, and the only prop that's expected to change in most apps is `locale`.
30 |
31 | ### useCommerce()
32 |
33 | Returns the configs (including defaults) that are defined in the nearest `CommerceProvider`.
34 |
35 | ```jsx
36 | import { useCommerce } from "@commerce-framework/hooks";
37 |
38 | const commerce = useCommerce();
39 | ```
40 |
41 | May be useful if you want to create another `CommerceProvider` and extend its options.
42 |
43 | ### useCustomer
44 |
45 | Returns the logged-in customer and its data.
46 |
47 | ```jsx
48 | import { useCustomer } from "@commerce-framework/hooks";
49 |
50 | const customer = useCustomer();
51 | const { data, error, loading } = customer;
52 |
53 | if (error) return
There was an error: {error.message}
;
54 | if (loading) return Loading...
;
55 |
56 | return ;
57 | ```
58 |
59 | The customer is revalidated by operations executed by `useLogin`, `useSignup` and `useLogout`.
60 |
61 | ### useLogin Hook
62 |
63 | Returns a function that allows a visitor to login into its customer account.
64 |
65 | ```jsx
66 | import { useLogin } from "@commerce-framework/hooks";
67 |
68 | const LoginView = () => {
69 | const login = useLogin();
70 |
71 | const handleLogin = async () => {
72 | await login({
73 | email,
74 | password,
75 | });
76 | };
77 |
78 | return ;
79 | };
80 | ```
81 |
82 | ### useLogout
83 |
84 | Returns a function that allows the current customer to log out.
85 |
86 | ```jsx
87 | import { useLogout } from "@commerce-framework/hooks";
88 |
89 | const LogoutLink = () => {
90 | const logout = useLogout();
91 | return logout()}>Logout;
92 | };
93 | ```
94 |
95 | ### useSignup
96 |
97 | Returns a function that allows a visitor to sign up into the store.
98 | The signup operation returns `null`.
99 |
100 | ```jsx
101 | import { useSignup } from "@commerce-framework/hooks";
102 |
103 | const SignupView = () => {
104 | const signup = useSignup();
105 |
106 | const handleSignup = async () => {
107 | await signup({
108 | email,
109 | firstName,
110 | lastName,
111 | password,
112 | });
113 | };
114 |
115 | return ;
116 | };
117 | ```
118 |
119 | ### usePrice
120 |
121 | Formats an amount--usually the price of a product-- into an internationalized string. It uses the current locale.
122 |
123 | ```jsx
124 | import { usePrice } from "@commerce-framework/hooks";
125 |
126 | const { price, basePrice, discount } = usePrice({
127 | amount: 100,
128 | baseAmount: 50,
129 | currencyCode: "USD",
130 | });
131 | ```
132 |
133 | `usePrice` receives an object with:
134 |
135 | - `amount:` A valid number, usually the sale price of a product. This is **required**.
136 | - `baseAmount:` A valid number, usually the listed price of a product. If it's higher than `amount` then the product is on sale and `usePrice` will return the discounted percentage. This is **optional**.
137 | - `currencyCode:` The currency code to use. This is **required**.
138 |
139 | `usePrice` returns an object with:
140 |
141 | - `price:` The formatted price of a product.
142 | - `basePrice:` The formatted base price of the product. Only present if `baseAmount` is set.
143 | - `discount:` The discounted percentage. Only present if `baseAmount` is set.
144 |
145 | ## Cart Hooks
146 |
147 | ### useCart
148 |
149 | Returns the current cart data.
150 |
151 | ```jsx
152 | import { useCart } from "@commerce-framework/hooks";
153 |
154 | const { data, error, isEmpty, loading } = useCart();
155 | ```
156 |
157 | ### useAddItem
158 |
159 | Returns a function that when called adds a new item to the current cart.
160 | The `addItem` operation returns the updated cart.
161 |
162 | ```jsx
163 | import { useAddItem } from "@commerce-framework/hooks";
164 |
165 | const AddToCartButton = ({ productId, variantId }) => {
166 | const addItem = useAddItem();
167 |
168 | const addToCart = async () => {
169 | await addItem({
170 | productId,
171 | variantId,
172 | });
173 | };
174 |
175 | return ;
176 | };
177 | ```
178 |
179 | ### useUpdateItem
180 |
181 | Returns a function to update an item of the current cart.
182 |
183 | ```jsx
184 | const updateItem = useUpdateItem();
185 |
186 | await updateItem({
187 | id,
188 | productId,
189 | variantId,
190 | quantity,
191 | });
192 |
193 | // You can optionally send the item to the hook:
194 | const updateItem = useUpdateItem(item);
195 |
196 | // And now only the quantity is required
197 | await updateItem({
198 | quantity,
199 | });
200 | ```
201 |
202 | ### useRemoveItem
203 |
204 | Returns a function that when called removes an item from the current cart.
205 |
206 | ```jsx
207 | const removeItem = useRemoveItem();
208 |
209 | await removeItem({
210 | id,
211 | });
212 | ```
213 |
214 | The `removeItem` operation returns the updated cart.
215 |
216 | The cart is deleted if the last remaining item is removed.
217 |
218 | ## Wishlist Hooks
219 |
220 | ### useWishlist
221 |
222 | Returns the current wishlist of the logged in user.
223 |
224 | ```jsx
225 | import { useWishlist } from "@commerce-framework/hooks";
226 |
227 | const { data } = useWishlist();
228 | ```
229 |
230 | ## WIP
231 |
232 | - getItem
233 | - getAllItems
234 | - useSearch
235 |
236 | ## Handling errors and loading states
237 |
238 | Data fetching looks like `useCart` and `useCustomer` return props to handle errors and loading states. For example, using the `useCommerce` hook:
239 |
240 | ```jsx
241 | const customer = useCustomer();
242 | const { data, error, loading } = customer;
243 |
244 | if (error) return There was an error: {error.message}
;
245 | if (loading) return Loading...
;
246 |
247 | return ;
248 | ```
249 |
250 | And using the `useCart` hook:
251 |
252 | ```jsx
253 | const cart = useCart();
254 | const { data, error, loading, isEmpty } = cart;
255 |
256 | if (error) return There was an error: {error.message}
;
257 | if (loading) return Loading...
;
258 | if (isEmpty) return The cart is currently empty
;
259 |
260 | return ;
261 | ```
262 |
263 | Hooks that execute user actions are async operations, you can track the loading state and errors using state hooks and try...catch. For example:
264 |
265 | ```jsx
266 | function LoginButton({ data }) {
267 | const login = useLogin();
268 |
269 | const [loading, setLoading] = useState(false);
270 | const [errorMsg, setErrorMsg] = useState(false);
271 | const handleClick = async () => {
272 | // We're about to run the action, so set the state to loading
273 | setLoading(true);
274 | // Reset the error message if it failed before
275 | setErrorMsg("");
276 |
277 | try {
278 | await login({
279 | email: data.email,
280 | password: data.password,
281 | });
282 | } catch (error) {
283 | // Handle error codes specific for the `login` action
284 | if (error.code === "invalid_credentials") {
285 | setErrorMsg(
286 | "Cannot find an account that matches the provided credentials"
287 | );
288 | } else {
289 | setErrorMsg(error.message);
290 | }
291 | } finally {
292 | setLoading(false);
293 | }
294 | };
295 |
296 | return (
297 |
298 |
301 | {errorMsg &&
{errorMsg}
}
302 |
303 | );
304 | }
305 | ```
306 |
307 | Error codes depend on the hook that is being used. In this case `invalid_credentials` is used by `useLogin`.
308 |
309 | All commerce related errors are an instance of `CommerceError`.
310 |
311 | ## Extending UI Hooks
312 |
313 | All hooks that involve a fetch operation can be extended with a custom fetcher function. For example, if we wanted to extend the `useCart` hook:
314 |
315 | ```jsx
316 | import useCartHook, { fetcher } from "commerce-lib/cart/use-cart";
317 |
318 | const useCart = useCartHook.extend(
319 | (options, input, fetch) => {
320 | // Do something different
321 | return fetcher(options, input, fetch);
322 | },
323 | {
324 | // Optionally change the default SWR options
325 | revalidateOnFocus: true,
326 | }
327 | );
328 |
329 | // Then in your component, using your new hook works in the same way
330 | const { data, error, isEmpty, updating } = useCart();
331 | ```
332 |
333 | The `extend` method is available for all hooks with fetch operations, it receives a fetcher function as the first parameter and SWR options as the second parameter if the hook uses SWR. `extend` returns a new hook that works exactly as the hook it's extending from.
334 |
335 | The fetcher function receives the following arguments:
336 |
337 | - `options:` This is an object that may have a `query` (if the hook is using GraphQL), a `url`, and a `method`.
338 | - `input:` An object with the data that's required to execute the operation.
339 | - `fetch:` The fetch function that is set by the `CommerceProvider`.
340 |
341 |
342 |
--------------------------------------------------------------------------------
/example /account/use-customer.tsx:
--------------------------------------------------------------------------------
1 | import type { HookFetcher } from '../src/utils/types'
2 | import type { SwrOptions } from '../src/utils/use-data'
3 | import useCommerceCustomer from '../src/use-customer'
4 | import type { Customer, CustomerData } from '../api/customers'
5 |
6 | const defaultOpts = {
7 | url: '/api/bigcommerce/customers',
8 | method: 'GET',
9 | }
10 |
11 | export type { Customer }
12 |
13 | export const fetcher: HookFetcher = async (
14 | options,
15 | _,
16 | fetch
17 | ) => {
18 | const data = await fetch({ ...defaultOpts, ...options })
19 | return data?.customer ?? null
20 | }
21 |
22 | export function extendHook(
23 | customFetcher: typeof fetcher,
24 | swrOptions?: SwrOptions
25 | ) {
26 | const useCustomer = () => {
27 | return useCommerceCustomer(defaultOpts, [], customFetcher, {
28 | revalidateOnFocus: false,
29 | ...swrOptions,
30 | })
31 | }
32 |
33 | useCustomer.extend = extendHook
34 |
35 | return useCustomer
36 | }
37 |
38 | export default extendHook(fetcher)
39 |
--------------------------------------------------------------------------------
/example /api/cart/handlers/add-item.ts:
--------------------------------------------------------------------------------
1 | import { parseCartItem } from '../../utils/parse-item'
2 | import getCartCookie from '../../utils/get-cart-cookie'
3 | import type { CartHandlers } from '..'
4 |
5 | // Return current cart info
6 | const addItem: CartHandlers['addItem'] = async ({
7 | res,
8 | body: { cartId, item },
9 | config,
10 | }) => {
11 | if (!item) {
12 | return res.status(400).json({
13 | data: null,
14 | errors: [{ message: 'Missing item' }],
15 | })
16 | }
17 | if (!item.quantity) item.quantity = 1
18 |
19 | const options = {
20 | method: 'POST',
21 | body: JSON.stringify({
22 | line_items: [parseCartItem(item)],
23 | ...(!cartId && config.storeChannelId
24 | ? { channel_id: config.storeChannelId }
25 | : {}),
26 | }),
27 | }
28 | const { data } = cartId
29 | ? await config.storeApiFetch(`/v3/carts/${cartId}/items`, options)
30 | : await config.storeApiFetch('/v3/carts', options)
31 |
32 | // Create or update the cart cookie
33 | res.setHeader(
34 | 'Set-Cookie',
35 | getCartCookie(config.cartCookie, data.id, config.cartCookieMaxAge)
36 | )
37 | res.status(200).json({ data })
38 | }
39 |
40 | export default addItem
41 |
--------------------------------------------------------------------------------
/example /api/cart/handlers/get-cart.ts:
--------------------------------------------------------------------------------
1 | import { BigcommerceApiError } from '../../utils/errors'
2 | import getCartCookie from '../../utils/get-cart-cookie'
3 | import type { Cart, CartHandlers } from '..'
4 |
5 | // Return current cart info
6 | const getCart: CartHandlers['getCart'] = async ({
7 | res,
8 | body: { cartId },
9 | config,
10 | }) => {
11 | let result: { data?: Cart } = {}
12 |
13 | if (cartId) {
14 | try {
15 | result = await config.storeApiFetch(`/v3/carts/${cartId}`)
16 | } catch (error) {
17 | if (error instanceof BigcommerceApiError && error.status === 404) {
18 | // Remove the cookie if it exists but the cart wasn't found
19 | res.setHeader('Set-Cookie', getCartCookie(config.cartCookie))
20 | } else {
21 | throw error
22 | }
23 | }
24 | }
25 |
26 | res.status(200).json({ data: result.data ?? null })
27 | }
28 |
29 | export default getCart
30 |
--------------------------------------------------------------------------------
/example /api/cart/handlers/remove-item.ts:
--------------------------------------------------------------------------------
1 | import getCartCookie from '../../utils/get-cart-cookie'
2 | import type { CartHandlers } from '..'
3 |
4 | // Return current cart info
5 | const removeItem: CartHandlers['removeItem'] = async ({
6 | res,
7 | body: { cartId, itemId },
8 | config,
9 | }) => {
10 | if (!cartId || !itemId) {
11 | return res.status(400).json({
12 | data: null,
13 | errors: [{ message: 'Invalid request' }],
14 | })
15 | }
16 |
17 | const result = await config.storeApiFetch<{ data: any } | null>(
18 | `/v3/carts/${cartId}/items/${itemId}`,
19 | { method: 'DELETE' }
20 | )
21 | const data = result?.data ?? null
22 |
23 | res.setHeader(
24 | 'Set-Cookie',
25 | data
26 | ? // Update the cart cookie
27 | getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
28 | : // Remove the cart cookie if the cart was removed (empty items)
29 | getCartCookie(config.cartCookie)
30 | )
31 | res.status(200).json({ data })
32 | }
33 |
34 | export default removeItem
35 |
--------------------------------------------------------------------------------
/example /api/cart/handlers/update-item.ts:
--------------------------------------------------------------------------------
1 | import { parseCartItem } from '../../utils/parse-item'
2 | import getCartCookie from '../../utils/get-cart-cookie'
3 | import type { CartHandlers } from '..'
4 |
5 | // Return current cart info
6 | const updateItem: CartHandlers['updateItem'] = async ({
7 | res,
8 | body: { cartId, itemId, item },
9 | config,
10 | }) => {
11 | if (!cartId || !itemId || !item) {
12 | return res.status(400).json({
13 | data: null,
14 | errors: [{ message: 'Invalid request' }],
15 | })
16 | }
17 |
18 | const { data } = await config.storeApiFetch(
19 | `/v3/carts/${cartId}/items/${itemId}`,
20 | {
21 | method: 'PUT',
22 | body: JSON.stringify({
23 | line_item: parseCartItem(item),
24 | }),
25 | }
26 | )
27 |
28 | // Update the cart cookie
29 | res.setHeader(
30 | 'Set-Cookie',
31 | getCartCookie(config.cartCookie, cartId, config.cartCookieMaxAge)
32 | )
33 | res.status(200).json({ data })
34 | }
35 |
36 | export default updateItem
37 |
--------------------------------------------------------------------------------
/example /api/cart/index.ts:
--------------------------------------------------------------------------------
1 | import isAllowedMethod from '../utils/is-allowed-method'
2 | import createApiHandler, {
3 | BigcommerceApiHandler,
4 | BigcommerceHandler,
5 | } from '../utils/create-api-handler'
6 | import { BigcommerceApiError } from '../utils/errors'
7 | import getCart from './handlers/get-cart'
8 | import addItem from './handlers/add-item'
9 | import updateItem from './handlers/update-item'
10 | import removeItem from './handlers/remove-item'
11 |
12 | export type ItemBody = {
13 | productId: number
14 | variantId: number
15 | quantity?: number
16 | }
17 |
18 | export type AddItemBody = { item: ItemBody }
19 |
20 | export type UpdateItemBody = { itemId: string; item: ItemBody }
21 |
22 | export type RemoveItemBody = { itemId: string }
23 |
24 | // TODO: this type should match:
25 | // https://developer.bigcommerce.com/api-reference/cart-checkout/server-server-cart-api/cart/getacart#responses
26 | export type Cart = {
27 | id: string
28 | parent_id?: string
29 | customer_id: number
30 | email: string
31 | currency: { code: string }
32 | tax_included: boolean
33 | base_amount: number
34 | discount_amount: number
35 | cart_amount: number
36 | line_items: {
37 | custom_items: any[]
38 | digital_items: any[]
39 | gift_certificates: any[]
40 | physical_items: any[]
41 | }
42 | // TODO: add missing fields
43 | }
44 |
45 | export type CartHandlers = {
46 | getCart: BigcommerceHandler
47 | addItem: BigcommerceHandler>
48 | updateItem: BigcommerceHandler<
49 | Cart,
50 | { cartId?: string } & Partial
51 | >
52 | removeItem: BigcommerceHandler<
53 | Cart,
54 | { cartId?: string } & Partial
55 | >
56 | }
57 |
58 | const METHODS = ['GET', 'POST', 'PUT', 'DELETE']
59 |
60 | // TODO: a complete implementation should have schema validation for `req.body`
61 | const cartApi: BigcommerceApiHandler = async (
62 | req,
63 | res,
64 | config,
65 | handlers
66 | ) => {
67 | if (!isAllowedMethod(req, res, METHODS)) return
68 |
69 | const { cookies } = req
70 | const cartId = cookies[config.cartCookie]
71 |
72 | try {
73 | // Return current cart info
74 | if (req.method === 'GET') {
75 | const body = { cartId }
76 | return await handlers['getCart']({ req, res, config, body })
77 | }
78 |
79 | // Create or add an item to the cart
80 | if (req.method === 'POST') {
81 | const body = { ...req.body, cartId }
82 | return await handlers['addItem']({ req, res, config, body })
83 | }
84 |
85 | // Update item in cart
86 | if (req.method === 'PUT') {
87 | const body = { ...req.body, cartId }
88 | return await handlers['updateItem']({ req, res, config, body })
89 | }
90 |
91 | // Remove an item from the cart
92 | if (req.method === 'DELETE') {
93 | const body = { ...req.body, cartId }
94 | return await handlers['removeItem']({ req, res, config, body })
95 | }
96 | } catch (error) {
97 | console.error(error)
98 |
99 | const message =
100 | error instanceof BigcommerceApiError
101 | ? 'An unexpected error ocurred with the Bigcommerce API'
102 | : 'An unexpected error ocurred'
103 |
104 | res.status(500).json({ data: null, errors: [{ message }] })
105 | }
106 | }
107 |
108 | export const handlers = { getCart, addItem, updateItem, removeItem }
109 |
110 | export default createApiHandler(cartApi, handlers, {})
111 |
--------------------------------------------------------------------------------
/example /api/catalog/handlers/get-products.ts:
--------------------------------------------------------------------------------
1 | import getAllProducts, { ProductEdge } from '../../operations/get-all-products'
2 | import type { ProductsHandlers } from '../products'
3 |
4 | const SORT: { [key: string]: string | undefined } = {
5 | latest: 'id',
6 | trending: 'total_sold',
7 | price: 'price',
8 | }
9 | const LIMIT = 12
10 |
11 | // Return current cart info
12 | const getProducts: ProductsHandlers['getProducts'] = async ({
13 | res,
14 | body: { search, category, brand, sort },
15 | config,
16 | }) => {
17 | // Use a dummy base as we only care about the relative path
18 | const url = new URL('/v3/catalog/products', 'http://a')
19 |
20 | url.searchParams.set('is_visible', 'true')
21 | url.searchParams.set('limit', String(LIMIT))
22 |
23 | if (search) url.searchParams.set('keyword', search)
24 |
25 | if (category && Number.isInteger(Number(category)))
26 | url.searchParams.set('categories:in', category)
27 |
28 | if (brand && Number.isInteger(Number(brand)))
29 | url.searchParams.set('brand_id', brand)
30 |
31 | if (sort) {
32 | const [_sort, direction] = sort.split('-')
33 | const sortValue = SORT[_sort]
34 |
35 | if (sortValue && direction) {
36 | url.searchParams.set('sort', sortValue)
37 | url.searchParams.set('direction', direction)
38 | }
39 | }
40 |
41 | // We only want the id of each product
42 | url.searchParams.set('include_fields', 'id')
43 |
44 | const { data } = await config.storeApiFetch<{ data: { id: number }[] }>(
45 | url.pathname + url.search
46 | )
47 | const entityIds = data.map((p) => p.id)
48 | const found = entityIds.length > 0
49 | // We want the GraphQL version of each product
50 | const graphqlData = await getAllProducts({
51 | variables: { first: LIMIT, entityIds },
52 | config,
53 | })
54 | // Put the products in an object that we can use to get them by id
55 | const productsById = graphqlData.products.reduce<{
56 | [k: number]: ProductEdge
57 | }>((prods, p) => {
58 | prods[p.node.entityId] = p
59 | return prods
60 | }, {})
61 | const products: ProductEdge[] = found ? [] : graphqlData.products
62 |
63 | // Populate the products array with the graphql products, in the order
64 | // assigned by the list of entity ids
65 | entityIds.forEach((id) => {
66 | const product = productsById[id]
67 | if (product) products.push(product)
68 | })
69 |
70 | res.status(200).json({ data: { products, found } })
71 | }
72 |
73 | export default getProducts
74 |
--------------------------------------------------------------------------------
/example /api/catalog/products.ts:
--------------------------------------------------------------------------------
1 | import isAllowedMethod from '../utils/is-allowed-method'
2 | import createApiHandler, {
3 | BigcommerceApiHandler,
4 | BigcommerceHandler,
5 | } from '../utils/create-api-handler'
6 | import { BigcommerceApiError } from '../utils/errors'
7 | import type { ProductEdge } from '../operations/get-all-products'
8 | import getProducts from './handlers/get-products'
9 |
10 | export type SearchProductsData = {
11 | products: ProductEdge[]
12 | found: boolean
13 | }
14 |
15 | export type ProductsHandlers = {
16 | getProducts: BigcommerceHandler<
17 | SearchProductsData,
18 | { search?: 'string'; category?: string; brand?: string; sort?: string }
19 | >
20 | }
21 |
22 | const METHODS = ['GET']
23 |
24 | // TODO: a complete implementation should have schema validation for `req.body`
25 | const productsApi: BigcommerceApiHandler<
26 | SearchProductsData,
27 | ProductsHandlers
28 | > = async (req, res, config, handlers) => {
29 | if (!isAllowedMethod(req, res, METHODS)) return
30 |
31 | try {
32 | const body = req.query
33 | return await handlers['getProducts']({ req, res, config, body })
34 | } catch (error) {
35 | console.error(error)
36 |
37 | const message =
38 | error instanceof BigcommerceApiError
39 | ? 'An unexpected error ocurred with the Bigcommerce API'
40 | : 'An unexpected error ocurred'
41 |
42 | res.status(500).json({ data: null, errors: [{ message }] })
43 | }
44 | }
45 |
46 | export const handlers = { getProducts }
47 |
48 | export default createApiHandler(productsApi, handlers, {})
49 |
--------------------------------------------------------------------------------
/example /api/checkout.ts:
--------------------------------------------------------------------------------
1 | import isAllowedMethod from './utils/is-allowed-method'
2 | import createApiHandler, {
3 | BigcommerceApiHandler,
4 | } from './utils/create-api-handler'
5 | import { BigcommerceApiError } from './utils/errors'
6 |
7 | const METHODS = ['GET']
8 | const fullCheckout = true
9 |
10 | // TODO: a complete implementation should have schema validation for `req.body`
11 | const checkoutApi: BigcommerceApiHandler = async (req, res, config) => {
12 | if (!isAllowedMethod(req, res, METHODS)) return
13 |
14 | const { cookies } = req
15 | const cartId = cookies[config.cartCookie]
16 |
17 | try {
18 | if (!cartId) {
19 | res.redirect('/cart')
20 | return
21 | }
22 |
23 | const { data } = await config.storeApiFetch(
24 | `/v3/carts/${cartId}/redirect_urls`,
25 | {
26 | method: 'POST',
27 | }
28 | )
29 |
30 | if (fullCheckout) {
31 | res.redirect(data.checkout_url)
32 | return
33 | }
34 |
35 | // TODO: make the embedded checkout work too!
36 | const html = `
37 |
38 |
39 |
40 |
41 |
42 | Checkout
43 |
44 |
54 |
55 |
56 |
57 |
58 |
59 | `
60 |
61 | res.status(200)
62 | res.setHeader('Content-Type', 'text/html')
63 | res.write(html)
64 | res.end()
65 | } catch (error) {
66 | console.error(error)
67 |
68 | const message =
69 | error instanceof BigcommerceApiError
70 | ? 'An unexpected error ocurred with the Bigcommerce API'
71 | : 'An unexpected error ocurred'
72 |
73 | res.status(500).json({ data: null, errors: [{ message }] })
74 | }
75 | }
76 |
77 | export default createApiHandler(checkoutApi, {}, {})
78 |
--------------------------------------------------------------------------------
/example /api/customers/handlers/get-logged-in-customer.ts:
--------------------------------------------------------------------------------
1 | import type { GetLoggedInCustomerQuery } from '../../../schema'
2 | import type { CustomersHandlers } from '..'
3 |
4 | export const getLoggedInCustomerQuery = /* GraphQL */ `
5 | query getLoggedInCustomer {
6 | customer {
7 | entityId
8 | firstName
9 | lastName
10 | email
11 | company
12 | customerGroupId
13 | notes
14 | phone
15 | addressCount
16 | attributeCount
17 | storeCredit {
18 | value
19 | currencyCode
20 | }
21 | }
22 | }
23 | `
24 |
25 | export type Customer = NonNullable
26 |
27 | const getLoggedInCustomer: CustomersHandlers['getLoggedInCustomer'] = async ({
28 | req,
29 | res,
30 | config,
31 | }) => {
32 | const token = req.cookies[config.customerCookie]
33 |
34 | if (token) {
35 | const { data } = await config.fetch(
36 | getLoggedInCustomerQuery,
37 | undefined,
38 | {
39 | headers: {
40 | cookie: `${config.customerCookie}=${token}`,
41 | },
42 | }
43 | )
44 | const { customer } = data
45 |
46 | if (!customer) {
47 | return res.status(400).json({
48 | data: null,
49 | errors: [{ message: 'Customer not found', code: 'not_found' }],
50 | })
51 | }
52 |
53 | return res.status(200).json({ data: { customer } })
54 | }
55 |
56 | res.status(200).json({ data: null })
57 | }
58 |
59 | export default getLoggedInCustomer
60 |
--------------------------------------------------------------------------------
/example /api/customers/handlers/login.ts:
--------------------------------------------------------------------------------
1 | import { FetcherError } from '../../../src/utils/errors'
2 | import login from '../../operations/login'
3 | import type { LoginHandlers } from '../login'
4 |
5 | const invalidCredentials = /invalid credentials/i
6 |
7 | const loginHandler: LoginHandlers['login'] = async ({
8 | res,
9 | body: { email, password },
10 | config,
11 | }) => {
12 | // TODO: Add proper validations with something like Ajv
13 | if (!(email && password)) {
14 | return res.status(400).json({
15 | data: null,
16 | errors: [{ message: 'Invalid request' }],
17 | })
18 | }
19 | // TODO: validate the password and email
20 | // Passwords must be at least 7 characters and contain both alphabetic
21 | // and numeric characters.
22 |
23 | try {
24 | await login({ variables: { email, password }, config, res })
25 | } catch (error) {
26 | // Check if the email and password didn't match an existing account
27 | if (
28 | error instanceof FetcherError &&
29 | invalidCredentials.test(error.message)
30 | ) {
31 | return res.status(401).json({
32 | data: null,
33 | errors: [
34 | {
35 | message:
36 | 'Cannot find an account that matches the provided credentials',
37 | code: 'invalid_credentials',
38 | },
39 | ],
40 | })
41 | }
42 |
43 | throw error
44 | }
45 |
46 | res.status(200).json({ data: null })
47 | }
48 |
49 | export default loginHandler
50 |
--------------------------------------------------------------------------------
/example /api/customers/handlers/logout.ts:
--------------------------------------------------------------------------------
1 | import { serialize } from 'cookie'
2 | import { LogoutHandlers } from '../logout'
3 |
4 | const logoutHandler: LogoutHandlers['logout'] = async ({
5 | res,
6 | body: { redirectTo },
7 | config,
8 | }) => {
9 | // Remove the cookie
10 | res.setHeader(
11 | 'Set-Cookie',
12 | serialize(config.customerCookie, '', { maxAge: -1, path: '/' })
13 | )
14 |
15 | // Only allow redirects to a relative URL
16 | if (redirectTo?.startsWith('/')) {
17 | res.redirect(redirectTo)
18 | } else {
19 | res.status(200).json({ data: null })
20 | }
21 | }
22 |
23 | export default logoutHandler
24 |
--------------------------------------------------------------------------------
/example /api/customers/handlers/signup.ts:
--------------------------------------------------------------------------------
1 | import { BigcommerceApiError } from '../../utils/errors'
2 | import login from '../../operations/login'
3 | import { SignupHandlers } from '../signup'
4 |
5 | const signup: SignupHandlers['signup'] = async ({
6 | res,
7 | body: { firstName, lastName, email, password },
8 | config,
9 | }) => {
10 | // TODO: Add proper validations with something like Ajv
11 | if (!(firstName && lastName && email && password)) {
12 | return res.status(400).json({
13 | data: null,
14 | errors: [{ message: 'Invalid request' }],
15 | })
16 | }
17 | // TODO: validate the password and email
18 | // Passwords must be at least 7 characters and contain both alphabetic
19 | // and numeric characters.
20 |
21 | try {
22 | await config.storeApiFetch('/v3/customers', {
23 | method: 'POST',
24 | body: JSON.stringify([
25 | {
26 | first_name: firstName,
27 | last_name: lastName,
28 | email,
29 | authentication: {
30 | new_password: password,
31 | },
32 | },
33 | ]),
34 | })
35 | } catch (error) {
36 | if (error instanceof BigcommerceApiError && error.status === 422) {
37 | const hasEmailError = '0.email' in error.data?.errors
38 |
39 | // If there's an error with the email, it most likely means it's duplicated
40 | if (hasEmailError) {
41 | return res.status(400).json({
42 | data: null,
43 | errors: [
44 | {
45 | message: 'The email is already in use',
46 | code: 'duplicated_email',
47 | },
48 | ],
49 | })
50 | }
51 | }
52 |
53 | throw error
54 | }
55 |
56 | // Login the customer right after creating it
57 | await login({ variables: { email, password }, res, config })
58 |
59 | res.status(200).json({ data: null })
60 | }
61 |
62 | export default signup
63 |
--------------------------------------------------------------------------------
/example /api/customers/index.ts:
--------------------------------------------------------------------------------
1 | import createApiHandler, {
2 | BigcommerceApiHandler,
3 | BigcommerceHandler,
4 | } from '../utils/create-api-handler'
5 | import isAllowedMethod from '../utils/is-allowed-method'
6 | import { BigcommerceApiError } from '../utils/errors'
7 | import getLoggedInCustomer, {
8 | Customer,
9 | } from './handlers/get-logged-in-customer'
10 |
11 | export type { Customer }
12 |
13 | export type CustomerData = {
14 | customer: Customer
15 | }
16 |
17 | export type CustomersHandlers = {
18 | getLoggedInCustomer: BigcommerceHandler
19 | }
20 |
21 | const METHODS = ['GET']
22 |
23 | const customersApi: BigcommerceApiHandler<
24 | CustomerData,
25 | CustomersHandlers
26 | > = async (req, res, config, handlers) => {
27 | if (!isAllowedMethod(req, res, METHODS)) return
28 |
29 | try {
30 | const body = null
31 | return await handlers['getLoggedInCustomer']({ req, res, config, body })
32 | } catch (error) {
33 | console.error(error)
34 |
35 | const message =
36 | error instanceof BigcommerceApiError
37 | ? 'An unexpected error ocurred with the Bigcommerce API'
38 | : 'An unexpected error ocurred'
39 |
40 | res.status(500).json({ data: null, errors: [{ message }] })
41 | }
42 | }
43 |
44 | const handlers = { getLoggedInCustomer }
45 |
46 | export default createApiHandler(customersApi, handlers, {})
47 |
--------------------------------------------------------------------------------
/example /api/customers/login.ts:
--------------------------------------------------------------------------------
1 | import createApiHandler, {
2 | BigcommerceApiHandler,
3 | BigcommerceHandler,
4 | } from '../utils/create-api-handler'
5 | import isAllowedMethod from '../utils/is-allowed-method'
6 | import { BigcommerceApiError } from '../utils/errors'
7 | import login from './handlers/login'
8 |
9 | export type LoginBody = {
10 | email: string
11 | password: string
12 | }
13 |
14 | export type LoginHandlers = {
15 | login: BigcommerceHandler>
16 | }
17 |
18 | const METHODS = ['POST']
19 |
20 | const loginApi: BigcommerceApiHandler = async (
21 | req,
22 | res,
23 | config,
24 | handlers
25 | ) => {
26 | if (!isAllowedMethod(req, res, METHODS)) return
27 |
28 | try {
29 | const body = req.body ?? {}
30 | return await handlers['login']({ req, res, config, body })
31 | } catch (error) {
32 | console.error(error)
33 |
34 | const message =
35 | error instanceof BigcommerceApiError
36 | ? 'An unexpected error ocurred with the Bigcommerce API'
37 | : 'An unexpected error ocurred'
38 |
39 | res.status(500).json({ data: null, errors: [{ message }] })
40 | }
41 | }
42 |
43 | const handlers = { login }
44 |
45 | export default createApiHandler(loginApi, handlers, {})
46 |
--------------------------------------------------------------------------------
/example /api/customers/logout.ts:
--------------------------------------------------------------------------------
1 | import createApiHandler, {
2 | BigcommerceApiHandler,
3 | BigcommerceHandler,
4 | } from '../utils/create-api-handler'
5 | import isAllowedMethod from '../utils/is-allowed-method'
6 | import { BigcommerceApiError } from '../utils/errors'
7 | import logout from './handlers/logout'
8 |
9 | export type LogoutHandlers = {
10 | logout: BigcommerceHandler
11 | }
12 |
13 | const METHODS = ['GET']
14 |
15 | const logoutApi: BigcommerceApiHandler = async (
16 | req,
17 | res,
18 | config,
19 | handlers
20 | ) => {
21 | if (!isAllowedMethod(req, res, METHODS)) return
22 |
23 | try {
24 | const redirectTo = req.query.redirect_to
25 | const body = typeof redirectTo === 'string' ? { redirectTo } : {}
26 |
27 | return await handlers['logout']({ req, res, config, body })
28 | } catch (error) {
29 | console.error(error)
30 |
31 | const message =
32 | error instanceof BigcommerceApiError
33 | ? 'An unexpected error ocurred with the Bigcommerce API'
34 | : 'An unexpected error ocurred'
35 |
36 | res.status(500).json({ data: null, errors: [{ message }] })
37 | }
38 | }
39 |
40 | const handlers = { logout }
41 |
42 | export default createApiHandler(logoutApi, handlers, {})
43 |
--------------------------------------------------------------------------------
/example /api/customers/signup.ts:
--------------------------------------------------------------------------------
1 | import createApiHandler, {
2 | BigcommerceApiHandler,
3 | BigcommerceHandler,
4 | } from '../utils/create-api-handler'
5 | import isAllowedMethod from '../utils/is-allowed-method'
6 | import { BigcommerceApiError } from '../utils/errors'
7 | import signup from './handlers/signup'
8 |
9 | export type SignupBody = {
10 | firstName: string
11 | lastName: string
12 | email: string
13 | password: string
14 | }
15 |
16 | export type SignupHandlers = {
17 | signup: BigcommerceHandler>
18 | }
19 |
20 | const METHODS = ['POST']
21 |
22 | const signupApi: BigcommerceApiHandler = async (
23 | req,
24 | res,
25 | config,
26 | handlers
27 | ) => {
28 | if (!isAllowedMethod(req, res, METHODS)) return
29 |
30 | const { cookies } = req
31 | const cartId = cookies[config.cartCookie]
32 |
33 | try {
34 | const body = { ...req.body, cartId }
35 | return await handlers['signup']({ req, res, config, body })
36 | } catch (error) {
37 | console.error(error)
38 |
39 | const message =
40 | error instanceof BigcommerceApiError
41 | ? 'An unexpected error ocurred with the Bigcommerce API'
42 | : 'An unexpected error ocurred'
43 |
44 | res.status(500).json({ data: null, errors: [{ message }] })
45 | }
46 | }
47 |
48 | const handlers = { signup }
49 |
50 | export default createApiHandler(signupApi, handlers, {})
51 |
--------------------------------------------------------------------------------
/example /api/definitions/store-content.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file was auto-generated by swagger-to-ts.
3 | * Do not make direct changes to the file.
4 | */
5 |
6 | export interface definitions {
7 | blogPost_Full: {
8 | /**
9 | * ID of this blog post. (READ-ONLY)
10 | */
11 | id?: number
12 | } & definitions['blogPost_Base']
13 | addresses: {
14 | /**
15 | * Full URL of where the resource is located.
16 | */
17 | url?: string
18 | /**
19 | * Resource being accessed.
20 | */
21 | resource?: string
22 | }
23 | formField: {
24 | /**
25 | * Name of the form field
26 | */
27 | name?: string
28 | /**
29 | * Value of the form field
30 | */
31 | value?: string
32 | }
33 | page_Full: {
34 | /**
35 | * ID of the page.
36 | */
37 | id?: number
38 | } & definitions['page_Base']
39 | redirect: {
40 | /**
41 | * Numeric ID of the redirect.
42 | */
43 | id?: number
44 | /**
45 | * The path from which to redirect.
46 | */
47 | path: string
48 | forward: definitions['forward']
49 | /**
50 | * URL of the redirect. READ-ONLY
51 | */
52 | url?: string
53 | }
54 | forward: {
55 | /**
56 | * The type of redirect. If it is a `manual` redirect then type will always be manual. Dynamic redirects will have the type of the page. Such as product or category.
57 | */
58 | type?: string
59 | /**
60 | * Reference of the redirect. Dynamic redirects will have the category or product number. Manual redirects will have the url that is being directed to.
61 | */
62 | ref?: number
63 | }
64 | customer_Full: {
65 | /**
66 | * Unique numeric ID of this customer. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
67 | */
68 | id?: number
69 | /**
70 | * Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
71 | */
72 | _authentication?: {
73 | force_reset?: string
74 | password?: string
75 | password_confirmation?: string
76 | }
77 | /**
78 | * The name of the company for which the customer works.
79 | */
80 | company?: string
81 | /**
82 | * First name of the customer.
83 | */
84 | first_name: string
85 | /**
86 | * Last name of the customer.
87 | */
88 | last_name: string
89 | /**
90 | * Email address of the customer.
91 | */
92 | email: string
93 | /**
94 | * Phone number of the customer.
95 | */
96 | phone?: string
97 | /**
98 | * Date on which the customer registered from the storefront or was created in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
99 | */
100 | date_created?: string
101 | /**
102 | * Date on which the customer updated their details in the storefront or was updated in the control panel. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
103 | */
104 | date_modified?: string
105 | /**
106 | * The amount of credit the customer has. (Float, Float as String, Integer)
107 | */
108 | store_credit?: string
109 | /**
110 | * The customer’s IP address when they signed up.
111 | */
112 | registration_ip_address?: string
113 | /**
114 | * The group to which the customer belongs.
115 | */
116 | customer_group_id?: number
117 | /**
118 | * Store-owner notes on the customer.
119 | */
120 | notes?: string
121 | /**
122 | * Used to identify customers who fall into special sales-tax categories – in particular, those who are fully or partially exempt from paying sales tax. Can be blank, or can contain a single AvaTax code. (The codes are case-sensitive.) Stores that subscribe to BigCommerce’s Avalara Premium integration will use this code to determine how/whether to apply sales tax. Does not affect sales-tax calculations for stores that do not subscribe to Avalara Premium.
123 | */
124 | tax_exempt_category?: string
125 | /**
126 | * Records whether the customer would like to receive marketing content from this store. READ-ONLY.This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
127 | */
128 | accepts_marketing?: boolean
129 | addresses?: definitions['addresses']
130 | /**
131 | * Array of custom fields. This is a READ-ONLY field; do not set or modify its value in a POST or PUT request.
132 | */
133 | form_fields?: definitions['formField'][]
134 | /**
135 | * Force a password change on next login.
136 | */
137 | reset_pass_on_login?: boolean
138 | }
139 | categoryAccessLevel: {
140 | /**
141 | * + `all` - Customers can access all categories
142 | * + `specific` - Customers can access a specific list of categories
143 | * + `none` - Customers are prevented from viewing any of the categories in this group.
144 | */
145 | type?: 'all' | 'specific' | 'none'
146 | /**
147 | * Is an array of category IDs and should be supplied only if `type` is specific.
148 | */
149 | categories?: string[]
150 | }
151 | timeZone: {
152 | /**
153 | * a string identifying the time zone, in the format: /.
154 | */
155 | name?: string
156 | /**
157 | * a negative or positive number, identifying the offset from UTC/GMT, in seconds, during winter/standard time.
158 | */
159 | raw_offset?: number
160 | /**
161 | * "-/+" offset from UTC/GMT, in seconds, during summer/daylight saving time.
162 | */
163 | dst_offset?: number
164 | /**
165 | * a boolean indicating whether this time zone observes daylight saving time.
166 | */
167 | dst_correction?: boolean
168 | date_format?: definitions['dateFormat']
169 | }
170 | count_Response: { count?: number }
171 | dateFormat: {
172 | /**
173 | * string that defines dates’ display format, in the pattern: M jS Y
174 | */
175 | display?: string
176 | /**
177 | * string that defines the CSV export format for orders, customers, and products, in the pattern: M jS Y
178 | */
179 | export?: string
180 | /**
181 | * string that defines dates’ extended-display format, in the pattern: M jS Y @ g:i A.
182 | */
183 | extended_display?: string
184 | }
185 | blogTags: { tag?: string; post_ids?: number[] }[]
186 | blogPost_Base: {
187 | /**
188 | * Title of this blog post.
189 | */
190 | title: string
191 | /**
192 | * URL for the public blog post.
193 | */
194 | url?: string
195 | /**
196 | * URL to preview the blog post. (READ-ONLY)
197 | */
198 | preview_url?: string
199 | /**
200 | * Text body of the blog post.
201 | */
202 | body: string
203 | /**
204 | * Tags to characterize the blog post.
205 | */
206 | tags?: string[]
207 | /**
208 | * Summary of the blog post. (READ-ONLY)
209 | */
210 | summary?: string
211 | /**
212 | * Whether the blog post is published.
213 | */
214 | is_published?: boolean
215 | published_date?: definitions['publishedDate']
216 | /**
217 | * Published date in `ISO 8601` format.
218 | */
219 | published_date_iso8601?: string
220 | /**
221 | * Description text for this blog post’s `` element.
222 | */
223 | meta_description?: string
224 | /**
225 | * Keywords for this blog post’s `` element.
226 | */
227 | meta_keywords?: string
228 | /**
229 | * Name of the blog post’s author.
230 | */
231 | author?: string
232 | /**
233 | * Local path to a thumbnail uploaded to `product_images/` via [WebDav](https://support.bigcommerce.com/s/article/File-Access-WebDAV).
234 | */
235 | thumbnail_path?: string
236 | }
237 | publishedDate: { timezone_type?: string; date?: string; timezone?: string }
238 | /**
239 | * Not returned in any responses, but accepts up to two fields allowing you to set the customer’s password. If a password is not supplied, it is generated automatically. For further information about using this object, please see the Customers resource documentation.
240 | */
241 | authentication: {
242 | force_reset?: string
243 | password?: string
244 | password_confirmation?: string
245 | }
246 | customer_Base: { [key: string]: any }
247 | page_Base: {
248 | /**
249 | * ID of any parent Web page.
250 | */
251 | parent_id?: number
252 | /**
253 | * `page`: free-text page
254 | * `link`: link to another web address
255 | * `rss_feed`: syndicated content from an RSS feed
256 | * `contact_form`: When the store's contact form is used.
257 | */
258 | type: 'page' | 'rss_feed' | 'contact_form' | 'raw' | 'link'
259 | /**
260 | * Where the page’s type is a contact form: object whose members are the fields enabled (in the control panel) for storefront display. Possible members are:`fullname`: full name of the customer submitting the form; `phone`: customer’s phone number, as submitted on the form; `companyname`: customer’s submitted company name; `orderno`: customer’s submitted order number; `rma`: customer’s submitted RMA (Return Merchandise Authorization) number.
261 | */
262 | contact_fields?: string
263 | /**
264 | * Where the page’s type is a contact form: email address that receives messages sent via the form.
265 | */
266 | email?: string
267 | /**
268 | * Page name, as displayed on the storefront.
269 | */
270 | name: string
271 | /**
272 | * Relative URL on the storefront for this page.
273 | */
274 | url?: string
275 | /**
276 | * Description contained within this page’s `` element.
277 | */
278 | meta_description?: string
279 | /**
280 | * HTML or variable that populates this page’s `` element, in default/desktop view. Required in POST if page type is `raw`.
281 | */
282 | body: string
283 | /**
284 | * HTML to use for this page's body when viewed in the mobile template (deprecated).
285 | */
286 | mobile_body?: string
287 | /**
288 | * If true, this page has a mobile version.
289 | */
290 | has_mobile_version?: boolean
291 | /**
292 | * If true, this page appears in the storefront’s navigation menu.
293 | */
294 | is_visible?: boolean
295 | /**
296 | * If true, this page is the storefront’s home page.
297 | */
298 | is_homepage?: boolean
299 | /**
300 | * Text specified for this page’s `` element. (If empty, the value of the name property is used.)
301 | */
302 | meta_title?: string
303 | /**
304 | * Layout template for this page. This field is writable only for stores with a Blueprint theme applied.
305 | */
306 | layout_file?: string
307 | /**
308 | * Order in which this page should display on the storefront. (Lower integers specify earlier display.)
309 | */
310 | sort_order?: number
311 | /**
312 | * Comma-separated list of keywords that shoppers can use to locate this page when searching the store.
313 | */
314 | search_keywords?: string
315 | /**
316 | * Comma-separated list of SEO-relevant keywords to include in the page’s `` element.
317 | */
318 | meta_keywords?: string
319 | /**
320 | * If page type is `rss_feed` the n this field is visisble. Required in POST required for `rss page` type.
321 | */
322 | feed: string
323 | /**
324 | * If page type is `link` this field is returned. Required in POST to create a `link` page.
325 | */
326 | link: string
327 | content_type?: 'application/json' | 'text/javascript' | 'text/html'
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/example /api/definitions/wishlist.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file was auto-generated by swagger-to-ts.
3 | * Do not make direct changes to the file.
4 | */
5 |
6 | export interface definitions {
7 | wishlist_Post: {
8 | /**
9 | * The customer id.
10 | */
11 | customer_id: number
12 | /**
13 | * Whether the wishlist is available to the public.
14 | */
15 | is_public?: boolean
16 | /**
17 | * The title of the wishlist.
18 | */
19 | name?: string
20 | /**
21 | * Array of Wishlist items.
22 | */
23 | items?: {
24 | /**
25 | * The ID of the product.
26 | */
27 | product_id?: number
28 | /**
29 | * The variant ID of the product.
30 | */
31 | variant_id?: number
32 | }[]
33 | }
34 | wishlist_Put: {
35 | /**
36 | * The customer id.
37 | */
38 | customer_id: number
39 | /**
40 | * Whether the wishlist is available to the public.
41 | */
42 | is_public?: boolean
43 | /**
44 | * The title of the wishlist.
45 | */
46 | name?: string
47 | /**
48 | * Array of Wishlist items.
49 | */
50 | items?: {
51 | /**
52 | * The ID of the item
53 | */
54 | id?: number
55 | /**
56 | * The ID of the product.
57 | */
58 | product_id?: number
59 | /**
60 | * The variant ID of the item.
61 | */
62 | variant_id?: number
63 | }[]
64 | }
65 | wishlist_Full: {
66 | /**
67 | * Wishlist ID, provided after creating a wishlist with a POST.
68 | */
69 | id?: number
70 | /**
71 | * The ID the customer to which the wishlist belongs.
72 | */
73 | customer_id?: number
74 | /**
75 | * The Wishlist's name.
76 | */
77 | name?: string
78 | /**
79 | * Whether the Wishlist is available to the public.
80 | */
81 | is_public?: boolean
82 | /**
83 | * The token of the Wishlist. This is created internally within BigCommerce. The Wishlist ID is to be used for external apps. Read-Only
84 | */
85 | token?: string
86 | /**
87 | * Array of Wishlist items
88 | */
89 | items?: definitions['wishlistItem_Full'][]
90 | }
91 | wishlistItem_Full: {
92 | /**
93 | * The ID of the item
94 | */
95 | id?: number
96 | /**
97 | * The ID of the product.
98 | */
99 | product_id?: number
100 | /**
101 | * The variant ID of the item.
102 | */
103 | variant_id?: number
104 | }
105 | wishlistItem_Post: {
106 | /**
107 | * The ID of the product.
108 | */
109 | product_id?: number
110 | /**
111 | * The variant ID of the product.
112 | */
113 | variant_id?: number
114 | }
115 | /**
116 | * Data about the response, including pagination and collection totals.
117 | */
118 | pagination: {
119 | /**
120 | * Total number of items in the result set.
121 | */
122 | total?: number
123 | /**
124 | * Total number of items in the collection response.
125 | */
126 | count?: number
127 | /**
128 | * The amount of items returned in the collection per page, controlled by the limit parameter.
129 | */
130 | per_page?: number
131 | /**
132 | * The page you are currently on within the collection.
133 | */
134 | current_page?: number
135 | /**
136 | * The total number of pages in the collection.
137 | */
138 | total_pages?: number
139 | }
140 | error: { status?: number; title?: string; type?: string }
141 | metaCollection: { pagination?: definitions['pagination'] }
142 | }
143 |
--------------------------------------------------------------------------------
/example /api/fragments/category-tree.ts:
--------------------------------------------------------------------------------
1 | export const categoryTreeItemFragment = /* GraphQL */ `
2 | fragment categoryTreeItem on CategoryTreeItem {
3 | entityId
4 | name
5 | path
6 | description
7 | productCount
8 | }
9 | `
10 |
--------------------------------------------------------------------------------
/example /api/fragments/product.ts:
--------------------------------------------------------------------------------
1 | export const productPrices = /* GraphQL */ `
2 | fragment productPrices on Prices {
3 | price {
4 | value
5 | currencyCode
6 | }
7 | salePrice {
8 | value
9 | currencyCode
10 | }
11 | retailPrice {
12 | value
13 | currencyCode
14 | }
15 | }
16 | `
17 |
18 | export const swatchOptionFragment = /* GraphQL */ `
19 | fragment swatchOption on SwatchOptionValue {
20 | isDefault
21 | hexColors
22 | }
23 | `
24 |
25 | export const multipleChoiceOptionFragment = /* GraphQL */ `
26 | fragment multipleChoiceOption on MultipleChoiceOption {
27 | values {
28 | edges {
29 | node {
30 | label
31 | ...swatchOption
32 | }
33 | }
34 | }
35 | }
36 |
37 | ${swatchOptionFragment}
38 | `
39 |
40 | export const productInfoFragment = /* GraphQL */ `
41 | fragment productInfo on Product {
42 | entityId
43 | name
44 | path
45 | brand {
46 | entityId
47 | }
48 | description
49 | prices {
50 | ...productPrices
51 | }
52 | images {
53 | edges {
54 | node {
55 | urlOriginal
56 | altText
57 | isDefault
58 | }
59 | }
60 | }
61 | variants {
62 | edges {
63 | node {
64 | entityId
65 | defaultImage {
66 | urlOriginal
67 | altText
68 | isDefault
69 | }
70 | }
71 | }
72 | }
73 | productOptions {
74 | edges {
75 | node {
76 | __typename
77 | entityId
78 | displayName
79 | ...multipleChoiceOption
80 | }
81 | }
82 | }
83 | localeMeta: metafields(namespace: $locale, keys: ["name", "description"])
84 | @include(if: $hasLocale) {
85 | edges {
86 | node {
87 | key
88 | value
89 | }
90 | }
91 | }
92 | }
93 |
94 | ${productPrices}
95 | ${multipleChoiceOptionFragment}
96 | `
97 |
98 | export const productConnectionFragment = /* GraphQL */ `
99 | fragment productConnnection on ProductConnection {
100 | pageInfo {
101 | startCursor
102 | endCursor
103 | }
104 | edges {
105 | cursor
106 | node {
107 | ...productInfo
108 | }
109 | }
110 | }
111 |
112 | ${productInfoFragment}
113 | `
114 |
--------------------------------------------------------------------------------
/example /api/index.ts:
--------------------------------------------------------------------------------
1 | import type { RequestInit } from '@vercel/fetch'
2 | import type { CommerceAPIConfig } from '../src/api'
3 | import fetchGraphqlApi from './utils/fetch-graphql-api'
4 | import fetchStoreApi from './utils/fetch-store-api'
5 |
6 | export interface BigcommerceConfig extends CommerceAPIConfig {
7 | // Indicates if the returned metadata with translations should be applied to the
8 | // data or returned as it is
9 | applyLocale?: boolean
10 | storeApiUrl: string
11 | storeApiToken: string
12 | storeApiClientId: string
13 | storeChannelId?: string
14 | storeApiFetch(endpoint: string, options?: RequestInit): Promise
15 | }
16 |
17 | const API_URL = process.env.BIGCOMMERCE_STOREFRONT_API_URL
18 | const API_TOKEN = process.env.BIGCOMMERCE_STOREFRONT_API_TOKEN
19 | const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL
20 | const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN
21 | const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID
22 | const STORE_CHANNEL_ID = process.env.BIGCOMMERCE_CHANNEL_ID
23 |
24 | if (!API_URL) {
25 | throw new Error(
26 | `The environment variable BIGCOMMERCE_STOREFRONT_API_URL is missing and it's required to access your store`
27 | )
28 | }
29 |
30 | if (!API_TOKEN) {
31 | throw new Error(
32 | `The environment variable BIGCOMMERCE_STOREFRONT_API_TOKEN is missing and it's required to access your store`
33 | )
34 | }
35 |
36 | if (!(STORE_API_URL && STORE_API_TOKEN && STORE_API_CLIENT_ID)) {
37 | throw new Error(
38 | `The environment variables BIGCOMMERCE_STORE_API_URL, BIGCOMMERCE_STORE_API_TOKEN, BIGCOMMERCE_STORE_API_CLIENT_ID have to be set in order to access the REST API of your store`
39 | )
40 | }
41 |
42 | export class Config {
43 | private config: BigcommerceConfig
44 |
45 | constructor(config: Omit) {
46 | this.config = {
47 | ...config,
48 | // The customerCookie is not customizable for now, BC sets the cookie and it's
49 | // not important to rename it
50 | customerCookie: 'SHOP_TOKEN',
51 | }
52 | }
53 |
54 | getConfig(userConfig: Partial = {}) {
55 | return Object.entries(userConfig).reduce(
56 | (cfg, [key, value]) => Object.assign(cfg, { [key]: value }),
57 | { ...this.config }
58 | )
59 | }
60 |
61 | setConfig(newConfig: Partial) {
62 | Object.assign(this.config, newConfig)
63 | }
64 | }
65 |
66 | const ONE_DAY = 60 * 60 * 24
67 | const config = new Config({
68 | commerceUrl: API_URL,
69 | apiToken: API_TOKEN,
70 | cartCookie: process.env.BIGCOMMERCE_CART_COOKIE ?? 'bc_cartId',
71 | cartCookieMaxAge: ONE_DAY * 30,
72 | fetch: fetchGraphqlApi,
73 | applyLocale: true,
74 | // REST API only
75 | storeApiUrl: STORE_API_URL,
76 | storeApiToken: STORE_API_TOKEN,
77 | storeApiClientId: STORE_API_CLIENT_ID,
78 | storeChannelId: STORE_CHANNEL_ID,
79 | storeApiFetch: fetchStoreApi,
80 | })
81 |
82 | export function getConfig(userConfig?: Partial) {
83 | return config.getConfig(userConfig)
84 | }
85 |
86 | export function setConfig(newConfig: Partial) {
87 | return config.setConfig(newConfig)
88 | }
89 |
--------------------------------------------------------------------------------
/example /api/operations/get-all-pages.ts:
--------------------------------------------------------------------------------
1 | import type { RecursivePartial, RecursiveRequired } from '../utils/types'
2 | import { BigcommerceConfig, getConfig } from '..'
3 | import { definitions } from '../definitions/store-content'
4 |
5 | export type Page = definitions['page_Full']
6 |
7 | export type GetAllPagesResult<
8 | T extends { pages: any[] } = { pages: Page[] }
9 | > = T
10 |
11 | async function getAllPages(opts?: {
12 | config?: BigcommerceConfig
13 | preview?: boolean
14 | }): Promise
15 |
16 | async function getAllPages(opts: {
17 | url: string
18 | config?: BigcommerceConfig
19 | preview?: boolean
20 | }): Promise>
21 |
22 | async function getAllPages({
23 | config,
24 | preview,
25 | }: {
26 | url?: string
27 | config?: BigcommerceConfig
28 | preview?: boolean
29 | } = {}): Promise {
30 | config = getConfig(config)
31 | // RecursivePartial forces the method to check for every prop in the data, which is
32 | // required in case there's a custom `url`
33 | const { data } = await config.storeApiFetch<
34 | RecursivePartial<{ data: Page[] }>
35 | >('/v3/content/pages')
36 | const pages = (data as RecursiveRequired) ?? []
37 |
38 | return {
39 | pages: preview ? pages : pages.filter((p) => p.is_visible),
40 | }
41 | }
42 |
43 | export default getAllPages
44 |
--------------------------------------------------------------------------------
/example /api/operations/get-all-product-paths.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | GetAllProductPathsQuery,
3 | GetAllProductPathsQueryVariables,
4 | } from '../../schema'
5 | import type { RecursivePartial, RecursiveRequired } from '../utils/types'
6 | import filterEdges from '../utils/filter-edges'
7 | import { BigcommerceConfig, getConfig } from '..'
8 |
9 | export const getAllProductPathsQuery = /* GraphQL */ `
10 | query getAllProductPaths($first: Int = 100) {
11 | site {
12 | products(first: $first) {
13 | edges {
14 | node {
15 | path
16 | }
17 | }
18 | }
19 | }
20 | }
21 | `
22 |
23 | export type ProductPath = NonNullable<
24 | NonNullable[0]
25 | >
26 |
27 | export type ProductPaths = ProductPath[]
28 |
29 | export type { GetAllProductPathsQueryVariables }
30 |
31 | export type GetAllProductPathsResult<
32 | T extends { products: any[] } = { products: ProductPaths }
33 | > = T
34 |
35 | async function getAllProductPaths(opts?: {
36 | variables?: GetAllProductPathsQueryVariables
37 | config?: BigcommerceConfig
38 | }): Promise
39 |
40 | async function getAllProductPaths<
41 | T extends { products: any[] },
42 | V = any
43 | >(opts: {
44 | query: string
45 | variables?: V
46 | config?: BigcommerceConfig
47 | }): Promise>
48 |
49 | async function getAllProductPaths({
50 | query = getAllProductPathsQuery,
51 | variables,
52 | config,
53 | }: {
54 | query?: string
55 | variables?: GetAllProductPathsQueryVariables
56 | config?: BigcommerceConfig
57 | } = {}): Promise {
58 | config = getConfig(config)
59 | // RecursivePartial forces the method to check for every prop in the data, which is
60 | // required in case there's a custom `query`
61 | const { data } = await config.fetch<
62 | RecursivePartial
63 | >(query, { variables })
64 | const products = data.site?.products?.edges
65 |
66 | return {
67 | products: filterEdges(products as RecursiveRequired),
68 | }
69 | }
70 |
71 | export default getAllProductPaths
72 |
--------------------------------------------------------------------------------
/example /api/operations/get-all-products.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | GetAllProductsQuery,
3 | GetAllProductsQueryVariables,
4 | } from '../../schema'
5 | import type { RecursivePartial, RecursiveRequired } from '../utils/types'
6 | import filterEdges from '../utils/filter-edges'
7 | import setProductLocaleMeta from '../utils/set-product-locale-meta'
8 | import { productConnectionFragment } from '../fragments/product'
9 | import { BigcommerceConfig, getConfig } from '..'
10 |
11 | export const getAllProductsQuery = /* GraphQL */ `
12 | query getAllProducts(
13 | $hasLocale: Boolean = false
14 | $locale: String = "null"
15 | $entityIds: [Int!]
16 | $first: Int = 10
17 | $products: Boolean = false
18 | $featuredProducts: Boolean = false
19 | $bestSellingProducts: Boolean = false
20 | $newestProducts: Boolean = false
21 | ) {
22 | site {
23 | products(first: $first, entityIds: $entityIds) @include(if: $products) {
24 | ...productConnnection
25 | }
26 | featuredProducts(first: $first) @include(if: $featuredProducts) {
27 | ...productConnnection
28 | }
29 | bestSellingProducts(first: $first) @include(if: $bestSellingProducts) {
30 | ...productConnnection
31 | }
32 | newestProducts(first: $first) @include(if: $newestProducts) {
33 | ...productConnnection
34 | }
35 | }
36 | }
37 |
38 | ${productConnectionFragment}
39 | `
40 |
41 | export type ProductEdge = NonNullable<
42 | NonNullable[0]
43 | >
44 |
45 | export type ProductNode = ProductEdge['node']
46 |
47 | export type GetAllProductsResult<
48 | T extends Record = {
49 | products: ProductEdge[]
50 | }
51 | > = T
52 |
53 | const FIELDS = [
54 | 'products',
55 | 'featuredProducts',
56 | 'bestSellingProducts',
57 | 'newestProducts',
58 | ]
59 |
60 | export type ProductTypes =
61 | | 'products'
62 | | 'featuredProducts'
63 | | 'bestSellingProducts'
64 | | 'newestProducts'
65 |
66 | export type ProductVariables = { field?: ProductTypes } & Omit<
67 | GetAllProductsQueryVariables,
68 | ProductTypes | 'hasLocale'
69 | >
70 |
71 | async function getAllProducts(opts?: {
72 | variables?: ProductVariables
73 | config?: BigcommerceConfig
74 | preview?: boolean
75 | }): Promise
76 |
77 | async function getAllProducts<
78 | T extends Record,
79 | V = any
80 | >(opts: {
81 | query: string
82 | variables?: V
83 | config?: BigcommerceConfig
84 | preview?: boolean
85 | }): Promise>
86 |
87 | async function getAllProducts({
88 | query = getAllProductsQuery,
89 | variables: { field = 'products', ...vars } = {},
90 | config,
91 | }: {
92 | query?: string
93 | variables?: ProductVariables
94 | config?: BigcommerceConfig
95 | preview?: boolean
96 | } = {}): Promise {
97 | config = getConfig(config)
98 |
99 | const locale = vars.locale || config.locale
100 | const variables: GetAllProductsQueryVariables = {
101 | ...vars,
102 | locale,
103 | hasLocale: !!locale,
104 | }
105 |
106 | if (!FIELDS.includes(field)) {
107 | throw new Error(
108 | `The field variable has to match one of ${FIELDS.join(', ')}`
109 | )
110 | }
111 |
112 | variables[field] = true
113 |
114 | // RecursivePartial forces the method to check for every prop in the data, which is
115 | // required in case there's a custom `query`
116 | const { data } = await config.fetch>(
117 | query,
118 | { variables }
119 | )
120 | const edges = data.site?.[field]?.edges
121 | const products = filterEdges(edges as RecursiveRequired)
122 |
123 | if (locale && config.applyLocale) {
124 | products.forEach((product: RecursivePartial) => {
125 | if (product.node) setProductLocaleMeta(product.node)
126 | })
127 | }
128 |
129 | return { products }
130 | }
131 |
132 | export default getAllProducts
133 |
--------------------------------------------------------------------------------
/example /api/operations/get-customer-id.ts:
--------------------------------------------------------------------------------
1 | import { GetCustomerIdQuery } from '../../schema'
2 | import { BigcommerceConfig, getConfig } from '..'
3 |
4 | export const getCustomerIdQuery = /* GraphQL */ `
5 | query getCustomerId {
6 | customer {
7 | entityId
8 | }
9 | }
10 | `
11 |
12 | async function getCustomerId({
13 | customerToken,
14 | config,
15 | }: {
16 | customerToken: string
17 | config?: BigcommerceConfig
18 | }): Promise {
19 | config = getConfig(config)
20 |
21 | const { data } = await config.fetch(
22 | getCustomerIdQuery,
23 | undefined,
24 | {
25 | headers: {
26 | cookie: `${config.customerCookie}=${customerToken}`,
27 | },
28 | }
29 | )
30 |
31 | return data?.customer?.entityId
32 | }
33 |
34 | export default getCustomerId
35 |
--------------------------------------------------------------------------------
/example /api/operations/get-customer-wishlist.ts:
--------------------------------------------------------------------------------
1 | import type { RecursivePartial, RecursiveRequired } from '../utils/types'
2 | import { definitions } from '../definitions/wishlist'
3 | import { BigcommerceConfig, getConfig } from '..'
4 | import getAllProducts, { ProductEdge } from './get-all-products'
5 |
6 | export type Wishlist = Omit & {
7 | items?: WishlistItem[]
8 | }
9 |
10 | export type WishlistItem = NonNullable<
11 | definitions['wishlist_Full']['items']
12 | >[0] & {
13 | product?: ProductEdge['node']
14 | }
15 |
16 | export type GetCustomerWishlistResult<
17 | T extends { wishlist?: any } = { wishlist?: Wishlist }
18 | > = T
19 |
20 | export type GetCustomerWishlistVariables = {
21 | customerId: number
22 | }
23 |
24 | async function getCustomerWishlist(opts: {
25 | variables: GetCustomerWishlistVariables
26 | config?: BigcommerceConfig
27 | includeProducts?: boolean
28 | }): Promise
29 |
30 | async function getCustomerWishlist<
31 | T extends { wishlist?: any },
32 | V = any
33 | >(opts: {
34 | url: string
35 | variables: V
36 | config?: BigcommerceConfig
37 | includeProducts?: boolean
38 | }): Promise>
39 |
40 | async function getCustomerWishlist({
41 | config,
42 | variables,
43 | includeProducts,
44 | }: {
45 | url?: string
46 | variables: GetCustomerWishlistVariables
47 | config?: BigcommerceConfig
48 | includeProducts?: boolean
49 | }): Promise {
50 | config = getConfig(config)
51 |
52 | const { data = [] } = await config.storeApiFetch<
53 | RecursivePartial<{ data: Wishlist[] }>
54 | >(`/v3/wishlists?customer_id=${variables.customerId}`)
55 | const wishlist = data[0]
56 |
57 | if (includeProducts && wishlist?.items?.length) {
58 | const entityIds = wishlist.items
59 | ?.map((item) => item?.product_id)
60 | .filter((id): id is number => !!id)
61 |
62 | if (entityIds?.length) {
63 | const graphqlData = await getAllProducts({
64 | variables: { first: 100, entityIds },
65 | config,
66 | })
67 | // Put the products in an object that we can use to get them by id
68 | const productsById = graphqlData.products.reduce<{
69 | [k: number]: ProductEdge
70 | }>((prods, p) => {
71 | prods[p.node.entityId] = p
72 | return prods
73 | }, {})
74 | // Populate the wishlist items with the graphql products
75 | wishlist.items.forEach((item) => {
76 | const product = item && productsById[item.product_id!]
77 | if (item && product) {
78 | item.product = product.node
79 | }
80 | })
81 | }
82 | }
83 |
84 | return { wishlist: wishlist as RecursiveRequired }
85 | }
86 |
87 | export default getCustomerWishlist
88 |
--------------------------------------------------------------------------------
/example /api/operations/get-page.ts:
--------------------------------------------------------------------------------
1 | import type { RecursivePartial, RecursiveRequired } from '../utils/types'
2 | import { BigcommerceConfig, getConfig } from '..'
3 | import { definitions } from '../definitions/store-content'
4 |
5 | export type Page = definitions['page_Full']
6 |
7 | export type GetPageResult = T
8 |
9 | export type PageVariables = {
10 | id: number
11 | }
12 |
13 | async function getPage(opts: {
14 | url?: string
15 | variables: PageVariables
16 | config?: BigcommerceConfig
17 | preview?: boolean
18 | }): Promise
19 |
20 | async function getPage(opts: {
21 | url: string
22 | variables: V
23 | config?: BigcommerceConfig
24 | preview?: boolean
25 | }): Promise>
26 |
27 | async function getPage({
28 | url,
29 | variables,
30 | config,
31 | preview,
32 | }: {
33 | url?: string
34 | variables: PageVariables
35 | config?: BigcommerceConfig
36 | preview?: boolean
37 | }): Promise {
38 | config = getConfig(config)
39 | // RecursivePartial forces the method to check for every prop in the data, which is
40 | // required in case there's a custom `url`
41 | const { data } = await config.storeApiFetch>(
42 | url || `/v3/content/pages?id=${variables.id}&include=body`
43 | )
44 | const firstPage = data?.[0]
45 | const page = firstPage as RecursiveRequired
46 |
47 | if (preview || page?.is_visible) {
48 | return { page }
49 | }
50 | return {}
51 | }
52 |
53 | export default getPage
54 |
--------------------------------------------------------------------------------
/example /api/operations/get-product.ts:
--------------------------------------------------------------------------------
1 | import type { GetProductQuery, GetProductQueryVariables } from '../../schema'
2 | import setProductLocaleMeta from '../utils/set-product-locale-meta'
3 | import { productInfoFragment } from '../fragments/product'
4 | import { BigcommerceConfig, getConfig } from '..'
5 |
6 | export const getProductQuery = /* GraphQL */ `
7 | query getProduct(
8 | $hasLocale: Boolean = false
9 | $locale: String = "null"
10 | $path: String!
11 | ) {
12 | site {
13 | route(path: $path) {
14 | node {
15 | __typename
16 | ... on Product {
17 | ...productInfo
18 | variants {
19 | edges {
20 | node {
21 | entityId
22 | defaultImage {
23 | urlOriginal
24 | altText
25 | isDefault
26 | }
27 | prices {
28 | ...productPrices
29 | }
30 | inventory {
31 | aggregated {
32 | availableToSell
33 | warningLevel
34 | }
35 | isInStock
36 | }
37 | productOptions {
38 | edges {
39 | node {
40 | __typename
41 | entityId
42 | displayName
43 | ...multipleChoiceOption
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
56 | ${productInfoFragment}
57 | `
58 |
59 | export type ProductNode = Extract<
60 | GetProductQuery['site']['route']['node'],
61 | { __typename: 'Product' }
62 | >
63 |
64 | export type GetProductResult<
65 | T extends { product?: any } = { product?: ProductNode }
66 | > = T
67 |
68 | export type ProductVariables = { locale?: string } & (
69 | | { path: string; slug?: never }
70 | | { path?: never; slug: string }
71 | )
72 |
73 | async function getProduct(opts: {
74 | variables: ProductVariables
75 | config?: BigcommerceConfig
76 | preview?: boolean
77 | }): Promise
78 |
79 | async function getProduct(opts: {
80 | query: string
81 | variables: V
82 | config?: BigcommerceConfig
83 | preview?: boolean
84 | }): Promise>
85 |
86 | async function getProduct({
87 | query = getProductQuery,
88 | variables: { slug, ...vars },
89 | config,
90 | }: {
91 | query?: string
92 | variables: ProductVariables
93 | config?: BigcommerceConfig
94 | preview?: boolean
95 | }): Promise {
96 | config = getConfig(config)
97 |
98 | const locale = vars.locale || config.locale
99 | const variables: GetProductQueryVariables = {
100 | ...vars,
101 | locale,
102 | hasLocale: !!locale,
103 | path: slug ? `/${slug}/` : vars.path!,
104 | }
105 | const { data } = await config.fetch(query, { variables })
106 | const product = data.site?.route?.node
107 |
108 | if (product?.__typename === 'Product') {
109 | if (locale && config.applyLocale) {
110 | setProductLocaleMeta(product)
111 | }
112 | return { product }
113 | }
114 |
115 | return {}
116 | }
117 |
118 | export default getProduct
119 |
--------------------------------------------------------------------------------
/example /api/operations/get-site-info.ts:
--------------------------------------------------------------------------------
1 | import type { GetSiteInfoQuery, GetSiteInfoQueryVariables } from '../../schema'
2 | import type { RecursivePartial, RecursiveRequired } from '../utils/types'
3 | import filterEdges from '../utils/filter-edges'
4 | import { BigcommerceConfig, getConfig } from '..'
5 | import { categoryTreeItemFragment } from '../fragments/category-tree'
6 |
7 | // Get 3 levels of categories
8 | export const getSiteInfoQuery = /* GraphQL */ `
9 | query getSiteInfo {
10 | site {
11 | categoryTree {
12 | ...categoryTreeItem
13 | children {
14 | ...categoryTreeItem
15 | children {
16 | ...categoryTreeItem
17 | }
18 | }
19 | }
20 | brands {
21 | pageInfo {
22 | startCursor
23 | endCursor
24 | }
25 | edges {
26 | cursor
27 | node {
28 | entityId
29 | name
30 | defaultImage {
31 | urlOriginal
32 | altText
33 | }
34 | pageTitle
35 | metaDesc
36 | metaKeywords
37 | searchKeywords
38 | path
39 | }
40 | }
41 | }
42 | }
43 | }
44 | ${categoryTreeItemFragment}
45 | `
46 |
47 | export type CategoriesTree = NonNullable<
48 | GetSiteInfoQuery['site']['categoryTree']
49 | >
50 |
51 | export type BrandEdge = NonNullable<
52 | NonNullable[0]
53 | >
54 |
55 | export type Brands = BrandEdge[]
56 |
57 | export type GetSiteInfoResult<
58 | T extends { categories: any[]; brands: any[] } = {
59 | categories: CategoriesTree
60 | brands: Brands
61 | }
62 | > = T
63 |
64 | async function getSiteInfo(opts?: {
65 | variables?: GetSiteInfoQueryVariables
66 | config?: BigcommerceConfig
67 | preview?: boolean
68 | }): Promise
69 |
70 | async function getSiteInfo<
71 | T extends { categories: any[]; brands: any[] },
72 | V = any
73 | >(opts: {
74 | query: string
75 | variables?: V
76 | config?: BigcommerceConfig
77 | preview?: boolean
78 | }): Promise>
79 |
80 | async function getSiteInfo({
81 | query = getSiteInfoQuery,
82 | variables,
83 | config,
84 | }: {
85 | query?: string
86 | variables?: GetSiteInfoQueryVariables
87 | config?: BigcommerceConfig
88 | preview?: boolean
89 | } = {}): Promise {
90 | config = getConfig(config)
91 | // RecursivePartial forces the method to check for every prop in the data, which is
92 | // required in case there's a custom `query`
93 | const { data } = await config.fetch>(
94 | query,
95 | { variables }
96 | )
97 | const categories = data.site?.categoryTree
98 | const brands = data.site?.brands?.edges
99 |
100 | return {
101 | categories: (categories as RecursiveRequired) ?? [],
102 | brands: filterEdges(brands as RecursiveRequired),
103 | }
104 | }
105 |
106 | export default getSiteInfo
107 |
--------------------------------------------------------------------------------
/example /api/operations/login.ts:
--------------------------------------------------------------------------------
1 | import type { ServerResponse } from 'http'
2 | import type { LoginMutation, LoginMutationVariables } from '../../schema'
3 | import type { RecursivePartial } from '../utils/types'
4 | import concatHeader from '../utils/concat-cookie'
5 | import { BigcommerceConfig, getConfig } from '..'
6 |
7 | export const loginMutation = /* GraphQL */ `
8 | mutation login($email: String!, $password: String!) {
9 | login(email: $email, password: $password) {
10 | result
11 | }
12 | }
13 | `
14 |
15 | export type LoginResult = T
16 |
17 | export type LoginVariables = LoginMutationVariables
18 |
19 | async function login(opts: {
20 | variables: LoginVariables
21 | config?: BigcommerceConfig
22 | res: ServerResponse
23 | }): Promise
24 |
25 | async function login(opts: {
26 | query: string
27 | variables: V
28 | res: ServerResponse
29 | config?: BigcommerceConfig
30 | }): Promise>
31 |
32 | async function login({
33 | query = loginMutation,
34 | variables,
35 | res: response,
36 | config,
37 | }: {
38 | query?: string
39 | variables: LoginVariables
40 | res: ServerResponse
41 | config?: BigcommerceConfig
42 | }): Promise {
43 | config = getConfig(config)
44 |
45 | const { data, res } = await config.fetch>(
46 | query,
47 | { variables }
48 | )
49 | // Bigcommerce returns a Set-Cookie header with the auth cookie
50 | let cookie = res.headers.get('Set-Cookie')
51 |
52 | if (cookie && typeof cookie === 'string') {
53 | // In development, don't set a secure cookie or the browser will ignore it
54 | if (process.env.NODE_ENV !== 'production') {
55 | cookie = cookie.replace('; Secure', '')
56 | // SameSite=none can't be set unless the cookie is Secure
57 | cookie = cookie.replace('; SameSite=none', '; SameSite=lax')
58 | }
59 |
60 | response.setHeader(
61 | 'Set-Cookie',
62 | concatHeader(response.getHeader('Set-Cookie'), cookie)!
63 | )
64 | }
65 |
66 | return {
67 | result: data.login?.result,
68 | }
69 | }
70 |
71 | export default login
72 |
--------------------------------------------------------------------------------
/example /api/utils/concat-cookie.ts:
--------------------------------------------------------------------------------
1 | type Header = string | number | string[] | undefined
2 |
3 | export default function concatHeader(prev: Header, val: Header) {
4 | if (!val) return prev
5 | if (!prev) return val
6 |
7 | if (Array.isArray(prev)) return prev.concat(String(val))
8 |
9 | prev = String(prev)
10 |
11 | if (Array.isArray(val)) return [prev].concat(val)
12 |
13 | return [prev, String(val)]
14 | }
15 |
--------------------------------------------------------------------------------
/example /api/utils/create-api-handler.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
2 | import { BigcommerceConfig, getConfig } from '..'
3 |
4 | export type BigcommerceApiHandler<
5 | T = any,
6 | H extends BigcommerceHandlers = {},
7 | Options extends {} = {}
8 | > = (
9 | req: NextApiRequest,
10 | res: NextApiResponse>,
11 | config: BigcommerceConfig,
12 | handlers: H,
13 | // Custom configs that may be used by a particular handler
14 | options: Options
15 | ) => void | Promise
16 |
17 | export type BigcommerceHandler = (options: {
18 | req: NextApiRequest
19 | res: NextApiResponse>
20 | config: BigcommerceConfig
21 | body: Body
22 | }) => void | Promise
23 |
24 | export type BigcommerceHandlers = {
25 | [k: string]: BigcommerceHandler
26 | }
27 |
28 | export type BigcommerceApiResponse = {
29 | data: T | null
30 | errors?: { message: string; code?: string }[]
31 | }
32 |
33 | export default function createApiHandler<
34 | T = any,
35 | H extends BigcommerceHandlers = {},
36 | Options extends {} = {}
37 | >(
38 | handler: BigcommerceApiHandler,
39 | handlers: H,
40 | defaultOptions: Options
41 | ) {
42 | return function getApiHandler({
43 | config,
44 | operations,
45 | options,
46 | }: {
47 | config?: BigcommerceConfig
48 | operations?: Partial
49 | options?: Options extends {} ? Partial : never
50 | } = {}): NextApiHandler {
51 | const ops = { ...operations, ...handlers }
52 | const opts = { ...defaultOptions, ...options }
53 |
54 | return function apiHandler(req, res) {
55 | return handler(req, res, getConfig(config), ops, opts)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/example /api/utils/errors.ts:
--------------------------------------------------------------------------------
1 | import type { Response } from '@vercel/fetch'
2 |
3 | // Used for GraphQL errors
4 | export class BigcommerceGraphQLError extends Error {}
5 |
6 | export class BigcommerceApiError extends Error {
7 | status: number
8 | res: Response
9 | data: any
10 |
11 | constructor(msg: string, res: Response, data?: any) {
12 | super(msg)
13 | this.name = 'BigcommerceApiError'
14 | this.status = res.status
15 | this.res = res
16 | this.data = data
17 | }
18 | }
19 |
20 | export class BigcommerceNetworkError extends Error {
21 | constructor(msg: string) {
22 | super(msg)
23 | this.name = 'BigcommerceNetworkError'
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/example /api/utils/fetch-graphql-api.ts:
--------------------------------------------------------------------------------
1 | import { FetcherError } from '../../src/utils/errors'
2 | import type { GraphQLFetcher } from '../../src/api'
3 | import { getConfig } from '..'
4 | import fetch from './fetch'
5 |
6 | const fetchGraphqlApi: GraphQLFetcher = async (
7 | query: string,
8 | { variables, preview } = {},
9 | fetchOptions
10 | ) => {
11 | // log.warn(query)
12 | const config = getConfig()
13 | const res = await fetch(config.commerceUrl + (preview ? '/preview' : ''), {
14 | ...fetchOptions,
15 | method: 'POST',
16 | headers: {
17 | Authorization: `Bearer ${config.apiToken}`,
18 | ...fetchOptions?.headers,
19 | 'Content-Type': 'application/json',
20 | },
21 | body: JSON.stringify({
22 | query,
23 | variables,
24 | }),
25 | })
26 |
27 | const json = await res.json()
28 | if (json.errors) {
29 | throw new FetcherError({
30 | errors: json.errors ?? [{ message: 'Failed to fetch Bigcommerce API' }],
31 | status: res.status,
32 | })
33 | }
34 |
35 | return { data: json.data, res }
36 | }
37 |
38 | export default fetchGraphqlApi
39 |
--------------------------------------------------------------------------------
/example /api/utils/fetch-store-api.ts:
--------------------------------------------------------------------------------
1 | import type { RequestInit, Response } from '@vercel/fetch'
2 | import { getConfig } from '..'
3 | import { BigcommerceApiError, BigcommerceNetworkError } from './errors'
4 | import fetch from './fetch'
5 |
6 | export default async function fetchStoreApi(
7 | endpoint: string,
8 | options?: RequestInit
9 | ): Promise {
10 | const config = getConfig()
11 | let res: Response
12 |
13 | try {
14 | res = await fetch(config.storeApiUrl + endpoint, {
15 | ...options,
16 | headers: {
17 | ...options?.headers,
18 | 'Content-Type': 'application/json',
19 | 'X-Auth-Token': config.storeApiToken,
20 | 'X-Auth-Client': config.storeApiClientId,
21 | },
22 | })
23 | } catch (error) {
24 | throw new BigcommerceNetworkError(
25 | `Fetch to Bigcommerce failed: ${error.message}`
26 | )
27 | }
28 |
29 | const contentType = res.headers.get('Content-Type')
30 | const isJSON = contentType?.includes('application/json')
31 |
32 | if (!res.ok) {
33 | const data = isJSON ? await res.json() : await getTextOrNull(res)
34 | const headers = getRawHeaders(res)
35 | const msg = `Big Commerce API error (${
36 | res.status
37 | }) \nHeaders: ${JSON.stringify(headers, null, 2)}\n${
38 | typeof data === 'string' ? data : JSON.stringify(data, null, 2)
39 | }`
40 |
41 | throw new BigcommerceApiError(msg, res, data)
42 | }
43 |
44 | if (res.status !== 204 && !isJSON) {
45 | throw new BigcommerceApiError(
46 | `Fetch to Bigcommerce API failed, expected JSON content but found: ${contentType}`,
47 | res
48 | )
49 | }
50 |
51 | // If something was removed, the response will be empty
52 | return res.status === 204 ? null : await res.json()
53 | }
54 |
55 | function getRawHeaders(res: Response) {
56 | const headers: { [key: string]: string } = {}
57 |
58 | res.headers.forEach((value, key) => {
59 | headers[key] = value
60 | })
61 |
62 | return headers
63 | }
64 |
65 | function getTextOrNull(res: Response) {
66 | try {
67 | return res.text()
68 | } catch (err) {
69 | return null
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/example /api/utils/fetch.ts:
--------------------------------------------------------------------------------
1 | import zeitFetch from '@vercel/fetch'
2 |
3 | export default zeitFetch()
4 |
--------------------------------------------------------------------------------
/example /api/utils/filter-edges.ts:
--------------------------------------------------------------------------------
1 | export default function filterEdges(
2 | edges: (T | null | undefined)[] | null | undefined
3 | ) {
4 | return edges?.filter((edge): edge is T => !!edge) ?? []
5 | }
6 |
--------------------------------------------------------------------------------
/example /api/utils/get-cart-cookie.ts:
--------------------------------------------------------------------------------
1 | import { serialize, CookieSerializeOptions } from 'cookie'
2 |
3 | export default function getCartCookie(
4 | name: string,
5 | cartId?: string,
6 | maxAge?: number
7 | ) {
8 | const options: CookieSerializeOptions =
9 | cartId && maxAge
10 | ? {
11 | maxAge,
12 | expires: new Date(Date.now() + maxAge * 1000),
13 | secure: process.env.NODE_ENV === 'production',
14 | path: '/',
15 | sameSite: 'lax',
16 | }
17 | : { maxAge: -1, path: '/' } // Removes the cookie
18 |
19 | return serialize(name, cartId || '', options)
20 | }
21 |
--------------------------------------------------------------------------------
/example /api/utils/is-allowed-method.ts:
--------------------------------------------------------------------------------
1 | import type { NextApiRequest, NextApiResponse } from 'next'
2 |
3 | export default function isAllowedMethod(
4 | req: NextApiRequest,
5 | res: NextApiResponse,
6 | allowedMethods: string[]
7 | ) {
8 | const methods = allowedMethods.includes('OPTIONS')
9 | ? allowedMethods
10 | : [...allowedMethods, 'OPTIONS']
11 |
12 | if (!req.method || !methods.includes(req.method)) {
13 | res.status(405)
14 | res.setHeader('Allow', methods.join(', '))
15 | res.end()
16 | return false
17 | }
18 |
19 | if (req.method === 'OPTIONS') {
20 | res.status(200)
21 | res.setHeader('Allow', methods.join(', '))
22 | res.setHeader('Content-Length', '0')
23 | res.end()
24 | return false
25 | }
26 |
27 | return true
28 | }
29 |
--------------------------------------------------------------------------------
/example /api/utils/parse-item.ts:
--------------------------------------------------------------------------------
1 | import type { ItemBody as WishlistItemBody } from '../wishlist'
2 | import type { ItemBody } from '../cart'
3 |
4 | export const parseWishlistItem = (item: WishlistItemBody) => ({
5 | product_id: item.productId,
6 | variant_id: item.variantId,
7 | })
8 |
9 | export const parseCartItem = (item: ItemBody) => ({
10 | quantity: item.quantity,
11 | product_id: item.productId,
12 | variant_id: item.variantId,
13 | })
14 |
--------------------------------------------------------------------------------
/example /api/utils/set-product-locale-meta.ts:
--------------------------------------------------------------------------------
1 | import type { ProductNode } from '../operations/get-all-products'
2 | import type { RecursivePartial } from './types'
3 |
4 | export default function setProductLocaleMeta(
5 | node: RecursivePartial
6 | ) {
7 | if (node.localeMeta?.edges) {
8 | node.localeMeta.edges = node.localeMeta.edges.filter((edge) => {
9 | const { key, value } = edge?.node ?? {}
10 | if (key && key in node) {
11 | ;(node as any)[key] = value
12 | return false
13 | }
14 | return true
15 | })
16 |
17 | if (!node.localeMeta.edges.length) {
18 | delete node.localeMeta
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/example /api/utils/types.ts:
--------------------------------------------------------------------------------
1 | export type RecursivePartial = {
2 | [P in keyof T]?: RecursivePartial
3 | }
4 |
5 | export type RecursiveRequired = {
6 | [P in keyof T]-?: RecursiveRequired
7 | }
8 |
--------------------------------------------------------------------------------
/example /api/wishlist/handlers/add-item.ts:
--------------------------------------------------------------------------------
1 | import type { WishlistHandlers } from '..'
2 | import getCustomerId from '../../operations/get-customer-id'
3 | import getCustomerWishlist from '../../operations/get-customer-wishlist'
4 | import { parseWishlistItem } from '../../utils/parse-item'
5 |
6 | // Returns the wishlist of the signed customer
7 | const addItem: WishlistHandlers['addItem'] = async ({
8 | res,
9 | body: { customerToken, item },
10 | config,
11 | }) => {
12 | if (!item) {
13 | return res.status(400).json({
14 | data: null,
15 | errors: [{ message: 'Missing item' }],
16 | })
17 | }
18 |
19 | const customerId =
20 | customerToken && (await getCustomerId({ customerToken, config }))
21 |
22 | if (!customerId) {
23 | return res.status(400).json({
24 | data: null,
25 | errors: [{ message: 'Invalid request' }],
26 | })
27 | }
28 |
29 | const { wishlist } = await getCustomerWishlist({
30 | variables: { customerId },
31 | config,
32 | })
33 | const options = {
34 | method: 'POST',
35 | body: JSON.stringify(
36 | wishlist
37 | ? {
38 | items: [parseWishlistItem(item)],
39 | }
40 | : {
41 | name: 'Wishlist',
42 | customer_id: customerId,
43 | items: [parseWishlistItem(item)],
44 | is_public: false,
45 | }
46 | ),
47 | }
48 |
49 | const { data } = wishlist
50 | ? await config.storeApiFetch(`/v3/wishlists/${wishlist.id}/items`, options)
51 | : await config.storeApiFetch('/v3/wishlists', options)
52 |
53 | res.status(200).json({ data })
54 | }
55 |
56 | export default addItem
57 |
--------------------------------------------------------------------------------
/example /api/wishlist/handlers/get-wishlist.ts:
--------------------------------------------------------------------------------
1 | import getCustomerId from '../../operations/get-customer-id'
2 | import getCustomerWishlist from '../../operations/get-customer-wishlist'
3 | import type { Wishlist, WishlistHandlers } from '..'
4 |
5 | // Return wishlist info
6 | const getWishlist: WishlistHandlers['getWishlist'] = async ({
7 | res,
8 | body: { customerToken, includeProducts },
9 | config,
10 | }) => {
11 | let result: { data?: Wishlist } = {}
12 |
13 | if (customerToken) {
14 | const customerId =
15 | customerToken && (await getCustomerId({ customerToken, config }))
16 |
17 | if (!customerId) {
18 | // If the customerToken is invalid, then this request is too
19 | return res.status(404).json({
20 | data: null,
21 | errors: [{ message: 'Wishlist not found' }],
22 | })
23 | }
24 |
25 | const { wishlist } = await getCustomerWishlist({
26 | variables: { customerId },
27 | includeProducts,
28 | config,
29 | })
30 |
31 | result = { data: wishlist }
32 | }
33 |
34 | res.status(200).json({ data: result.data ?? null })
35 | }
36 |
37 | export default getWishlist
38 |
--------------------------------------------------------------------------------
/example /api/wishlist/handlers/remove-item.ts:
--------------------------------------------------------------------------------
1 | import getCustomerId from '../../operations/get-customer-id'
2 | import getCustomerWishlist, {
3 | Wishlist,
4 | } from '../../operations/get-customer-wishlist'
5 | import type { WishlistHandlers } from '..'
6 |
7 | // Return current wishlist info
8 | const removeItem: WishlistHandlers['removeItem'] = async ({
9 | res,
10 | body: { customerToken, itemId },
11 | config,
12 | }) => {
13 | const customerId =
14 | customerToken && (await getCustomerId({ customerToken, config }))
15 | const { wishlist } =
16 | (customerId &&
17 | (await getCustomerWishlist({
18 | variables: { customerId },
19 | config,
20 | }))) ||
21 | {}
22 |
23 | if (!wishlist || !itemId) {
24 | return res.status(400).json({
25 | data: null,
26 | errors: [{ message: 'Invalid request' }],
27 | })
28 | }
29 |
30 | const result = await config.storeApiFetch<{ data: Wishlist } | null>(
31 | `/v3/wishlists/${wishlist.id}/items/${itemId}`,
32 | { method: 'DELETE' }
33 | )
34 | const data = result?.data ?? null
35 |
36 | res.status(200).json({ data })
37 | }
38 |
39 | export default removeItem
40 |
--------------------------------------------------------------------------------
/example /api/wishlist/index.ts:
--------------------------------------------------------------------------------
1 | import isAllowedMethod from '../utils/is-allowed-method'
2 | import createApiHandler, {
3 | BigcommerceApiHandler,
4 | BigcommerceHandler,
5 | } from '../utils/create-api-handler'
6 | import { BigcommerceApiError } from '../utils/errors'
7 | import type {
8 | Wishlist,
9 | WishlistItem,
10 | } from '../operations/get-customer-wishlist'
11 | import getWishlist from './handlers/get-wishlist'
12 | import addItem from './handlers/add-item'
13 | import removeItem from './handlers/remove-item'
14 |
15 | export type { Wishlist, WishlistItem }
16 |
17 | export type ItemBody = {
18 | productId: number
19 | variantId: number
20 | }
21 |
22 | export type AddItemBody = { item: ItemBody }
23 |
24 | export type RemoveItemBody = { itemId: string }
25 |
26 | export type WishlistBody = {
27 | customer_id: number
28 | is_public: number
29 | name: string
30 | items: any[]
31 | }
32 |
33 | export type AddWishlistBody = { wishlist: WishlistBody }
34 |
35 | export type WishlistHandlers = {
36 | getWishlist: BigcommerceHandler<
37 | Wishlist,
38 | { customerToken?: string; includeProducts?: boolean }
39 | >
40 | addItem: BigcommerceHandler<
41 | Wishlist,
42 | { customerToken?: string } & Partial
43 | >
44 | removeItem: BigcommerceHandler<
45 | Wishlist,
46 | { customerToken?: string } & Partial
47 | >
48 | }
49 |
50 | const METHODS = ['GET', 'POST', 'DELETE']
51 |
52 | // TODO: a complete implementation should have schema validation for `req.body`
53 | const wishlistApi: BigcommerceApiHandler = async (
54 | req,
55 | res,
56 | config,
57 | handlers
58 | ) => {
59 | if (!isAllowedMethod(req, res, METHODS)) return
60 |
61 | const { cookies } = req
62 | const customerToken = cookies[config.customerCookie]
63 |
64 | try {
65 | // Return current wishlist info
66 | if (req.method === 'GET') {
67 | const body = {
68 | customerToken,
69 | includeProducts: req.query.products === '1',
70 | }
71 | return await handlers['getWishlist']({ req, res, config, body })
72 | }
73 |
74 | // Add an item to the wishlist
75 | if (req.method === 'POST') {
76 | const body = { ...req.body, customerToken }
77 | return await handlers['addItem']({ req, res, config, body })
78 | }
79 |
80 | // Remove an item from the wishlist
81 | if (req.method === 'DELETE') {
82 | const body = { ...req.body, customerToken }
83 | return await handlers['removeItem']({ req, res, config, body })
84 | }
85 | } catch (error) {
86 | console.error(error)
87 |
88 | const message =
89 | error instanceof BigcommerceApiError
90 | ? 'An unexpected error ocurred with the Bigcommerce API'
91 | : 'An unexpected error ocurred'
92 |
93 | res.status(500).json({ data: null, errors: [{ message }] })
94 | }
95 | }
96 |
97 | export const handlers = {
98 | getWishlist,
99 | addItem,
100 | removeItem,
101 | }
102 |
103 | export default createApiHandler(wishlistApi, handlers, {})
104 |
--------------------------------------------------------------------------------
/example /auth/use-login.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { HookFetcher } from '../src/utils/types'
3 | import { CommerceError } from '../src/utils/errors'
4 | import useCommerceLogin from '../src/use-login'
5 | import type { LoginBody } from '../api/customers/login'
6 | import useCustomer from '../account/use-customer'
7 |
8 | const defaultOpts = {
9 | url: '/api/bigcommerce/customers/login',
10 | method: 'POST',
11 | }
12 |
13 | export type LoginInput = LoginBody
14 |
15 | export const fetcher: HookFetcher = (
16 | options,
17 | { email, password },
18 | fetch
19 | ) => {
20 | if (!(email && password)) {
21 | throw new CommerceError({
22 | message:
23 | 'A first name, last name, email and password are required to login',
24 | })
25 | }
26 |
27 | return fetch({
28 | ...defaultOpts,
29 | ...options,
30 | body: { email, password },
31 | })
32 | }
33 |
34 | export function extendHook(customFetcher: typeof fetcher) {
35 | const useLogin = () => {
36 | const { revalidate } = useCustomer()
37 | const fn = useCommerceLogin(defaultOpts, customFetcher)
38 |
39 | return useCallback(
40 | async function login(input: LoginInput) {
41 | const data = await fn(input)
42 | await revalidate()
43 | return data
44 | },
45 | [fn]
46 | )
47 | }
48 |
49 | useLogin.extend = extendHook
50 |
51 | return useLogin
52 | }
53 |
54 | export default extendHook(fetcher)
55 |
--------------------------------------------------------------------------------
/example /auth/use-logout.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { HookFetcher } from '../src/utils/types'
3 | import useCommerceLogout from '../src/use-logout'
4 | import useCustomer from '../account/use-customer'
5 |
6 | const defaultOpts = {
7 | url: '/api/bigcommerce/customers/logout',
8 | method: 'GET',
9 | }
10 |
11 | export const fetcher: HookFetcher = (options, _, fetch) => {
12 | return fetch({
13 | ...defaultOpts,
14 | ...options,
15 | })
16 | }
17 |
18 | export function extendHook(customFetcher: typeof fetcher) {
19 | const useLogout = () => {
20 | const { mutate } = useCustomer()
21 | const fn = useCommerceLogout(defaultOpts, customFetcher)
22 |
23 | return useCallback(
24 | async function login() {
25 | const data = await fn(null)
26 | await mutate(null, false)
27 | return data
28 | },
29 | [fn]
30 | )
31 | }
32 |
33 | useLogout.extend = extendHook
34 |
35 | return useLogout
36 | }
37 |
38 | export default extendHook(fetcher)
39 |
--------------------------------------------------------------------------------
/example /auth/use-signup.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { HookFetcher } from '../src/utils/types'
3 | import { CommerceError } from '../src/utils/errors'
4 | import useCommerceSignup from '../src/use-signup'
5 | import type { SignupBody } from '../api/customers/signup'
6 | import useCustomer from '../account/use-customer'
7 |
8 | const defaultOpts = {
9 | url: '/api/bigcommerce/customers/signup',
10 | method: 'POST',
11 | }
12 |
13 | export type SignupInput = SignupBody
14 |
15 | export const fetcher: HookFetcher = (
16 | options,
17 | { firstName, lastName, email, password },
18 | fetch
19 | ) => {
20 | if (!(firstName && lastName && email && password)) {
21 | throw new CommerceError({
22 | message:
23 | 'A first name, last name, email and password are required to signup',
24 | })
25 | }
26 |
27 | return fetch({
28 | ...defaultOpts,
29 | ...options,
30 | body: { firstName, lastName, email, password },
31 | })
32 | }
33 |
34 | export function extendHook(customFetcher: typeof fetcher) {
35 | const useSignup = () => {
36 | const { revalidate } = useCustomer()
37 | const fn = useCommerceSignup(defaultOpts, customFetcher)
38 |
39 | return useCallback(
40 | async function signup(input: SignupInput) {
41 | const data = await fn(input)
42 | await revalidate()
43 | return data
44 | },
45 | [fn]
46 | )
47 | }
48 |
49 | useSignup.extend = extendHook
50 |
51 | return useSignup
52 | }
53 |
54 | export default extendHook(fetcher)
55 |
--------------------------------------------------------------------------------
/example /cart/use-add-item.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { HookFetcher } from '../src/utils/types'
3 | import { CommerceError } from '../src/utils/errors'
4 | import useCartAddItem from '../src/cart/use-add-item'
5 | import type { ItemBody, AddItemBody } from '../api/cart'
6 | import useCart, { Cart } from './use-cart'
7 |
8 | const defaultOpts = {
9 | url: '/api/bigcommerce/cart',
10 | method: 'POST',
11 | }
12 |
13 | export type AddItemInput = ItemBody
14 |
15 | export const fetcher: HookFetcher = (
16 | options,
17 | { item },
18 | fetch
19 | ) => {
20 | if (
21 | item.quantity &&
22 | (!Number.isInteger(item.quantity) || item.quantity! < 1)
23 | ) {
24 | throw new CommerceError({
25 | message: 'The item quantity has to be a valid integer greater than 0',
26 | })
27 | }
28 |
29 | return fetch({
30 | ...defaultOpts,
31 | ...options,
32 | body: { item },
33 | })
34 | }
35 |
36 | export function extendHook(customFetcher: typeof fetcher) {
37 | const useAddItem = () => {
38 | const { mutate } = useCart()
39 | const fn = useCartAddItem(defaultOpts, customFetcher)
40 |
41 | return useCallback(
42 | async function addItem(input: AddItemInput) {
43 | const data = await fn({ item: input })
44 | await mutate(data, false)
45 | return data
46 | },
47 | [fn, mutate]
48 | )
49 | }
50 |
51 | useAddItem.extend = extendHook
52 |
53 | return useAddItem
54 | }
55 |
56 | export default extendHook(fetcher)
57 |
--------------------------------------------------------------------------------
/example /cart/use-cart-actions.tsx:
--------------------------------------------------------------------------------
1 | import useAddItem from './use-add-item'
2 | import useRemoveItem from './use-remove-item'
3 | import useUpdateItem from './use-update-item'
4 |
5 | // This hook is probably not going to be used, but it's here
6 | // to show how a commerce should be structuring it
7 | export default function useCartActions() {
8 | const addItem = useAddItem()
9 | const updateItem = useUpdateItem()
10 | const removeItem = useRemoveItem()
11 |
12 | return { addItem, updateItem, removeItem }
13 | }
14 |
--------------------------------------------------------------------------------
/example /cart/use-cart.tsx:
--------------------------------------------------------------------------------
1 | import type { HookFetcher } from '../src/utils/types'
2 | import type { SwrOptions } from '../src/utils/use-data'
3 | import useCommerceCart, { CartInput } from '../src/cart/use-cart'
4 | import type { Cart } from '../api/cart'
5 |
6 | const defaultOpts = {
7 | url: '/api/bigcommerce/cart',
8 | method: 'GET',
9 | }
10 |
11 | export type { Cart }
12 |
13 | export const fetcher: HookFetcher = (
14 | options,
15 | { cartId },
16 | fetch
17 | ) => {
18 | return cartId ? fetch({ ...defaultOpts, ...options }) : null
19 | }
20 |
21 | export function extendHook(
22 | customFetcher: typeof fetcher,
23 | swrOptions?: SwrOptions
24 | ) {
25 | const useCart = () => {
26 | const response = useCommerceCart(defaultOpts, [], customFetcher, {
27 | revalidateOnFocus: false,
28 | ...swrOptions,
29 | })
30 |
31 | // Uses a getter to only calculate the prop when required
32 | // response.data is also a getter and it's better to not trigger it early
33 | Object.defineProperty(response, 'isEmpty', {
34 | get() {
35 | return Object.values(response.data?.line_items ?? {}).every(
36 | (items) => !items.length
37 | )
38 | },
39 | set: (x) => x,
40 | })
41 |
42 | return response
43 | }
44 |
45 | useCart.extend = extendHook
46 |
47 | return useCart
48 | }
49 |
50 | export default extendHook(fetcher)
51 |
--------------------------------------------------------------------------------
/example /cart/use-remove-item.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { HookFetcher } from '../src/utils/types'
3 | import useCartRemoveItem from '../src/cart/use-remove-item'
4 | import type { RemoveItemBody } from '../api/cart'
5 | import useCart, { Cart } from './use-cart'
6 |
7 | const defaultOpts = {
8 | url: '/api/bigcommerce/cart',
9 | method: 'DELETE',
10 | }
11 |
12 | export type RemoveItemInput = {
13 | id: string
14 | }
15 |
16 | export const fetcher: HookFetcher = (
17 | options,
18 | { itemId },
19 | fetch
20 | ) => {
21 | return fetch({
22 | ...defaultOpts,
23 | ...options,
24 | body: { itemId },
25 | })
26 | }
27 |
28 | export function extendHook(customFetcher: typeof fetcher) {
29 | const useRemoveItem = (item?: any) => {
30 | const { mutate } = useCart()
31 | const fn = useCartRemoveItem(
32 | defaultOpts,
33 | customFetcher
34 | )
35 |
36 | return useCallback(
37 | async function removeItem(input: RemoveItemInput) {
38 | const data = await fn({ itemId: input.id ?? item?.id })
39 | await mutate(data, false)
40 | return data
41 | },
42 | [fn, mutate]
43 | )
44 | }
45 |
46 | useRemoveItem.extend = extendHook
47 |
48 | return useRemoveItem
49 | }
50 |
51 | export default extendHook(fetcher)
52 |
--------------------------------------------------------------------------------
/example /cart/use-update-item.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import debounce from 'lodash.debounce'
3 | import type { HookFetcher } from '../src/utils/types'
4 | import { CommerceError } from '../src/utils/errors'
5 | import useCartUpdateItem from '../src/cart/use-update-item'
6 | import type { ItemBody, UpdateItemBody } from '../api/cart'
7 | import { fetcher as removeFetcher } from './use-remove-item'
8 | import useCart, { Cart } from './use-cart'
9 |
10 | const defaultOpts = {
11 | url: '/api/bigcommerce/cart',
12 | method: 'PUT',
13 | }
14 |
15 | export type UpdateItemInput = Partial<{ id: string } & ItemBody>
16 |
17 | export const fetcher: HookFetcher = (
18 | options,
19 | { itemId, item },
20 | fetch
21 | ) => {
22 | if (Number.isInteger(item.quantity)) {
23 | // Also allow the update hook to remove an item if the quantity is lower than 1
24 | if (item.quantity! < 1) {
25 | return removeFetcher(null, { itemId }, fetch)
26 | }
27 | } else if (item.quantity) {
28 | throw new CommerceError({
29 | message: 'The item quantity has to be a valid integer',
30 | })
31 | }
32 |
33 | return fetch({
34 | ...defaultOpts,
35 | ...options,
36 | body: { itemId, item },
37 | })
38 | }
39 |
40 | function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
41 | const useUpdateItem = (item?: any) => {
42 | const { mutate } = useCart()
43 | const fn = useCartUpdateItem(
44 | defaultOpts,
45 | customFetcher
46 | )
47 |
48 | return useCallback(
49 | debounce(async (input: UpdateItemInput) => {
50 | const data = await fn({
51 | itemId: input.id ?? item?.id,
52 | item: {
53 | productId: input.productId ?? item?.product_id,
54 | variantId: input.productId ?? item?.variant_id,
55 | quantity: input.quantity,
56 | },
57 | })
58 | await mutate(data, false)
59 | return data
60 | }, cfg?.wait ?? 500),
61 | [fn, mutate]
62 | )
63 | }
64 |
65 | useUpdateItem.extend = extendHook
66 |
67 | return useUpdateItem
68 | }
69 |
70 | export default extendHook(fetcher)
71 |
--------------------------------------------------------------------------------
/example /index.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import * as React from 'react'
3 | import {
4 | CommerceConfig,
5 | CommerceProvider as CoreCommerceProvider,
6 | useCommerce as useCoreCommerce,
7 | } from './src'
8 | import { FetcherError } from './src/utils/errors'
9 |
10 | async function getText(res: Response) {
11 | try {
12 | return (await res.text()) || res.statusText
13 | } catch (error) {
14 | return res.statusText
15 | }
16 | }
17 |
18 | async function getError(res: Response) {
19 | if (res.headers.get('Content-Type')?.includes('application/json')) {
20 | const data = await res.json()
21 | return new FetcherError({ errors: data.errors, status: res.status })
22 | }
23 | return new FetcherError({ message: await getText(res), status: res.status })
24 | }
25 |
26 | export const bigcommerceConfig: CommerceConfig = {
27 | locale: 'en-us',
28 | cartCookie: 'bc_cartId',
29 | async fetcher({ url, method = 'GET', variables, body: bodyObj }) {
30 | const hasBody = Boolean(variables || bodyObj)
31 | const body = hasBody
32 | ? JSON.stringify(variables ? { variables } : bodyObj)
33 | : undefined
34 | const headers = hasBody ? { 'Content-Type': 'application/json' } : undefined
35 | const res = await fetch(url!, { method, body, headers })
36 |
37 | if (res.ok) {
38 | const { data } = await res.json()
39 | return data
40 | }
41 |
42 | throw await getError(res)
43 | },
44 | }
45 |
46 | export type BigcommerceConfig = Partial
47 |
48 | export type BigcommerceProps = {
49 | children?: ReactNode
50 | locale: string
51 | } & BigcommerceConfig
52 |
53 | export function CommerceProvider({ children, ...config }: BigcommerceProps) {
54 | return (
55 |
56 | {children}
57 |
58 | )
59 | }
60 |
61 | export const useCommerce = () => useCoreCommerce()
62 |
--------------------------------------------------------------------------------
/example /products/use-price.tsx:
--------------------------------------------------------------------------------
1 | export * from '../src/use-price'
2 | export { default } from '../src/use-price'
3 |
--------------------------------------------------------------------------------
/example /products/use-search.tsx:
--------------------------------------------------------------------------------
1 | import type { HookFetcher } from '../src/utils/types'
2 | import type { SwrOptions } from '../src/utils/use-data'
3 | import useCommerceSearch from '../src/products/use-search'
4 | import type { SearchProductsData } from '../api/catalog/products'
5 |
6 | const defaultOpts = {
7 | url: '/api/bigcommerce/catalog/products',
8 | method: 'GET',
9 | }
10 |
11 | export type SearchProductsInput = {
12 | search?: string
13 | categoryId?: number
14 | brandId?: number
15 | sort?: string
16 | }
17 |
18 | export const fetcher: HookFetcher = (
19 | options,
20 | { search, categoryId, brandId, sort },
21 | fetch
22 | ) => {
23 | // Use a dummy base as we only care about the relative path
24 | const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
25 |
26 | if (search) url.searchParams.set('search', search)
27 | if (Number.isInteger(categoryId))
28 | url.searchParams.set('category', String(categoryId))
29 | if (Number.isInteger(categoryId))
30 | url.searchParams.set('brand', String(brandId))
31 | if (sort) url.searchParams.set('sort', sort)
32 |
33 | return fetch({
34 | url: url.pathname + url.search,
35 | method: options?.method ?? defaultOpts.method,
36 | })
37 | }
38 |
39 | export function extendHook(
40 | customFetcher: typeof fetcher,
41 | swrOptions?: SwrOptions
42 | ) {
43 | const useSearch = (input: SearchProductsInput = {}) => {
44 | const response = useCommerceSearch(
45 | defaultOpts,
46 | [
47 | ['search', input.search],
48 | ['categoryId', input.categoryId],
49 | ['brandId', input.brandId],
50 | ['sort', input.sort],
51 | ],
52 | customFetcher,
53 | { revalidateOnFocus: false, ...swrOptions }
54 | )
55 |
56 | return response
57 | }
58 |
59 | useSearch.extend = extendHook
60 |
61 | return useSearch
62 | }
63 |
64 | export default extendHook(fetcher)
65 |
--------------------------------------------------------------------------------
/example /schema.graphql:
--------------------------------------------------------------------------------
1 | """
2 | Login result
3 | """
4 | type LoginResult {
5 | """
6 | The result of a login
7 | """
8 | result: String!
9 | }
10 |
11 | """
12 | Logout result
13 | """
14 | type LogoutResult {
15 | """
16 | The result of a logout
17 | """
18 | result: String!
19 | }
20 |
21 | type Mutation {
22 | login(email: String!, password: String!): LoginResult!
23 | logout: LogoutResult!
24 | }
25 |
26 | """
27 | Aggregated
28 | """
29 | type Aggregated {
30 | """
31 | Number of available products in stock. This can be 'null' if inventory is not set orif the store's Inventory Settings disable displaying stock levels on the storefront.
32 | """
33 | availableToSell: Long!
34 |
35 | """
36 | Indicates a threshold low-stock level. This can be 'null' if the inventory warning level is not set or if the store's Inventory Settings disable displaying stock levels on the storefront.
37 | """
38 | warningLevel: Int!
39 | }
40 |
41 | """
42 | Aggregated Product Inventory
43 | """
44 | type AggregatedInventory {
45 | """
46 | Number of available products in stock. This can be 'null' if inventory is not set orif the store's Inventory Settings disable displaying stock levels on the storefront.
47 | """
48 | availableToSell: Int!
49 |
50 | """
51 | Indicates a threshold low-stock level. This can be 'null' if the inventory warning level is not set or if the store's Inventory Settings disable displaying stock levels on the storefront.
52 | """
53 | warningLevel: Int!
54 | }
55 |
56 | """
57 | Brand
58 | """
59 | type Brand implements Node {
60 | """
61 | The ID of an object
62 | """
63 | id: ID!
64 |
65 | """
66 | Id of the brand.
67 | """
68 | entityId: Int!
69 |
70 | """
71 | Name of the brand.
72 | """
73 | name: String!
74 |
75 | """
76 | Default image for brand.
77 | """
78 | defaultImage: Image
79 |
80 | """
81 | Page title for the brand.
82 | """
83 | pageTitle: String!
84 |
85 | """
86 | Meta description for the brand.
87 | """
88 | metaDesc: String!
89 |
90 | """
91 | Meta keywords for the brand.
92 | """
93 | metaKeywords: [String!]!
94 |
95 | """
96 | Search keywords for the brand.
97 | """
98 | searchKeywords: [String!]!
99 |
100 | """
101 | Path for the brand page.
102 | """
103 | path: String!
104 | products(
105 | before: String
106 | after: String
107 | first: Int
108 | last: Int
109 | ): ProductConnection!
110 |
111 | """
112 | Metafield data related to a brand.
113 | """
114 | metafields(
115 | namespace: String!
116 | keys: [String!] = []
117 | before: String
118 | after: String
119 | first: Int
120 | last: Int
121 | ): MetafieldConnection!
122 | }
123 |
124 | """
125 | A connection to a list of items.
126 | """
127 | type BrandConnection {
128 | """
129 | Information to aid in pagination.
130 | """
131 | pageInfo: PageInfo!
132 |
133 | """
134 | A list of edges.
135 | """
136 | edges: [BrandEdge]
137 | }
138 |
139 | """
140 | An edge in a connection.
141 | """
142 | type BrandEdge {
143 | """
144 | The item at the end of the edge.
145 | """
146 | node: Brand!
147 |
148 | """
149 | A cursor for use in pagination.
150 | """
151 | cursor: String!
152 | }
153 |
154 | """
155 | Breadcrumb
156 | """
157 | type Breadcrumb {
158 | """
159 | Category id.
160 | """
161 | entityId: Int!
162 |
163 | """
164 | Name of the category.
165 | """
166 | name: String!
167 | }
168 |
169 | """
170 | A connection to a list of items.
171 | """
172 | type BreadcrumbConnection {
173 | """
174 | Information to aid in pagination.
175 | """
176 | pageInfo: PageInfo!
177 |
178 | """
179 | A list of edges.
180 | """
181 | edges: [BreadcrumbEdge]
182 | }
183 |
184 | """
185 | An edge in a connection.
186 | """
187 | type BreadcrumbEdge {
188 | """
189 | The item at the end of the edge.
190 | """
191 | node: Breadcrumb!
192 |
193 | """
194 | A cursor for use in pagination.
195 | """
196 | cursor: String!
197 | }
198 |
199 | """
200 | Bulk pricing tier that sets a fixed price for the product or variant.
201 | """
202 | type BulkPricingFixedPriceDiscount implements BulkPricingTier {
203 | """
204 | This price will override the current product price.
205 | """
206 | price: BigDecimal!
207 |
208 | """
209 | Minimum item quantity that applies to this bulk pricing tier.
210 | """
211 | minimumQuantity: Int!
212 |
213 | """
214 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound.
215 | """
216 | maximumQuantity: Int
217 | }
218 |
219 | """
220 | Bulk pricing tier that reduces the price of the product or variant by a percentage.
221 | """
222 | type BulkPricingPercentageDiscount implements BulkPricingTier {
223 | """
224 | The percentage that will be removed from the product price.
225 | """
226 | percentOff: BigDecimal!
227 |
228 | """
229 | Minimum item quantity that applies to this bulk pricing tier.
230 | """
231 | minimumQuantity: Int!
232 |
233 | """
234 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound.
235 | """
236 | maximumQuantity: Int
237 | }
238 |
239 | """
240 | Bulk pricing tier that will subtract an amount from the price of the product or variant.
241 | """
242 | type BulkPricingRelativePriceDiscount implements BulkPricingTier {
243 | """
244 | The price of the product/variant will be reduced by this priceAdjustment.
245 | """
246 | priceAdjustment: BigDecimal!
247 |
248 | """
249 | Minimum item quantity that applies to this bulk pricing tier.
250 | """
251 | minimumQuantity: Int!
252 |
253 | """
254 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound.
255 | """
256 | maximumQuantity: Int
257 | }
258 |
259 | """
260 | A set of bulk pricing tiers that define price discounts which apply when purchasing specified quantities of a product or variant.
261 | """
262 | interface BulkPricingTier {
263 | """
264 | Minimum item quantity that applies to this bulk pricing tier.
265 | """
266 | minimumQuantity: Int!
267 |
268 | """
269 | Maximum item quantity that applies to this bulk pricing tier - if not defined then the tier does not have an upper bound.
270 | """
271 | maximumQuantity: Int
272 | }
273 |
274 | """
275 | Product Option
276 | """
277 | interface CatalogProductOption {
278 | """
279 | Unique ID for the option.
280 | """
281 | entityId: Int!
282 |
283 | """
284 | Display name for the option.
285 | """
286 | displayName: String!
287 |
288 | """
289 | One of the option values is required to be selected for the checkout.
290 | """
291 | isRequired: Boolean!
292 | }
293 |
294 | """
295 | Product Option Value
296 | """
297 | interface CatalogProductOptionValue {
298 | """
299 | Unique ID for the option value.
300 | """
301 | entityId: Int!
302 |
303 | """
304 | Label for the option value.
305 | """
306 | label: String!
307 |
308 | """
309 | Indicates whether this value is the chosen default selected value.
310 | """
311 | isDefault: Boolean!
312 | }
313 |
314 | """
315 | Category
316 | """
317 | type Category implements Node {
318 | """
319 | The ID of an object
320 | """
321 | id: ID!
322 |
323 | """
324 | Unique ID for the category.
325 | """
326 | entityId: Int!
327 |
328 | """
329 | Category name.
330 | """
331 | name: String!
332 |
333 | """
334 | Category path.
335 | """
336 | path: String!
337 |
338 | """
339 | Default image for the category.
340 | """
341 | defaultImage: Image
342 |
343 | """
344 | Category description.
345 | """
346 | description: String!
347 |
348 | """
349 | Category breadcrumbs.
350 | """
351 | breadcrumbs(
352 | depth: Int!
353 | before: String
354 | after: String
355 | first: Int
356 | last: Int
357 | ): BreadcrumbConnection!
358 | products(
359 | before: String
360 | after: String
361 | first: Int
362 | last: Int
363 | ): ProductConnection!
364 |
365 | """
366 | Metafield data related to a category.
367 | """
368 | metafields(
369 | namespace: String!
370 | keys: [String!] = []
371 | before: String
372 | after: String
373 | first: Int
374 | last: Int
375 | ): MetafieldConnection!
376 | }
377 |
378 | """
379 | A connection to a list of items.
380 | """
381 | type CategoryConnection {
382 | """
383 | Information to aid in pagination.
384 | """
385 | pageInfo: PageInfo!
386 |
387 | """
388 | A list of edges.
389 | """
390 | edges: [CategoryEdge]
391 | }
392 |
393 | """
394 | An edge in a connection.
395 | """
396 | type CategoryEdge {
397 | """
398 | The item at the end of the edge.
399 | """
400 | node: Category!
401 |
402 | """
403 | A cursor for use in pagination.
404 | """
405 | cursor: String!
406 | }
407 |
408 | """
409 | An item in a tree of categories.
410 | """
411 | type CategoryTreeItem {
412 | """
413 | The id category.
414 | """
415 | entityId: Int!
416 |
417 | """
418 | The name of category.
419 | """
420 | name: String!
421 |
422 | """
423 | Path assigned to this category
424 | """
425 | path: String!
426 |
427 | """
428 | The description of this category.
429 | """
430 | description: String!
431 |
432 | """
433 | The number of products in this category.
434 | """
435 | productCount: Int!
436 |
437 | """
438 | Subcategories of this category
439 | """
440 | children: [CategoryTreeItem!]!
441 | }
442 |
443 | """
444 | A simple yes/no question represented by a checkbox.
445 | """
446 | type CheckboxOption implements CatalogProductOption {
447 | """
448 | Indicates the default checked status.
449 | """
450 | checkedByDefault: Boolean!
451 |
452 | """
453 | Unique ID for the option.
454 | """
455 | entityId: Int!
456 |
457 | """
458 | Display name for the option.
459 | """
460 | displayName: String!
461 |
462 | """
463 | One of the option values is required to be selected for the checkout.
464 | """
465 | isRequired: Boolean!
466 | }
467 |
468 | """
469 | Contact field
470 | """
471 | type ContactField {
472 | """
473 | Store address line.
474 | """
475 | address: String!
476 |
477 | """
478 | Store country.
479 | """
480 | country: String!
481 |
482 | """
483 | Store address type.
484 | """
485 | addressType: String!
486 |
487 | """
488 | Store email.
489 | """
490 | email: String!
491 |
492 | """
493 | Store phone number.
494 | """
495 | phone: String!
496 | }
497 |
498 | """
499 | Custom field
500 | """
501 | type CustomField {
502 | """
503 | Custom field id.
504 | """
505 | entityId: Int!
506 |
507 | """
508 | Name of the custom field.
509 | """
510 | name: String!
511 |
512 | """
513 | Value of the custom field.
514 | """
515 | value: String!
516 | }
517 |
518 | """
519 | A connection to a list of items.
520 | """
521 | type CustomFieldConnection {
522 | """
523 | Information to aid in pagination.
524 | """
525 | pageInfo: PageInfo!
526 |
527 | """
528 | A list of edges.
529 | """
530 | edges: [CustomFieldEdge]
531 | }
532 |
533 | """
534 | An edge in a connection.
535 | """
536 | type CustomFieldEdge {
537 | """
538 | The item at the end of the edge.
539 | """
540 | node: CustomField!
541 |
542 | """
543 | A cursor for use in pagination.
544 | """
545 | cursor: String!
546 | }
547 |
548 | """
549 | A customer that shops on a store
550 | """
551 | type Customer {
552 | """
553 | The ID of the customer.
554 | """
555 | entityId: Int!
556 |
557 | """
558 | The company name of the customer.
559 | """
560 | company: String!
561 |
562 | """
563 | The customer group id of the customer.
564 | """
565 | customerGroupId: Int!
566 |
567 | """
568 | The email address of the customer.
569 | """
570 | email: String!
571 |
572 | """
573 | The first name of the customer.
574 | """
575 | firstName: String!
576 |
577 | """
578 | The last name of the customer.
579 | """
580 | lastName: String!
581 |
582 | """
583 | The notes of the customer.
584 | """
585 | notes: String!
586 |
587 | """
588 | The phone number of the customer.
589 | """
590 | phone: String!
591 |
592 | """
593 | The tax exempt category of the customer.
594 | """
595 | taxExemptCategory: String!
596 |
597 | """
598 | Customer addresses count.
599 | """
600 | addressCount: Int!
601 |
602 | """
603 | Customer attributes count.
604 | """
605 | attributeCount: Int!
606 |
607 | """
608 | Customer store credit.
609 | """
610 | storeCredit: [Money!]!
611 |
612 | """
613 | Customer attributes.
614 | """
615 | attributes: CustomerAttributes!
616 | }
617 |
618 | """
619 | A custom, store-specific attribute for a customer
620 | """
621 | type CustomerAttribute {
622 | """
623 | The ID of the custom customer attribute
624 | """
625 | entityId: Int!
626 |
627 | """
628 | The value of the custom customer attribute
629 | """
630 | value: String
631 |
632 | """
633 | The name of the custom customer attribute
634 | """
635 | name: String!
636 | }
637 |
638 | """
639 | Custom, store-specific customer attributes
640 | """
641 | type CustomerAttributes {
642 | attribute(
643 | """
644 | The ID of the customer attribute
645 | """
646 | entityId: Int!
647 | ): CustomerAttribute!
648 | }
649 |
650 | """
651 | A calendar for allowing selection of a date.
652 | """
653 | type DateFieldOption implements CatalogProductOption {
654 | """
655 | Unique ID for the option.
656 | """
657 | entityId: Int!
658 |
659 | """
660 | Display name for the option.
661 | """
662 | displayName: String!
663 |
664 | """
665 | One of the option values is required to be selected for the checkout.
666 | """
667 | isRequired: Boolean!
668 | }
669 |
670 | scalar DateTime
671 |
672 | """
673 | Date Time Extended
674 | """
675 | type DateTimeExtended {
676 | """
677 | ISO-8601 formatted date in UTC
678 | """
679 | utc: DateTime!
680 | }
681 |
682 | """
683 | Display field
684 | """
685 | type DisplayField {
686 | """
687 | Short date format.
688 | """
689 | shortDateFormat: String!
690 |
691 | """
692 | Extended date format.
693 | """
694 | extendedDateFormat: String!
695 | }
696 |
697 | """
698 | A form allowing selection and uploading of a file from the user's local computer.
699 | """
700 | type FileUploadFieldOption implements CatalogProductOption {
701 | """
702 | Unique ID for the option.
703 | """
704 | entityId: Int!
705 |
706 | """
707 | Display name for the option.
708 | """
709 | displayName: String!
710 |
711 | """
712 | One of the option values is required to be selected for the checkout.
713 | """
714 | isRequired: Boolean!
715 | }
716 |
717 | """
718 | Image
719 | """
720 | type Image {
721 | """
722 | Absolute path to image using store CDN.
723 | """
724 | url(width: Int!, height: Int): String!
725 |
726 | """
727 | Absolute path to original image using store CDN.
728 | """
729 | urlOriginal: String!
730 |
731 | """
732 | Text description of an image that can be used for SEO and/or accessibility purposes.
733 | """
734 | altText: String!
735 |
736 | """
737 | Indicates whether this is the primary image.
738 | """
739 | isDefault: Boolean!
740 | }
741 |
742 | """
743 | A connection to a list of items.
744 | """
745 | type ImageConnection {
746 | """
747 | Information to aid in pagination.
748 | """
749 | pageInfo: PageInfo!
750 |
751 | """
752 | A list of edges.
753 | """
754 | edges: [ImageEdge]
755 | }
756 |
757 | """
758 | An edge in a connection.
759 | """
760 | type ImageEdge {
761 | """
762 | The item at the end of the edge.
763 | """
764 | node: Image!
765 |
766 | """
767 | A cursor for use in pagination.
768 | """
769 | cursor: String!
770 | }
771 |
772 | """
773 | An inventory
774 | """
775 | type Inventory {
776 | """
777 | Locations
778 | """
779 | locations(
780 | entityIds: [Int!]
781 | codes: [String!]
782 | typeIds: [String!]
783 | before: String
784 | after: String
785 | first: Int
786 | last: Int
787 | ): LocationConnection!
788 | }
789 |
790 | """
791 | Inventory By Locations
792 | """
793 | type InventoryByLocations {
794 | """
795 | Location id.
796 | """
797 | locationEntityId: Long!
798 |
799 | """
800 | Number of available products in stock.
801 | """
802 | availableToSell: Long!
803 |
804 | """
805 | Indicates a threshold low-stock level.
806 | """
807 | warningLevel: Int!
808 |
809 | """
810 | Indicates whether this product is in stock.
811 | """
812 | isInStock: Boolean!
813 | }
814 |
815 | """
816 | A connection to a list of items.
817 | """
818 | type LocationConnection {
819 | """
820 | Information to aid in pagination.
821 | """
822 | pageInfo: PageInfo!
823 |
824 | """
825 | A list of edges.
826 | """
827 | edges: [LocationEdge]
828 | }
829 |
830 | """
831 | An edge in a connection.
832 | """
833 | type LocationEdge {
834 | """
835 | The item at the end of the edge.
836 | """
837 | node: InventoryByLocations!
838 |
839 | """
840 | A cursor for use in pagination.
841 | """
842 | cursor: String!
843 | }
844 |
845 | """
846 | Logo field
847 | """
848 | type LogoField {
849 | """
850 | Logo title.
851 | """
852 | title: String!
853 |
854 | """
855 | Store logo image.
856 | """
857 | image: Image!
858 | }
859 |
860 | """
861 | Measurement
862 | """
863 | type Measurement {
864 | """
865 | Unformatted weight measurement value.
866 | """
867 | value: Float!
868 |
869 | """
870 | Unit of measurement.
871 | """
872 | unit: String!
873 | }
874 |
875 | """
876 | A connection to a list of items.
877 | """
878 | type MetafieldConnection {
879 | """
880 | Information to aid in pagination.
881 | """
882 | pageInfo: PageInfo!
883 |
884 | """
885 | A list of edges.
886 | """
887 | edges: [MetafieldEdge]
888 | }
889 |
890 | """
891 | An edge in a connection.
892 | """
893 | type MetafieldEdge {
894 | """
895 | The item at the end of the edge.
896 | """
897 | node: Metafields!
898 |
899 | """
900 | A cursor for use in pagination.
901 | """
902 | cursor: String!
903 | }
904 |
905 | """
906 | Key/Value pairs of data attached tied to a resource entity (product, brand, category, etc.)
907 | """
908 | type Metafields {
909 | """
910 | The ID of an object
911 | """
912 | id: ID!
913 |
914 | """
915 | The ID of the metafield when referencing via our backend API.
916 | """
917 | entityId: Int!
918 |
919 | """
920 | A label for identifying a metafield data value.
921 | """
922 | key: String!
923 |
924 | """
925 | A metafield value.
926 | """
927 | value: String!
928 | }
929 |
930 | """
931 | A money object - includes currency code and a money amount
932 | """
933 | type Money {
934 | """
935 | Currency code of the current money.
936 | """
937 | currencyCode: String!
938 |
939 | """
940 | The amount of money.
941 | """
942 | value: BigDecimal!
943 | }
944 |
945 | """
946 | A min and max pair of money objects
947 | """
948 | type MoneyRange {
949 | """
950 | Minimum money object.
951 | """
952 | min: Money!
953 |
954 | """
955 | Maximum money object.
956 | """
957 | max: Money!
958 | }
959 |
960 | """
961 | A multi-line text input field, aka a text box.
962 | """
963 | type MultiLineTextFieldOption implements CatalogProductOption {
964 | """
965 | Unique ID for the option.
966 | """
967 | entityId: Int!
968 |
969 | """
970 | Display name for the option.
971 | """
972 | displayName: String!
973 |
974 | """
975 | One of the option values is required to be selected for the checkout.
976 | """
977 | isRequired: Boolean!
978 | }
979 |
980 | """
981 | An option type that has a fixed list of values.
982 | """
983 | type MultipleChoiceOption implements CatalogProductOption {
984 | """
985 | The chosen display style for this multiple choice option.
986 | """
987 | displayStyle: String!
988 |
989 | """
990 | List of option values.
991 | """
992 | values(
993 | before: String
994 | after: String
995 | first: Int
996 | last: Int
997 | ): ProductOptionValueConnection!
998 |
999 | """
1000 | Unique ID for the option.
1001 | """
1002 | entityId: Int!
1003 |
1004 | """
1005 | Display name for the option.
1006 | """
1007 | displayName: String!
1008 |
1009 | """
1010 | One of the option values is required to be selected for the checkout.
1011 | """
1012 | isRequired: Boolean!
1013 | }
1014 |
1015 | """
1016 | A simple multiple choice value comprised of an id and a label.
1017 | """
1018 | type MultipleChoiceOptionValue implements CatalogProductOptionValue {
1019 | """
1020 | Unique ID for the option value.
1021 | """
1022 | entityId: Int!
1023 |
1024 | """
1025 | Label for the option value.
1026 | """
1027 | label: String!
1028 |
1029 | """
1030 | Indicates whether this value is the chosen default selected value.
1031 | """
1032 | isDefault: Boolean!
1033 | }
1034 |
1035 | """
1036 | An object with an ID
1037 | """
1038 | interface Node {
1039 | """
1040 | The id of the object.
1041 | """
1042 | id: ID!
1043 | }
1044 |
1045 | """
1046 | A single line text input field that only accepts numbers.
1047 | """
1048 | type NumberFieldOption implements CatalogProductOption {
1049 | """
1050 | Unique ID for the option.
1051 | """
1052 | entityId: Int!
1053 |
1054 | """
1055 | Display name for the option.
1056 | """
1057 | displayName: String!
1058 |
1059 | """
1060 | One of the option values is required to be selected for the checkout.
1061 | """
1062 | isRequired: Boolean!
1063 | }
1064 |
1065 | """
1066 | A connection to a list of items.
1067 | """
1068 | type OptionConnection {
1069 | """
1070 | Information to aid in pagination.
1071 | """
1072 | pageInfo: PageInfo!
1073 |
1074 | """
1075 | A list of edges.
1076 | """
1077 | edges: [OptionEdge]
1078 | }
1079 |
1080 | """
1081 | An edge in a connection.
1082 | """
1083 | type OptionEdge {
1084 | """
1085 | The item at the end of the edge.
1086 | """
1087 | node: ProductOption!
1088 |
1089 | """
1090 | A cursor for use in pagination.
1091 | """
1092 | cursor: String!
1093 | }
1094 |
1095 | """
1096 | A connection to a list of items.
1097 | """
1098 | type OptionValueConnection {
1099 | """
1100 | Information to aid in pagination.
1101 | """
1102 | pageInfo: PageInfo!
1103 |
1104 | """
1105 | A list of edges.
1106 | """
1107 | edges: [OptionValueEdge]
1108 | }
1109 |
1110 | """
1111 | An edge in a connection.
1112 | """
1113 | type OptionValueEdge {
1114 | """
1115 | The item at the end of the edge.
1116 | """
1117 | node: ProductOptionValue!
1118 |
1119 | """
1120 | A cursor for use in pagination.
1121 | """
1122 | cursor: String!
1123 | }
1124 |
1125 | input OptionValueId {
1126 | optionEntityId: Int!
1127 | valueEntityId: Int!
1128 | }
1129 |
1130 | """
1131 | Information about pagination in a connection.
1132 | """
1133 | type PageInfo {
1134 | """
1135 | When paginating forwards, are there more items?
1136 | """
1137 | hasNextPage: Boolean!
1138 |
1139 | """
1140 | When paginating backwards, are there more items?
1141 | """
1142 | hasPreviousPage: Boolean!
1143 |
1144 | """
1145 | When paginating backwards, the cursor to continue.
1146 | """
1147 | startCursor: String
1148 |
1149 | """
1150 | When paginating forwards, the cursor to continue.
1151 | """
1152 | endCursor: String
1153 | }
1154 |
1155 | """
1156 | The min and max range of prices that apply to this product.
1157 | """
1158 | type PriceRanges {
1159 | """
1160 | Product price min/max range.
1161 | """
1162 | priceRange: MoneyRange!
1163 |
1164 | """
1165 | Product retail price min/max range.
1166 | """
1167 | retailPriceRange: MoneyRange
1168 | }
1169 |
1170 | """
1171 | The various prices that can be set on a product.
1172 | """
1173 | type Prices {
1174 | """
1175 | Calculated price of the product.
1176 | """
1177 | price: Money!
1178 |
1179 | """
1180 | Sale price of the product.
1181 | """
1182 | salePrice: Money
1183 |
1184 | """
1185 | Original price of the product.
1186 | """
1187 | basePrice: Money
1188 |
1189 | """
1190 | Retail price of the product.
1191 | """
1192 | retailPrice: Money
1193 |
1194 | """
1195 | Minimum advertised price of the product.
1196 | """
1197 | mapPrice: Money
1198 |
1199 | """
1200 | Product price min/max range.
1201 | """
1202 | priceRange: MoneyRange!
1203 |
1204 | """
1205 | Product retail price min/max range.
1206 | """
1207 | retailPriceRange: MoneyRange
1208 |
1209 | """
1210 | The difference between the retail price (MSRP) and the current price, which can be presented to the shopper as their savings.
1211 | """
1212 | saved: Money
1213 |
1214 | """
1215 | List of bulk pricing tiers applicable to a product or variant.
1216 | """
1217 | bulkPricing: [BulkPricingTier!]!
1218 | }
1219 |
1220 | """
1221 | Product
1222 | """
1223 | type Product implements Node {
1224 | """
1225 | The ID of an object
1226 | """
1227 | id: ID!
1228 |
1229 | """
1230 | Id of the product.
1231 | """
1232 | entityId: Int!
1233 |
1234 | """
1235 | Default product variant when no options are selected.
1236 | """
1237 | sku: String!
1238 |
1239 | """
1240 | Relative URL path to product page.
1241 | """
1242 | path: String!
1243 |
1244 | """
1245 | Name of the product.
1246 | """
1247 | name: String!
1248 |
1249 | """
1250 | Description of the product.
1251 | """
1252 | description: String!
1253 |
1254 | """
1255 | Description of the product in plain text.
1256 | """
1257 | plainTextDescription(characterLimit: Int = 120): String!
1258 |
1259 | """
1260 | Warranty information of the product.
1261 | """
1262 | warranty: String!
1263 |
1264 | """
1265 | Minimum purchasable quantity for this product in a single order.
1266 | """
1267 | minPurchaseQuantity: Int
1268 |
1269 | """
1270 | Maximum purchasable quantity for this product in a single order.
1271 | """
1272 | maxPurchaseQuantity: Int
1273 |
1274 | """
1275 | Absolute URL path for adding a product to cart.
1276 | """
1277 | addToCartUrl: String!
1278 |
1279 | """
1280 | Absolute URL path for adding a product to customer's wishlist.
1281 | """
1282 | addToWishlistUrl: String!
1283 |
1284 | """
1285 | Prices object determined by supplied product ID, variant ID, and selected option IDs.
1286 | """
1287 | prices(includeTax: Boolean = false, currencyCode: currencyCode): Prices
1288 |
1289 | """
1290 | The minimum and maximum price of this product based on variant pricing and/or modifier price rules.
1291 | """
1292 | priceRanges(includeTax: Boolean = false): PriceRanges
1293 | @deprecated(reason: "Use priceRanges inside prices node instead.")
1294 |
1295 | """
1296 | Weight of the product.
1297 | """
1298 | weight: Measurement
1299 |
1300 | """
1301 | Height of the product.
1302 | """
1303 | height: Measurement
1304 |
1305 | """
1306 | Width of the product.
1307 | """
1308 | width: Measurement
1309 |
1310 | """
1311 | Depth of the product.
1312 | """
1313 | depth: Measurement
1314 |
1315 | """
1316 | Product options.
1317 | """
1318 | options(
1319 | before: String
1320 | after: String
1321 | first: Int
1322 | last: Int
1323 | ): OptionConnection!
1324 |
1325 | """
1326 | Product options.
1327 | """
1328 | productOptions(
1329 | before: String
1330 | after: String
1331 | first: Int
1332 | last: Int
1333 | ): ProductOptionConnection!
1334 |
1335 | """
1336 | Summary of the product reviews, includes the total number of reviews submitted and summation of the ratings on the reviews (ratings range from 0-5 per review).
1337 | """
1338 | reviewSummary: Reviews!
1339 |
1340 | """
1341 | Type of product, ex: physical, digital
1342 | """
1343 | type: String!
1344 |
1345 | """
1346 | The availability state of the product.
1347 | """
1348 | availability: String!
1349 | @deprecated(reason: "Use status inside availabilityV2 instead.")
1350 |
1351 | """
1352 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'.
1353 | """
1354 | availabilityDescription: String!
1355 | @deprecated(reason: "Use description inside availabilityV2 instead.")
1356 |
1357 | """
1358 | The availability state of the product.
1359 | """
1360 | availabilityV2: ProductAvailability!
1361 |
1362 | """
1363 | List of categories associated with the product.
1364 | """
1365 | categories(
1366 | before: String
1367 | after: String
1368 | first: Int
1369 | last: Int
1370 | ): CategoryConnection!
1371 |
1372 | """
1373 | Brand associated with the product.
1374 | """
1375 | brand: Brand
1376 |
1377 | """
1378 | Variants associated with the product.
1379 | """
1380 | variants(
1381 | before: String
1382 | after: String
1383 | first: Int
1384 | last: Int
1385 | entityIds: [Int!] = []
1386 | optionValueIds: [OptionValueId!] = []
1387 | ): VariantConnection!
1388 |
1389 | """
1390 | Custom fields of the product.
1391 | """
1392 | customFields(
1393 | names: [String!] = []
1394 | before: String
1395 | after: String
1396 | first: Int
1397 | last: Int
1398 | ): CustomFieldConnection!
1399 |
1400 | """
1401 | A list of the images for a product.
1402 | """
1403 | images(before: String, after: String, first: Int, last: Int): ImageConnection!
1404 |
1405 | """
1406 | Default image for a product.
1407 | """
1408 | defaultImage: Image
1409 |
1410 | """
1411 | Related products for this product.
1412 | """
1413 | relatedProducts(
1414 | before: String
1415 | after: String
1416 | first: Int
1417 | last: Int
1418 | ): RelatedProductsConnection!
1419 |
1420 | """
1421 | Inventory information of the product.
1422 | """
1423 | inventory: ProductInventory!
1424 |
1425 | """
1426 | Metafield data related to a product.
1427 | """
1428 | metafields(
1429 | namespace: String!
1430 | keys: [String!] = []
1431 | before: String
1432 | after: String
1433 | first: Int
1434 | last: Int
1435 | ): MetafieldConnection!
1436 |
1437 | """
1438 | Product creation date
1439 | """
1440 | createdAt: DateTimeExtended!
1441 | @deprecated(reason: "Alpha version. Do not use in production.")
1442 | }
1443 |
1444 | """
1445 | Product availability
1446 | """
1447 | interface ProductAvailability {
1448 | """
1449 | The availability state of the product.
1450 | """
1451 | status: ProductAvailabilityStatus!
1452 |
1453 | """
1454 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'.
1455 | """
1456 | description: String!
1457 | }
1458 |
1459 | """
1460 | Product availability status
1461 | """
1462 | enum ProductAvailabilityStatus {
1463 | Available
1464 | Preorder
1465 | Unavailable
1466 | }
1467 |
1468 | """
1469 | Available Product
1470 | """
1471 | type ProductAvailable implements ProductAvailability {
1472 | """
1473 | The availability state of the product.
1474 | """
1475 | status: ProductAvailabilityStatus!
1476 |
1477 | """
1478 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'.
1479 | """
1480 | description: String!
1481 | }
1482 |
1483 | """
1484 | A connection to a list of items.
1485 | """
1486 | type ProductConnection {
1487 | """
1488 | Information to aid in pagination.
1489 | """
1490 | pageInfo: PageInfo!
1491 |
1492 | """
1493 | A list of edges.
1494 | """
1495 | edges: [ProductEdge]
1496 | }
1497 |
1498 | """
1499 | An edge in a connection.
1500 | """
1501 | type ProductEdge {
1502 | """
1503 | The item at the end of the edge.
1504 | """
1505 | node: Product!
1506 |
1507 | """
1508 | A cursor for use in pagination.
1509 | """
1510 | cursor: String!
1511 | }
1512 |
1513 | """
1514 | Product Inventory Information
1515 | """
1516 | type ProductInventory {
1517 | """
1518 | Indicates whether this product is in stock.
1519 | """
1520 | isInStock: Boolean!
1521 |
1522 | """
1523 | Indicates whether this product's inventory is being tracked on variant level. If true, you may wish to check the variants node to understand the true inventory of each individual variant, rather than relying on this product-level aggregate to understand how many items may be added to cart.
1524 | """
1525 | hasVariantInventory: Boolean!
1526 |
1527 | """
1528 | Aggregated product inventory information. This data may not be available if not set or if the store's Inventory Settings have disabled displaying stock levels on the storefront.
1529 | """
1530 | aggregated: AggregatedInventory
1531 | }
1532 |
1533 | """
1534 | Product Option
1535 | """
1536 | type ProductOption {
1537 | """
1538 | Unique ID for the option.
1539 | """
1540 | entityId: Int!
1541 |
1542 | """
1543 | Display name for the option.
1544 | """
1545 | displayName: String!
1546 |
1547 | """
1548 | One of the option values is required to be selected for the checkout.
1549 | """
1550 | isRequired: Boolean!
1551 |
1552 | """
1553 | Option values.
1554 | """
1555 | values(
1556 | before: String
1557 | after: String
1558 | first: Int
1559 | last: Int
1560 | ): OptionValueConnection!
1561 | }
1562 |
1563 | """
1564 | A connection to a list of items.
1565 | """
1566 | type ProductOptionConnection {
1567 | """
1568 | Information to aid in pagination.
1569 | """
1570 | pageInfo: PageInfo!
1571 |
1572 | """
1573 | A list of edges.
1574 | """
1575 | edges: [ProductOptionEdge]
1576 | }
1577 |
1578 | """
1579 | An edge in a connection.
1580 | """
1581 | type ProductOptionEdge {
1582 | """
1583 | The item at the end of the edge.
1584 | """
1585 | node: CatalogProductOption!
1586 |
1587 | """
1588 | A cursor for use in pagination.
1589 | """
1590 | cursor: String!
1591 | }
1592 |
1593 | """
1594 | Product Option Value
1595 | """
1596 | type ProductOptionValue {
1597 | """
1598 | Unique ID for the option value.
1599 | """
1600 | entityId: Int!
1601 |
1602 | """
1603 | Label for the option value.
1604 | """
1605 | label: String!
1606 | }
1607 |
1608 | """
1609 | A connection to a list of items.
1610 | """
1611 | type ProductOptionValueConnection {
1612 | """
1613 | Information to aid in pagination.
1614 | """
1615 | pageInfo: PageInfo!
1616 |
1617 | """
1618 | A list of edges.
1619 | """
1620 | edges: [ProductOptionValueEdge]
1621 | }
1622 |
1623 | """
1624 | An edge in a connection.
1625 | """
1626 | type ProductOptionValueEdge {
1627 | """
1628 | The item at the end of the edge.
1629 | """
1630 | node: CatalogProductOptionValue!
1631 |
1632 | """
1633 | A cursor for use in pagination.
1634 | """
1635 | cursor: String!
1636 | }
1637 |
1638 | """
1639 | A Product PickList Value - a product to be mapped to the base product if selected.
1640 | """
1641 | type ProductPickListOptionValue implements CatalogProductOptionValue {
1642 | """
1643 | The ID of the product associated with this option value.
1644 | """
1645 | productId: Int!
1646 |
1647 | """
1648 | Unique ID for the option value.
1649 | """
1650 | entityId: Int!
1651 |
1652 | """
1653 | Label for the option value.
1654 | """
1655 | label: String!
1656 |
1657 | """
1658 | Indicates whether this value is the chosen default selected value.
1659 | """
1660 | isDefault: Boolean!
1661 | }
1662 |
1663 | """
1664 | PreOrder Product
1665 | """
1666 | type ProductPreOrder implements ProductAvailability {
1667 | """
1668 | The message to be shown in the store when a product is put into the pre-order availability state, e.g. "Expected release date is %%DATE%%"
1669 | """
1670 | message: String
1671 |
1672 | """
1673 | Product release date
1674 | """
1675 | willBeReleasedAt: DateTimeExtended
1676 |
1677 | """
1678 | The availability state of the product.
1679 | """
1680 | status: ProductAvailabilityStatus!
1681 |
1682 | """
1683 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'.
1684 | """
1685 | description: String!
1686 | }
1687 |
1688 | """
1689 | Unavailable Product
1690 | """
1691 | type ProductUnavailable implements ProductAvailability {
1692 | """
1693 | The message to be shown in the store when "Call for pricing" is enabled for this product, e.g. "Contact us at 555-5555"
1694 | """
1695 | message: String
1696 |
1697 | """
1698 | The availability state of the product.
1699 | """
1700 | status: ProductAvailabilityStatus!
1701 |
1702 | """
1703 | A few words telling the customer how long it will normally take to ship this product, such as 'Usually ships in 24 hours'.
1704 | """
1705 | description: String!
1706 | }
1707 |
1708 | type Query {
1709 | site: Site!
1710 |
1711 | """
1712 | The currently logged in customer.
1713 | """
1714 | customer: Customer
1715 |
1716 | """
1717 | Fetches an object given its ID
1718 | """
1719 | node(
1720 | """
1721 | The ID of an object
1722 | """
1723 | id: ID!
1724 | ): Node
1725 | inventory: Inventory!
1726 | @deprecated(reason: "Alpha version. Do not use in production.")
1727 | }
1728 |
1729 | """
1730 | A connection to a list of items.
1731 | """
1732 | type RelatedProductsConnection {
1733 | """
1734 | Information to aid in pagination.
1735 | """
1736 | pageInfo: PageInfo!
1737 |
1738 | """
1739 | A list of edges.
1740 | """
1741 | edges: [RelatedProductsEdge]
1742 | }
1743 |
1744 | """
1745 | An edge in a connection.
1746 | """
1747 | type RelatedProductsEdge {
1748 | """
1749 | The item at the end of the edge.
1750 | """
1751 | node: Product!
1752 |
1753 | """
1754 | A cursor for use in pagination.
1755 | """
1756 | cursor: String!
1757 | }
1758 |
1759 | """
1760 | Review Rating Summary
1761 | """
1762 | type Reviews {
1763 | """
1764 | Total number of reviews on product.
1765 | """
1766 | numberOfReviews: Int!
1767 |
1768 | """
1769 | Summation of rating scores from each review.
1770 | """
1771 | summationOfRatings: Int!
1772 | }
1773 |
1774 | """
1775 | route
1776 | """
1777 | type Route {
1778 | """
1779 | node
1780 | """
1781 | node: Node
1782 | }
1783 |
1784 | """
1785 | Store settings information from the control panel.
1786 | """
1787 | type Settings {
1788 | """
1789 | The name of the store.
1790 | """
1791 | storeName: String!
1792 |
1793 | """
1794 | The hash of the store.
1795 | """
1796 | storeHash: String!
1797 |
1798 | """
1799 | The current store status.
1800 | """
1801 | status: StorefrontStatusType!
1802 |
1803 | """
1804 | Logo information for the store.
1805 | """
1806 | logo: LogoField!
1807 |
1808 | """
1809 | Contact information for the store.
1810 | """
1811 | contact: ContactField
1812 |
1813 | """
1814 | Store urls.
1815 | """
1816 | url: UrlField!
1817 |
1818 | """
1819 | Store display format information.
1820 | """
1821 | display: DisplayField!
1822 |
1823 | """
1824 | Channel ID.
1825 | """
1826 | channelId: Long!
1827 | }
1828 |
1829 | """
1830 | A site
1831 | """
1832 | type Site {
1833 | categoryTree: [CategoryTreeItem!]!
1834 |
1835 | """
1836 | Details of the brand.
1837 | """
1838 | brands(
1839 | before: String
1840 | after: String
1841 | first: Int
1842 | last: Int
1843 | productEntityIds: [Int!] = []
1844 | ): BrandConnection!
1845 |
1846 | """
1847 | Details of the products.
1848 | """
1849 | products(
1850 | before: String
1851 | after: String
1852 | first: Int
1853 | last: Int
1854 | ids: [ID!] = []
1855 | entityIds: [Int!] = []
1856 | ): ProductConnection!
1857 |
1858 | """
1859 | Details of the newest products.
1860 | """
1861 | newestProducts(
1862 | before: String
1863 | after: String
1864 | first: Int
1865 | last: Int
1866 | ): ProductConnection!
1867 |
1868 | """
1869 | Details of the best selling products.
1870 | """
1871 | bestSellingProducts(
1872 | before: String
1873 | after: String
1874 | first: Int
1875 | last: Int
1876 | ): ProductConnection!
1877 |
1878 | """
1879 | Details of the featured products.
1880 | """
1881 | featuredProducts(
1882 | before: String
1883 | after: String
1884 | first: Int
1885 | last: Int
1886 | ): ProductConnection!
1887 |
1888 | """
1889 | A single product object with variant pricing overlay capabilities.
1890 | """
1891 | product(
1892 | id: ID
1893 | entityId: Int
1894 | variantEntityId: Int
1895 | optionValueIds: [OptionValueId!] = []
1896 | sku: String
1897 | ): Product
1898 |
1899 | """
1900 | Route for a node
1901 | """
1902 | route(path: String!): Route!
1903 |
1904 | """
1905 | Store settings.
1906 | """
1907 | settings: Settings
1908 | }
1909 |
1910 | """
1911 | Storefront Mode
1912 | """
1913 | enum StorefrontStatusType {
1914 | LAUNCHED
1915 | MAINTENANCE
1916 | PRE_LAUNCH
1917 | HIBERNATION
1918 | }
1919 |
1920 | """
1921 | A swatch option value - swatch values can be associated with a list of hexidecimal colors or an image.
1922 | """
1923 | type SwatchOptionValue implements CatalogProductOptionValue {
1924 | """
1925 | List of up to 3 hex encoded colors to associate with a swatch value.
1926 | """
1927 | hexColors: [String!]!
1928 |
1929 | """
1930 | Absolute path of a swatch texture image.
1931 | """
1932 | imageUrl(width: Int!, height: Int): String
1933 |
1934 | """
1935 | Unique ID for the option value.
1936 | """
1937 | entityId: Int!
1938 |
1939 | """
1940 | Label for the option value.
1941 | """
1942 | label: String!
1943 |
1944 | """
1945 | Indicates whether this value is the chosen default selected value.
1946 | """
1947 | isDefault: Boolean!
1948 | }
1949 |
1950 | """
1951 | A single line text input field.
1952 | """
1953 | type TextFieldOption implements CatalogProductOption {
1954 | """
1955 | Unique ID for the option.
1956 | """
1957 | entityId: Int!
1958 |
1959 | """
1960 | Display name for the option.
1961 | """
1962 | displayName: String!
1963 |
1964 | """
1965 | One of the option values is required to be selected for the checkout.
1966 | """
1967 | isRequired: Boolean!
1968 | }
1969 |
1970 | """
1971 | Url field
1972 | """
1973 | type UrlField {
1974 | """
1975 | Store url.
1976 | """
1977 | vanityUrl: String!
1978 |
1979 | """
1980 | CDN url to fetch assets.
1981 | """
1982 | cdnUrl: String!
1983 | }
1984 |
1985 | """
1986 | Variant
1987 | """
1988 | type Variant implements Node {
1989 | """
1990 | The ID of an object
1991 | """
1992 | id: ID!
1993 |
1994 | """
1995 | Id of the variant.
1996 | """
1997 | entityId: Int!
1998 |
1999 | """
2000 | Sku of the variant.
2001 | """
2002 | sku: String!
2003 |
2004 | """
2005 | The variant's weight. If a weight was not explicitly specified on the variant, this will be the product's weight.
2006 | """
2007 | weight: Measurement
2008 |
2009 | """
2010 | The variant's height. If a height was not explicitly specified on the variant, this will be the product's height.
2011 | """
2012 | height: Measurement
2013 |
2014 | """
2015 | The variant's width. If a width was not explicitly specified on the variant, this will be the product's width.
2016 | """
2017 | width: Measurement
2018 |
2019 | """
2020 | The variant's depth. If a depth was not explicitly specified on the variant, this will be the product's depth.
2021 | """
2022 | depth: Measurement
2023 |
2024 | """
2025 | The options which define a variant.
2026 | """
2027 | options(
2028 | before: String
2029 | after: String
2030 | first: Int
2031 | last: Int
2032 | ): OptionConnection!
2033 |
2034 | """
2035 | Product options that compose this variant.
2036 | """
2037 | productOptions(
2038 | before: String
2039 | after: String
2040 | first: Int
2041 | last: Int
2042 | ): ProductOptionConnection!
2043 |
2044 | """
2045 | Default image for a variant.
2046 | """
2047 | defaultImage: Image
2048 |
2049 | """
2050 | Variant prices
2051 | """
2052 | prices(includeTax: Boolean = false, currencyCode: currencyCode): Prices
2053 |
2054 | """
2055 | Variant inventory
2056 | """
2057 | inventory: VariantInventory
2058 |
2059 | """
2060 | Metafield data related to a variant.
2061 | """
2062 | metafields(
2063 | namespace: String!
2064 | keys: [String!] = []
2065 | before: String
2066 | after: String
2067 | first: Int
2068 | last: Int
2069 | ): MetafieldConnection!
2070 | }
2071 |
2072 | """
2073 | A connection to a list of items.
2074 | """
2075 | type VariantConnection {
2076 | """
2077 | Information to aid in pagination.
2078 | """
2079 | pageInfo: PageInfo!
2080 |
2081 | """
2082 | A list of edges.
2083 | """
2084 | edges: [VariantEdge]
2085 | }
2086 |
2087 | """
2088 | An edge in a connection.
2089 | """
2090 | type VariantEdge {
2091 | """
2092 | The item at the end of the edge.
2093 | """
2094 | node: Variant!
2095 |
2096 | """
2097 | A cursor for use in pagination.
2098 | """
2099 | cursor: String!
2100 | }
2101 |
2102 | """
2103 | Variant Inventory
2104 | """
2105 | type VariantInventory {
2106 | """
2107 | Aggregated product variant inventory information. This data may not be available if not set or if the store's Inventory Settings have disabled displaying stock levels on the storefront.
2108 | """
2109 | aggregated: Aggregated
2110 |
2111 | """
2112 | Indicates whether this product is in stock.
2113 | """
2114 | isInStock: Boolean!
2115 |
2116 | """
2117 | Inventory by locations.
2118 | """
2119 | byLocation(
2120 | locationEntityIds: [Int!] = []
2121 | before: String
2122 | after: String
2123 | first: Int
2124 | last: Int
2125 | ): LocationConnection
2126 | }
2127 |
2128 | """
2129 | Please select a currency
2130 | """
2131 | enum currencyCode {
2132 | ADP
2133 | AED
2134 | AFA
2135 | AFN
2136 | ALK
2137 | ALL
2138 | AMD
2139 | ANG
2140 | AOA
2141 | AOK
2142 | AON
2143 | AOR
2144 | ARA
2145 | ARL
2146 | ARM
2147 | ARP
2148 | ARS
2149 | ATS
2150 | AUD
2151 | AWG
2152 | AZM
2153 | AZN
2154 | BAD
2155 | BAM
2156 | BAN
2157 | BBD
2158 | BDT
2159 | BEC
2160 | BEF
2161 | BEL
2162 | BGL
2163 | BGM
2164 | BGN
2165 | BGO
2166 | BHD
2167 | BIF
2168 | BMD
2169 | BND
2170 | BOB
2171 | BOL
2172 | BOP
2173 | BOV
2174 | BRB
2175 | BRC
2176 | BRE
2177 | BRL
2178 | BRN
2179 | BRR
2180 | BRZ
2181 | BSD
2182 | BTN
2183 | BUK
2184 | BWP
2185 | BYB
2186 | BYR
2187 | BZD
2188 | CAD
2189 | CDF
2190 | CHE
2191 | CHF
2192 | CHW
2193 | CLE
2194 | CLF
2195 | CLP
2196 | CNX
2197 | CNY
2198 | COP
2199 | COU
2200 | CRC
2201 | CSD
2202 | CSK
2203 | CVE
2204 | CYP
2205 | CZK
2206 | DDM
2207 | DEM
2208 | DJF
2209 | DKK
2210 | DOP
2211 | DZD
2212 | ECS
2213 | ECV
2214 | EEK
2215 | EGP
2216 | ERN
2217 | ESA
2218 | ESB
2219 | ESP
2220 | ETB
2221 | EUR
2222 | FIM
2223 | FJD
2224 | FKP
2225 | FRF
2226 | GBP
2227 | GEK
2228 | GEL
2229 | GHC
2230 | GHS
2231 | GIP
2232 | GMD
2233 | GNF
2234 | GNS
2235 | GQE
2236 | GRD
2237 | GTQ
2238 | GWE
2239 | GWP
2240 | GYD
2241 | HKD
2242 | HNL
2243 | HRD
2244 | HRK
2245 | HTG
2246 | HUF
2247 | IDR
2248 | IEP
2249 | ILP
2250 | ILR
2251 | ILS
2252 | INR
2253 | IQD
2254 | ISJ
2255 | ISK
2256 | ITL
2257 | JMD
2258 | JOD
2259 | JPY
2260 | KES
2261 | KGS
2262 | KHR
2263 | KMF
2264 | KRH
2265 | KRO
2266 | KRW
2267 | KWD
2268 | KYD
2269 | KZT
2270 | LAK
2271 | LBP
2272 | LKR
2273 | LRD
2274 | LSL
2275 | LTL
2276 | LTT
2277 | LUC
2278 | LUF
2279 | LUL
2280 | LVL
2281 | LVR
2282 | LYD
2283 | MAD
2284 | MAF
2285 | MCF
2286 | MDC
2287 | MDL
2288 | MGA
2289 | MGF
2290 | MKD
2291 | MKN
2292 | MLF
2293 | MMK
2294 | MNT
2295 | MOP
2296 | MRO
2297 | MTL
2298 | MTP
2299 | MUR
2300 | MVP
2301 | MVR
2302 | MWK
2303 | MXN
2304 | MXP
2305 | MXV
2306 | MYR
2307 | MZE
2308 | MZM
2309 | MZN
2310 | NAD
2311 | NGN
2312 | NIC
2313 | NIO
2314 | NLG
2315 | NOK
2316 | NPR
2317 | NZD
2318 | OMR
2319 | PAB
2320 | PEI
2321 | PEN
2322 | PES
2323 | PGK
2324 | PHP
2325 | PKR
2326 | PLN
2327 | PLZ
2328 | PTE
2329 | PYG
2330 | QAR
2331 | RHD
2332 | ROL
2333 | RON
2334 | RSD
2335 | RUB
2336 | RUR
2337 | RWF
2338 | SAR
2339 | SBD
2340 | SCR
2341 | SDD
2342 | SDG
2343 | SDP
2344 | SEK
2345 | SGD
2346 | SHP
2347 | SIT
2348 | SKK
2349 | SLL
2350 | SOS
2351 | SRD
2352 | SRG
2353 | SSP
2354 | STD
2355 | SUR
2356 | SVC
2357 | SYP
2358 | SZL
2359 | THB
2360 | TJR
2361 | TJS
2362 | TMM
2363 | TMT
2364 | TND
2365 | TOP
2366 | TPE
2367 | TRL
2368 | TRY
2369 | TTD
2370 | TWD
2371 | TZS
2372 | UAH
2373 | UAK
2374 | UGS
2375 | UGX
2376 | USD
2377 | USN
2378 | USS
2379 | UYI
2380 | UYP
2381 | UYU
2382 | UZS
2383 | VEB
2384 | VEF
2385 | VND
2386 | VNN
2387 | VUV
2388 | WST
2389 | XAF
2390 | XCD
2391 | XEU
2392 | XFO
2393 | XFU
2394 | XOF
2395 | XPF
2396 | XRE
2397 | YDD
2398 | YER
2399 | YUD
2400 | YUM
2401 | YUN
2402 | YUR
2403 | ZAL
2404 | ZAR
2405 | ZMK
2406 | ZMW
2407 | ZRN
2408 | ZRZ
2409 | ZWD
2410 | ZWL
2411 | ZWR
2412 | }
2413 |
2414 | """
2415 | The `BigDecimal` scalar type represents signed fractional values with arbitrary precision.
2416 | """
2417 | scalar BigDecimal
2418 |
2419 | """
2420 | The `Long` scalar type represents non-fractional signed whole numeric values. Long can represent values between -(2^63) and 2^63 - 1.
2421 | """
2422 | scalar Long
2423 |
--------------------------------------------------------------------------------
/example /scripts/generate-definitions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Generates definitions for REST API endpoints that are being
3 | * used by ../api using https://github.com/drwpow/swagger-to-ts
4 | */
5 | const { readFileSync, promises } = require('fs')
6 | const path = require('path')
7 | const fetch = require('node-fetch')
8 | const swaggerToTS = require('@manifoldco/swagger-to-ts').default
9 |
10 | async function getSchema(filename) {
11 | const url = `https://next-api.stoplight.io/projects/8433/files/${filename}`
12 | const res = await fetch(url)
13 |
14 | if (!res.ok) {
15 | throw new Error(`Request failed with ${res.status}: ${res.statusText}`)
16 | }
17 |
18 | return res.json()
19 | }
20 |
21 | const schemas = Object.entries({
22 | '../api/definitions/catalog.ts':
23 | 'BigCommerce_Catalog_API.oas2.yml?ref=version%2F20.930',
24 | '../api/definitions/store-content.ts':
25 | 'BigCommerce_Store_Content_API.oas2.yml?ref=version%2F20.930',
26 | '../api/definitions/wishlist.ts':
27 | 'BigCommerce_Wishlist_API.oas2.yml?ref=version%2F20.930',
28 | // swagger-to-ts is not working for the schema of the cart API
29 | // '../api/definitions/cart.ts':
30 | // 'BigCommerce_Server_to_Server_Cart_API.oas2.yml',
31 | })
32 |
33 | async function writeDefinitions() {
34 | const ops = schemas.map(async ([dest, filename]) => {
35 | const destination = path.join(__dirname, dest)
36 | const schema = await getSchema(filename)
37 | const definition = swaggerToTS(schema.content, {
38 | prettierConfig: 'package.json',
39 | })
40 |
41 | await promises.writeFile(destination, definition)
42 |
43 | console.log(`✔️ Added definitions for: ${dest}`)
44 | })
45 |
46 | await Promise.all(ops)
47 | }
48 |
49 | writeDefinitions()
50 |
--------------------------------------------------------------------------------
/example /src/api/index.ts:
--------------------------------------------------------------------------------
1 | import type { RequestInit, Response } from '@vercel/fetch'
2 |
3 | export interface CommerceAPIConfig {
4 | locale?: string
5 | commerceUrl: string
6 | apiToken: string
7 | cartCookie: string
8 | cartCookieMaxAge: number
9 | customerCookie: string
10 | fetch(
11 | query: string,
12 | queryData?: CommerceAPIFetchOptions,
13 | fetchOptions?: RequestInit
14 | ): Promise>
15 | }
16 |
17 | export type GraphQLFetcher<
18 | Data extends GraphQLFetcherResult = GraphQLFetcherResult,
19 | Variables = any
20 | > = (
21 | query: string,
22 | queryData?: CommerceAPIFetchOptions,
23 | fetchOptions?: RequestInit
24 | ) => Promise
25 |
26 | export interface GraphQLFetcherResult {
27 | data: Data
28 | res: Response
29 | }
30 |
31 | export interface CommerceAPIFetchOptions {
32 | variables?: Variables
33 | preview?: boolean
34 | }
35 |
36 | // TODO: define interfaces for all the available operations and API endpoints
37 |
--------------------------------------------------------------------------------
/example /src/cart/use-add-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useAddItem = useAction
4 |
5 | export default useAddItem
6 |
--------------------------------------------------------------------------------
/example /src/cart/use-cart-actions.tsx:
--------------------------------------------------------------------------------
1 | import type { HookFetcher, HookFetcherOptions } from '../utils/types'
2 | import useAddItem from './use-add-item'
3 | import useRemoveItem from './use-remove-item'
4 | import useUpdateItem from './use-update-item'
5 |
6 | // This hook is probably not going to be used, but it's here
7 | // to show how a commerce should be structuring it
8 | export default function useCartActions(
9 | options: HookFetcherOptions,
10 | fetcher: HookFetcher
11 | ) {
12 | const addItem = useAddItem(options, fetcher)
13 | const updateItem = useUpdateItem(options, fetcher)
14 | const removeItem = useRemoveItem(options, fetcher)
15 |
16 | return { addItem, updateItem, removeItem }
17 | }
18 |
--------------------------------------------------------------------------------
/example /src/cart/use-cart.tsx:
--------------------------------------------------------------------------------
1 | import type { responseInterface } from 'swr'
2 | import Cookies from 'js-cookie'
3 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
4 | import useData, { SwrOptions } from '../utils/use-data'
5 | import { useCommerce } from '..'
6 |
7 | export type CartResponse = responseInterface & {
8 | isEmpty: boolean
9 | }
10 |
11 | export type CartInput = {
12 | cartId: string | undefined
13 | }
14 |
15 | export default function useCart(
16 | options: HookFetcherOptions,
17 | input: HookInput,
18 | fetcherFn: HookFetcher,
19 | swrOptions?: SwrOptions
20 | ) {
21 | const { cartCookie } = useCommerce()
22 |
23 | const fetcher: typeof fetcherFn = (options, input, fetch) => {
24 | input.cartId = Cookies.get(cartCookie)
25 | return fetcherFn(options, input, fetch)
26 | }
27 |
28 | const response = useData(options, input, fetcher, swrOptions)
29 |
30 | return Object.assign(response, { isEmpty: true }) as CartResponse
31 | }
32 |
--------------------------------------------------------------------------------
/example /src/cart/use-remove-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useRemoveItem = useAction
4 |
5 | export default useRemoveItem
6 |
--------------------------------------------------------------------------------
/example /src/cart/use-update-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useUpdateItem = useAction
4 |
5 | export default useUpdateItem
6 |
--------------------------------------------------------------------------------
/example /src/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ReactNode,
3 | MutableRefObject,
4 | createContext,
5 | useContext,
6 | useMemo,
7 | useRef,
8 | } from 'react'
9 | import * as React from 'react'
10 | import { Fetcher } from './utils/types'
11 |
12 | const Commerce = createContext({})
13 |
14 | export type CommerceProps = {
15 | children?: ReactNode
16 | config: CommerceConfig
17 | }
18 |
19 | export type CommerceConfig = { fetcher: Fetcher } & Omit<
20 | CommerceContextValue,
21 | 'fetcherRef'
22 | >
23 |
24 | export type CommerceContextValue = {
25 | fetcherRef: MutableRefObject>
26 | locale: string
27 | cartCookie: string
28 | }
29 |
30 | export function CommerceProvider({ children, config }: CommerceProps) {
31 | if (!config) {
32 | throw new Error('CommerceProvider requires a valid config object')
33 | }
34 |
35 | const fetcherRef = useRef(config.fetcher)
36 | // Because the config is an object, if the parent re-renders this provider
37 | // will re-render every consumer unless we memoize the config
38 | const cfg = useMemo(
39 | () => ({
40 | fetcherRef,
41 | locale: config.locale,
42 | cartCookie: config.cartCookie,
43 | }),
44 | [config.locale, config.cartCookie]
45 | )
46 |
47 | return {children}
48 | }
49 |
50 | export function useCommerce() {
51 | return useContext(Commerce) as T
52 | }
53 |
--------------------------------------------------------------------------------
/example /src/products/use-search.tsx:
--------------------------------------------------------------------------------
1 | import useData from '../utils/use-data'
2 |
3 | const useSearch = useData
4 |
5 | export default useSearch
6 |
--------------------------------------------------------------------------------
/example /src/use-customer.tsx:
--------------------------------------------------------------------------------
1 | import useData from './utils/use-data'
2 |
3 | const useCustomer = useData
4 |
5 | export default useCustomer
6 |
--------------------------------------------------------------------------------
/example /src/use-login.tsx:
--------------------------------------------------------------------------------
1 | import useAction from './utils/use-action'
2 |
3 | const useLogin = useAction
4 |
5 | export default useLogin
6 |
--------------------------------------------------------------------------------
/example /src/use-logout.tsx:
--------------------------------------------------------------------------------
1 | import useAction from './utils/use-action'
2 |
3 | const useLogout = useAction
4 |
5 | export default useLogout
6 |
--------------------------------------------------------------------------------
/example /src/use-price.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useCommerce } from '.'
3 |
4 | export function formatPrice({
5 | amount,
6 | currencyCode,
7 | locale,
8 | }: {
9 | amount: number
10 | currencyCode: string
11 | locale: string
12 | }) {
13 | const formatCurrency = new Intl.NumberFormat(locale, {
14 | style: 'currency',
15 | currency: currencyCode,
16 | })
17 |
18 | return formatCurrency.format(amount)
19 | }
20 |
21 | export function formatVariantPrice({
22 | amount,
23 | baseAmount,
24 | currencyCode,
25 | locale,
26 | }: {
27 | baseAmount: number
28 | amount: number
29 | currencyCode: string
30 | locale: string
31 | }) {
32 | const hasDiscount = baseAmount > amount
33 | const formatDiscount = new Intl.NumberFormat(locale, { style: 'percent' })
34 | const discount = hasDiscount
35 | ? formatDiscount.format((baseAmount - amount) / baseAmount)
36 | : null
37 |
38 | const price = formatPrice({ amount, currencyCode, locale })
39 | const basePrice = hasDiscount
40 | ? formatPrice({ amount: baseAmount, currencyCode, locale })
41 | : null
42 |
43 | return { price, basePrice, discount }
44 | }
45 |
46 | export default function usePrice(
47 | data?: {
48 | amount: number
49 | baseAmount?: number
50 | currencyCode: string
51 | } | null
52 | ) {
53 | const { amount, baseAmount, currencyCode } = data ?? {}
54 | const { locale } = useCommerce()
55 | const value = useMemo(() => {
56 | if (typeof amount !== 'number' || !currencyCode) return ''
57 |
58 | return baseAmount
59 | ? formatVariantPrice({ amount, baseAmount, currencyCode, locale })
60 | : formatPrice({ amount, currencyCode, locale })
61 | }, [amount, baseAmount, currencyCode])
62 |
63 | return typeof value === 'string' ? { price: value } : value
64 | }
65 |
--------------------------------------------------------------------------------
/example /src/use-signup.tsx:
--------------------------------------------------------------------------------
1 | import useAction from './utils/use-action'
2 |
3 | const useSignup = useAction
4 |
5 | export default useSignup
6 |
--------------------------------------------------------------------------------
/example /src/utils/errors.ts:
--------------------------------------------------------------------------------
1 | export type ErrorData = {
2 | message: string
3 | code?: string
4 | }
5 |
6 | export type ErrorProps = {
7 | code?: string
8 | } & (
9 | | { message: string; errors?: never }
10 | | { message?: never; errors: ErrorData[] }
11 | )
12 |
13 | export class CommerceError extends Error {
14 | code?: string
15 | errors: ErrorData[]
16 |
17 | constructor({ message, code, errors }: ErrorProps) {
18 | const error: ErrorData = message
19 | ? { message, ...(code ? { code } : {}) }
20 | : errors![0]
21 |
22 | super(error.message)
23 | this.errors = message ? [error] : errors!
24 |
25 | if (error.code) this.code = error.code
26 | }
27 | }
28 |
29 | export class FetcherError extends CommerceError {
30 | status: number
31 |
32 | constructor(
33 | options: {
34 | status: number
35 | } & ErrorProps
36 | ) {
37 | super(options)
38 | this.status = options.status
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/example /src/utils/types.ts:
--------------------------------------------------------------------------------
1 | // Core fetcher added by CommerceProvider
2 | export type Fetcher = (options: FetcherOptions) => T | Promise
3 |
4 | export type FetcherOptions = {
5 | url?: string
6 | query?: string
7 | method?: string
8 | variables?: any
9 | body?: any
10 | }
11 |
12 | export type HookFetcher = (
13 | options: HookFetcherOptions | null,
14 | input: Input,
15 | fetch: (options: FetcherOptions) => Promise
16 | ) => Result | Promise
17 |
18 | export type HookFetcherOptions = {
19 | query?: string
20 | url?: string
21 | method?: string
22 | }
23 |
24 | export type HookInput = [string, string | number | boolean | undefined][]
25 |
--------------------------------------------------------------------------------
/example /src/utils/use-action.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { HookFetcher, HookFetcherOptions } from './types'
3 | import { useCommerce } from '..'
4 |
5 | export default function useAction(
6 | options: HookFetcherOptions,
7 | fetcher: HookFetcher
8 | ) {
9 | const { fetcherRef } = useCommerce()
10 |
11 | return useCallback(
12 | (input: Input) => fetcher(options, input, fetcherRef.current),
13 | [fetcher]
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/example /src/utils/use-data.tsx:
--------------------------------------------------------------------------------
1 | import useSWR, { ConfigInterface, responseInterface } from 'swr'
2 | import type { HookInput, HookFetcher, HookFetcherOptions } from './types'
3 | import { CommerceError } from './errors'
4 | import { useCommerce } from '..'
5 |
6 | export type SwrOptions = ConfigInterface<
7 | Result,
8 | CommerceError,
9 | HookFetcher
10 | >
11 |
12 | export type UseData = (
13 | options: HookFetcherOptions | (() => HookFetcherOptions | null),
14 | input: HookInput,
15 | fetcherFn: HookFetcher,
16 | swrOptions?: SwrOptions
17 | ) => responseInterface
18 |
19 | const useData: UseData = (options, input, fetcherFn, swrOptions) => {
20 | const { fetcherRef } = useCommerce()
21 | const fetcher = async (
22 | url?: string,
23 | query?: string,
24 | method?: string,
25 | ...args: any[]
26 | ) => {
27 | try {
28 | return await fetcherFn(
29 | { url, query, method },
30 | // Transform the input array into an object
31 | args.reduce((obj, val, i) => {
32 | obj[input[i][0]!] = val
33 | return obj
34 | }, {}),
35 | fetcherRef.current
36 | )
37 | } catch (error) {
38 | // SWR will not log errors, but any error that's not an instance
39 | // of CommerceError is not welcomed by this hook
40 | if (!(error instanceof CommerceError)) {
41 | console.error(error)
42 | }
43 | throw error
44 | }
45 | }
46 | const response = useSWR(
47 | () => {
48 | const opts = typeof options === 'function' ? options() : options
49 | return opts
50 | ? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
51 | : null
52 | },
53 | fetcher,
54 | swrOptions
55 | )
56 |
57 | return response
58 | }
59 |
60 | export default useData
61 |
--------------------------------------------------------------------------------
/example /src/wishlist/use-add-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useAddItem = useAction
4 |
5 | export default useAddItem
6 |
--------------------------------------------------------------------------------
/example /src/wishlist/use-remove-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useRemoveItem = useAction
4 |
5 | export default useRemoveItem
6 |
--------------------------------------------------------------------------------
/example /src/wishlist/use-wishlist.tsx:
--------------------------------------------------------------------------------
1 | import type { responseInterface } from 'swr'
2 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
3 | import useData, { SwrOptions } from '../utils/use-data'
4 |
5 | export type WishlistResponse = responseInterface & {
6 | isEmpty: boolean
7 | }
8 |
9 | export default function useWishlist(
10 | options: HookFetcherOptions,
11 | input: HookInput,
12 | fetcherFn: HookFetcher,
13 | swrOptions?: SwrOptions
14 | ) {
15 | const response = useData(options, input, fetcherFn, swrOptions)
16 | return Object.assign(response, { isEmpty: true }) as WishlistResponse
17 | }
18 |
--------------------------------------------------------------------------------
/example /wishlist/use-add-item.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { HookFetcher } from '../src/utils/types'
3 | import { CommerceError } from '../src/utils/errors'
4 | import useWishlistAddItem from '../src/wishlist/use-add-item'
5 | import type { ItemBody, AddItemBody } from '../api/wishlist'
6 | import useCustomer from '../account/use-customer'
7 | import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
8 |
9 | const defaultOpts = {
10 | url: '/api/bigcommerce/wishlist',
11 | method: 'POST',
12 | }
13 |
14 | export type AddItemInput = ItemBody
15 |
16 | export const fetcher: HookFetcher = (
17 | options,
18 | { item },
19 | fetch
20 | ) => {
21 | // TODO: add validations before doing the fetch
22 | return fetch({
23 | ...defaultOpts,
24 | ...options,
25 | body: { item },
26 | })
27 | }
28 |
29 | export function extendHook(customFetcher: typeof fetcher) {
30 | const useAddItem = (opts?: UseWishlistOptions) => {
31 | const { data: customer } = useCustomer()
32 | const { revalidate } = useWishlist(opts)
33 | const fn = useWishlistAddItem(defaultOpts, customFetcher)
34 |
35 | return useCallback(
36 | async function addItem(input: AddItemInput) {
37 | if (!customer) {
38 | // A signed customer is required in order to have a wishlist
39 | throw new CommerceError({
40 | message: 'Signed customer not found',
41 | })
42 | }
43 |
44 | const data = await fn({ item: input })
45 | await revalidate()
46 | return data
47 | },
48 | [fn, revalidate, customer]
49 | )
50 | }
51 |
52 | useAddItem.extend = extendHook
53 |
54 | return useAddItem
55 | }
56 |
57 | export default extendHook(fetcher)
58 |
--------------------------------------------------------------------------------
/example /wishlist/use-remove-item.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { HookFetcher } from '../src/utils/types'
3 | import { CommerceError } from '../src/utils/errors'
4 | import useWishlistRemoveItem from '../src/wishlist/use-remove-item'
5 | import type { RemoveItemBody } from '../api/wishlist'
6 | import useCustomer from '../account/use-customer'
7 | import useWishlist, { UseWishlistOptions, Wishlist } from './use-wishlist'
8 |
9 | const defaultOpts = {
10 | url: '/api/bigcommerce/wishlist',
11 | method: 'DELETE',
12 | }
13 |
14 | export type RemoveItemInput = {
15 | id: string | number
16 | }
17 |
18 | export const fetcher: HookFetcher = (
19 | options,
20 | { itemId },
21 | fetch
22 | ) => {
23 | return fetch({
24 | ...defaultOpts,
25 | ...options,
26 | body: { itemId },
27 | })
28 | }
29 |
30 | export function extendHook(customFetcher: typeof fetcher) {
31 | const useRemoveItem = (opts?: UseWishlistOptions) => {
32 | const { data: customer } = useCustomer()
33 | const { revalidate } = useWishlist(opts)
34 | const fn = useWishlistRemoveItem(
35 | defaultOpts,
36 | customFetcher
37 | )
38 |
39 | return useCallback(
40 | async function removeItem(input: RemoveItemInput) {
41 | if (!customer) {
42 | // A signed customer is required in order to have a wishlist
43 | throw new CommerceError({
44 | message: 'Signed customer not found',
45 | })
46 | }
47 |
48 | const data = await fn({ itemId: String(input.id) })
49 | await revalidate()
50 | return data
51 | },
52 | [fn, revalidate, customer]
53 | )
54 | }
55 |
56 | useRemoveItem.extend = extendHook
57 |
58 | return useRemoveItem
59 | }
60 |
61 | export default extendHook(fetcher)
62 |
--------------------------------------------------------------------------------
/example /wishlist/use-wishlist-actions.tsx:
--------------------------------------------------------------------------------
1 | import useAddItem from './use-add-item'
2 | import useRemoveItem from './use-remove-item'
3 |
4 | // This hook is probably not going to be used, but it's here
5 | // to show how a commerce should be structuring it
6 | export default function useWishlistActions() {
7 | const addItem = useAddItem()
8 | const removeItem = useRemoveItem()
9 |
10 | return { addItem, removeItem }
11 | }
12 |
--------------------------------------------------------------------------------
/example /wishlist/use-wishlist.tsx:
--------------------------------------------------------------------------------
1 | import { HookFetcher } from '../src/utils/types'
2 | import { SwrOptions } from '../src/utils/use-data'
3 | import useCommerceWishlist from '../src/wishlist/use-wishlist'
4 | import type { Wishlist } from '../api/wishlist'
5 | import useCustomer from '../account/use-customer'
6 |
7 | const defaultOpts = {
8 | url: '/api/bigcommerce/wishlist',
9 | method: 'GET',
10 | }
11 |
12 | export type { Wishlist }
13 |
14 | export interface UseWishlistOptions {
15 | includeProducts?: boolean
16 | }
17 |
18 | export interface UseWishlistInput extends UseWishlistOptions {
19 | customerId?: number
20 | }
21 |
22 | export const fetcher: HookFetcher = (
23 | options,
24 | { customerId, includeProducts },
25 | fetch
26 | ) => {
27 | if (!customerId) return null
28 |
29 | // Use a dummy base as we only care about the relative path
30 | const url = new URL(options?.url ?? defaultOpts.url, 'http://a')
31 |
32 | if (includeProducts) url.searchParams.set('products', '1')
33 |
34 | return fetch({
35 | url: url.pathname + url.search,
36 | method: options?.method ?? defaultOpts.method,
37 | })
38 | }
39 |
40 | export function extendHook(
41 | customFetcher: typeof fetcher,
42 | swrOptions?: SwrOptions
43 | ) {
44 | const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
45 | const { data: customer } = useCustomer()
46 | const response = useCommerceWishlist(
47 | defaultOpts,
48 | [
49 | ['customerId', customer?.entityId],
50 | ['includeProducts', includeProducts],
51 | ],
52 | customFetcher,
53 | {
54 | revalidateOnFocus: false,
55 | ...swrOptions,
56 | }
57 | )
58 |
59 | // Uses a getter to only calculate the prop when required
60 | // response.data is also a getter and it's better to not trigger it early
61 | Object.defineProperty(response, 'isEmpty', {
62 | get() {
63 | return (response.data?.items?.length || 0) <= 0
64 | },
65 | set: (x) => x,
66 | })
67 |
68 | return response
69 | }
70 |
71 | useWishlist.extend = extendHook
72 |
73 | return useWishlist
74 | }
75 |
76 | export default extendHook(fetcher)
77 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "commerce-framework",
3 | "version": "1.0.0",
4 | "description": "Next.js Commerce Framework",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "git+https://github.com/vercel/commerce-framework.git"
12 | },
13 | "keywords": [
14 | "nextjs",
15 | "commerce"
16 | ],
17 | "author": "",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/vercel/commerce-framework/issues"
21 | },
22 | "homepage": "https://github.com/vercel/commerce-framework#readme",
23 | "dependencies": {
24 | "@types/lodash.debounce": "^4.0.6",
25 | "@vercel/fetch": "^6.1.0",
26 | "js-cookie": "^2.2.1",
27 | "swr": "^0.3.8"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/commerce/api/index.ts:
--------------------------------------------------------------------------------
1 | import type { RequestInit, Response } from "@vercel/fetch";
2 |
3 | export interface CommerceAPIConfig {
4 | locale?: string;
5 | commerceUrl: string;
6 | apiToken: string;
7 | cartCookie: string;
8 | cartCookieMaxAge: number;
9 | customerCookie: string;
10 | fetch(
11 | query: string,
12 | queryData?: CommerceAPIFetchOptions,
13 | fetchOptions?: RequestInit
14 | ): Promise>;
15 | }
16 |
17 | export type GraphQLFetcher<
18 | Data extends GraphQLFetcherResult = GraphQLFetcherResult,
19 | Variables = any
20 | > = (
21 | query: string,
22 | queryData?: CommerceAPIFetchOptions,
23 | fetchOptions?: RequestInit
24 | ) => Promise;
25 |
26 | export interface GraphQLFetcherResult {
27 | data: Data;
28 | res: Response;
29 | }
30 |
31 | export interface CommerceAPIFetchOptions {
32 | variables?: Variables;
33 | preview?: boolean;
34 | }
35 |
--------------------------------------------------------------------------------
/src/commerce/cart/use-add-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useAddItem = useAction
4 |
5 | export default useAddItem
6 |
--------------------------------------------------------------------------------
/src/commerce/cart/use-cart-actions.tsx:
--------------------------------------------------------------------------------
1 | import type { HookFetcher, HookFetcherOptions } from '../utils/types'
2 | import useAddItem from './use-add-item'
3 | import useRemoveItem from './use-remove-item'
4 | import useUpdateItem from './use-update-item'
5 |
6 | // This hook is probably not going to be used, but it's here
7 | // to show how a commerce should be structuring it
8 | export default function useCartActions(
9 | options: HookFetcherOptions,
10 | fetcher: HookFetcher
11 | ) {
12 | const addItem = useAddItem(options, fetcher)
13 | const updateItem = useUpdateItem(options, fetcher)
14 | const removeItem = useRemoveItem(options, fetcher)
15 |
16 | return { addItem, updateItem, removeItem }
17 | }
18 |
--------------------------------------------------------------------------------
/src/commerce/cart/use-cart.tsx:
--------------------------------------------------------------------------------
1 | import type { responseInterface } from 'swr'
2 | import Cookies from 'js-cookie'
3 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
4 | import useData, { SwrOptions } from '../utils/use-data'
5 | import { useCommerce } from '..'
6 |
7 | export type CartResponse = responseInterface & {
8 | isEmpty: boolean
9 | }
10 |
11 | export type CartInput = {
12 | cartId: string | undefined
13 | }
14 |
15 | export default function useCart(
16 | options: HookFetcherOptions,
17 | input: HookInput,
18 | fetcherFn: HookFetcher,
19 | swrOptions?: SwrOptions
20 | ) {
21 | const { cartCookie } = useCommerce()
22 |
23 | const fetcher: typeof fetcherFn = (options, input, fetch) => {
24 | input.cartId = Cookies.get(cartCookie)
25 | return fetcherFn(options, input, fetch)
26 | }
27 |
28 | const response = useData(options, input, fetcher, swrOptions)
29 |
30 | return Object.assign(response, { isEmpty: true }) as CartResponse
31 | }
32 |
--------------------------------------------------------------------------------
/src/commerce/cart/use-remove-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useRemoveItem = useAction
4 |
5 | export default useRemoveItem
6 |
--------------------------------------------------------------------------------
/src/commerce/cart/use-update-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useUpdateItem = useAction
4 |
5 | export default useUpdateItem
6 |
--------------------------------------------------------------------------------
/src/commerce/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ReactNode,
3 | MutableRefObject,
4 | createContext,
5 | useContext,
6 | useMemo,
7 | useRef,
8 | } from 'react'
9 | import * as React from 'react'
10 | import { Fetcher } from './utils/types'
11 |
12 | const Commerce = createContext({})
13 |
14 | export type CommerceProps = {
15 | children?: ReactNode
16 | config: CommerceConfig
17 | }
18 |
19 | export type CommerceConfig = { fetcher: Fetcher } & Omit<
20 | CommerceContextValue,
21 | 'fetcherRef'
22 | >
23 |
24 | export type CommerceContextValue = {
25 | fetcherRef: MutableRefObject>
26 | locale: string
27 | cartCookie: string
28 | }
29 |
30 | export function CommerceProvider({ children, config }: CommerceProps) {
31 | if (!config) {
32 | throw new Error('CommerceProvider requires a valid config object')
33 | }
34 |
35 | const fetcherRef = useRef(config.fetcher)
36 | // Because the config is an object, if the parent re-renders this provider
37 | // will re-render every consumer unless we memoize the config
38 | const cfg = useMemo(
39 | () => ({
40 | fetcherRef,
41 | locale: config.locale,
42 | cartCookie: config.cartCookie,
43 | }),
44 | [config.locale, config.cartCookie]
45 | )
46 |
47 | return {children}
48 | }
49 |
50 | export function useCommerce() {
51 | return useContext(Commerce) as T
52 | }
53 |
--------------------------------------------------------------------------------
/src/commerce/products/use-search.tsx:
--------------------------------------------------------------------------------
1 | import useData from '../utils/use-data'
2 |
3 | const useSearch = useData
4 |
5 | export default useSearch
6 |
--------------------------------------------------------------------------------
/src/commerce/use-customer.tsx:
--------------------------------------------------------------------------------
1 | import useData from './utils/use-data'
2 |
3 | const useCustomer = useData
4 |
5 | export default useCustomer
6 |
--------------------------------------------------------------------------------
/src/commerce/use-login.tsx:
--------------------------------------------------------------------------------
1 | import useAction from './utils/use-action'
2 |
3 | const useLogin = useAction
4 |
5 | export default useLogin
6 |
--------------------------------------------------------------------------------
/src/commerce/use-logout.tsx:
--------------------------------------------------------------------------------
1 | import useAction from './utils/use-action'
2 |
3 | const useLogout = useAction
4 |
5 | export default useLogout
6 |
--------------------------------------------------------------------------------
/src/commerce/use-price.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react'
2 | import { useCommerce } from '.'
3 |
4 | export function formatPrice({
5 | amount,
6 | currencyCode,
7 | locale,
8 | }: {
9 | amount: number
10 | currencyCode: string
11 | locale: string
12 | }) {
13 | const formatCurrency = new Intl.NumberFormat(locale, {
14 | style: 'currency',
15 | currency: currencyCode,
16 | })
17 |
18 | return formatCurrency.format(amount)
19 | }
20 |
21 | export function formatVariantPrice({
22 | amount,
23 | baseAmount,
24 | currencyCode,
25 | locale,
26 | }: {
27 | baseAmount: number
28 | amount: number
29 | currencyCode: string
30 | locale: string
31 | }) {
32 | const hasDiscount = baseAmount > amount
33 | const formatDiscount = new Intl.NumberFormat(locale, { style: 'percent' })
34 | const discount = hasDiscount
35 | ? formatDiscount.format((baseAmount - amount) / baseAmount)
36 | : null
37 |
38 | const price = formatPrice({ amount, currencyCode, locale })
39 | const basePrice = hasDiscount
40 | ? formatPrice({ amount: baseAmount, currencyCode, locale })
41 | : null
42 |
43 | return { price, basePrice, discount }
44 | }
45 |
46 | export default function usePrice(
47 | data?: {
48 | amount: number
49 | baseAmount?: number
50 | currencyCode: string
51 | } | null
52 | ) {
53 | const { amount, baseAmount, currencyCode } = data ?? {}
54 | const { locale } = useCommerce()
55 | const value = useMemo(() => {
56 | if (typeof amount !== 'number' || !currencyCode) return ''
57 |
58 | return baseAmount
59 | ? formatVariantPrice({ amount, baseAmount, currencyCode, locale })
60 | : formatPrice({ amount, currencyCode, locale })
61 | }, [amount, baseAmount, currencyCode])
62 |
63 | return typeof value === 'string' ? { price: value } : value
64 | }
65 |
--------------------------------------------------------------------------------
/src/commerce/use-signup.tsx:
--------------------------------------------------------------------------------
1 | import useAction from './utils/use-action'
2 |
3 | const useSignup = useAction
4 |
5 | export default useSignup
6 |
--------------------------------------------------------------------------------
/src/commerce/utils/errors.ts:
--------------------------------------------------------------------------------
1 | export type ErrorData = {
2 | message: string
3 | code?: string
4 | }
5 |
6 | export type ErrorProps = {
7 | code?: string
8 | } & (
9 | | { message: string; errors?: never }
10 | | { message?: never; errors: ErrorData[] }
11 | )
12 |
13 | export class CommerceError extends Error {
14 | code?: string
15 | errors: ErrorData[]
16 |
17 | constructor({ message, code, errors }: ErrorProps) {
18 | const error: ErrorData = message
19 | ? { message, ...(code ? { code } : {}) }
20 | : errors![0]
21 |
22 | super(error.message)
23 | this.errors = message ? [error] : errors!
24 |
25 | if (error.code) this.code = error.code
26 | }
27 | }
28 |
29 | export class FetcherError extends CommerceError {
30 | status: number
31 |
32 | constructor(
33 | options: {
34 | status: number
35 | } & ErrorProps
36 | ) {
37 | super(options)
38 | this.status = options.status
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/commerce/utils/types.ts:
--------------------------------------------------------------------------------
1 | // Core fetcher added by CommerceProvider
2 | export type Fetcher = (options: FetcherOptions) => T | Promise
3 |
4 | export type FetcherOptions = {
5 | url?: string
6 | query?: string
7 | method?: string
8 | variables?: any
9 | body?: any
10 | }
11 |
12 | export type HookFetcher = (
13 | options: HookFetcherOptions | null,
14 | input: Input,
15 | fetch: (options: FetcherOptions) => Promise
16 | ) => Result | Promise
17 |
18 | export type HookFetcherOptions = {
19 | query?: string
20 | url?: string
21 | method?: string
22 | }
23 |
24 | export type HookInput = [string, string | number | boolean | undefined][]
25 |
--------------------------------------------------------------------------------
/src/commerce/utils/use-action.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import type { HookFetcher, HookFetcherOptions } from './types'
3 | import { useCommerce } from '..'
4 |
5 | export default function useAction(
6 | options: HookFetcherOptions,
7 | fetcher: HookFetcher
8 | ) {
9 | const { fetcherRef } = useCommerce()
10 |
11 | return useCallback(
12 | (input: Input) => fetcher(options, input, fetcherRef.current),
13 | [fetcher]
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/src/commerce/utils/use-data.tsx:
--------------------------------------------------------------------------------
1 | import useSWR, { ConfigInterface, responseInterface } from 'swr'
2 | import type { HookInput, HookFetcher, HookFetcherOptions } from './types'
3 | import { CommerceError } from './errors'
4 | import { useCommerce } from '..'
5 |
6 | export type SwrOptions = ConfigInterface<
7 | Result,
8 | CommerceError,
9 | HookFetcher
10 | >
11 |
12 | export type UseData = (
13 | options: HookFetcherOptions | (() => HookFetcherOptions | null),
14 | input: HookInput,
15 | fetcherFn: HookFetcher,
16 | swrOptions?: SwrOptions
17 | ) => responseInterface
18 |
19 | const useData: UseData = (options, input, fetcherFn, swrOptions) => {
20 | const { fetcherRef } = useCommerce()
21 | const fetcher = async (
22 | url?: string,
23 | query?: string,
24 | method?: string,
25 | ...args: any[]
26 | ) => {
27 | try {
28 | return await fetcherFn(
29 | { url, query, method },
30 | // Transform the input array into an object
31 | args.reduce((obj, val, i) => {
32 | obj[input[i][0]!] = val
33 | return obj
34 | }, {}),
35 | fetcherRef.current
36 | )
37 | } catch (error) {
38 | // SWR will not log errors, but any error that's not an instance
39 | // of CommerceError is not welcomed by this hook
40 | if (!(error instanceof CommerceError)) {
41 | console.error(error)
42 | }
43 | throw error
44 | }
45 | }
46 | const response = useSWR(
47 | () => {
48 | const opts = typeof options === 'function' ? options() : options
49 | return opts
50 | ? [opts.url, opts.query, opts.method, ...input.map((e) => e[1])]
51 | : null
52 | },
53 | fetcher,
54 | swrOptions
55 | )
56 |
57 | return response
58 | }
59 |
60 | export default useData
61 |
--------------------------------------------------------------------------------
/src/commerce/wishlist/use-add-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useAddItem = useAction
4 |
5 | export default useAddItem
6 |
--------------------------------------------------------------------------------
/src/commerce/wishlist/use-remove-item.tsx:
--------------------------------------------------------------------------------
1 | import useAction from '../utils/use-action'
2 |
3 | const useRemoveItem = useAction
4 |
5 | export default useRemoveItem
6 |
--------------------------------------------------------------------------------
/src/commerce/wishlist/use-wishlist.tsx:
--------------------------------------------------------------------------------
1 | import type { responseInterface } from 'swr'
2 | import type { HookInput, HookFetcher, HookFetcherOptions } from '../utils/types'
3 | import useData, { SwrOptions } from '../utils/use-data'
4 |
5 | export type WishlistResponse = responseInterface & {
6 | isEmpty: boolean
7 | }
8 |
9 | export default function useWishlist(
10 | options: HookFetcherOptions,
11 | input: HookInput,
12 | fetcherFn: HookFetcher,
13 | swrOptions?: SwrOptions
14 | ) {
15 | const response = useData(options, input, fetcherFn, swrOptions)
16 | return Object.assign(response, { isEmpty: true }) as WishlistResponse
17 | }
18 |
--------------------------------------------------------------------------------
/yarn.lock:
--------------------------------------------------------------------------------
1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
2 | # yarn lockfile v1
3 |
4 |
5 | "@types/async-retry@1.2.1":
6 | version "1.2.1"
7 | resolved "https://registry.yarnpkg.com/@types/async-retry/-/async-retry-1.2.1.tgz#fa9ac165907a8ee78f4924f4e393b656c65b5bb4"
8 | integrity sha512-yMQ6CVgICWtyFNBqJT3zqOc+TnqqEPLo4nKJNPFwcialiylil38Ie6q1ENeFTjvaLOkVim9K5LisHgAKJWidGQ==
9 |
10 | "@types/lodash.debounce@^4.0.6":
11 | version "4.0.6"
12 | resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.6.tgz#c5a2326cd3efc46566c47e4c0aa248dc0ee57d60"
13 | integrity sha512-4WTmnnhCfDvvuLMaF3KV4Qfki93KebocUF45msxhYyjMttZDQYzHkO639ohhk8+oco2cluAFL3t5+Jn4mleylQ==
14 | dependencies:
15 | "@types/lodash" "*"
16 |
17 | "@types/lodash@*":
18 | version "4.14.165"
19 | resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f"
20 | integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg==
21 |
22 | "@types/lru-cache@4.1.1":
23 | version "4.1.1"
24 | resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-4.1.1.tgz#b2d87a5e3df8d4b18ca426c5105cd701c2306d40"
25 | integrity sha512-8mNEUG6diOrI6pMqOHrHPDBB1JsrpedeMK9AWGzVCQ7StRRribiT9BRvUmF8aUws9iBbVlgVekOT5Sgzc1MTKw==
26 |
27 | "@types/node-fetch@2.3.2":
28 | version "2.3.2"
29 | resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.3.2.tgz#e01893b176c6fa1367743726380d65bce5d6576b"
30 | integrity sha512-yW0EOebSsQme9yKu09XbdDfle4/SmWZMK4dfteWcSLCYNQQcF+YOv0kIrvm+9pO11/ghA4E6A+RNQqvYj4Nr3A==
31 | dependencies:
32 | "@types/node" "*"
33 |
34 | "@types/node@*":
35 | version "14.14.7"
36 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.7.tgz#8ea1e8f8eae2430cf440564b98c6dfce1ec5945d"
37 | integrity sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==
38 |
39 | "@types/node@10.12.18":
40 | version "10.12.18"
41 | resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
42 | integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
43 |
44 | "@vercel/fetch-cached-dns@^2.0.1":
45 | version "2.0.1"
46 | resolved "https://registry.yarnpkg.com/@vercel/fetch-cached-dns/-/fetch-cached-dns-2.0.1.tgz#b929ba5b4b6f7108abf49adaf03309159047c134"
47 | integrity sha512-4a2IoekfGUgV/dinAB7Tx5oqA+Pg9I/6x/t8n/yduHmdclP5EdWTN4gPrwOKVECKVn2pV1VxAT8q4toSzwa2Eg==
48 | dependencies:
49 | "@types/node-fetch" "2.3.2"
50 | "@zeit/dns-cached-resolve" "2.1.0"
51 |
52 | "@vercel/fetch-retry@^5.0.2":
53 | version "5.0.3"
54 | resolved "https://registry.yarnpkg.com/@vercel/fetch-retry/-/fetch-retry-5.0.3.tgz#cce5d23f6e64f6f525c24e2ac7c78f65d6c5b1f4"
55 | integrity sha512-DIIoBY92r+sQ6iHSf5WjKiYvkdsDIMPWKYATlE0KcUAj2RV6SZK9UWpUzBRKsofXqedOqpVjrI0IE6AWL7JRtg==
56 | dependencies:
57 | async-retry "^1.3.1"
58 | debug "^3.1.0"
59 |
60 | "@vercel/fetch@^6.1.0":
61 | version "6.1.0"
62 | resolved "https://registry.yarnpkg.com/@vercel/fetch/-/fetch-6.1.0.tgz#4959cd264d25e811b46491818a9d9ca5d752a2a9"
63 | integrity sha512-xR0GQggKhPvwEWrqcrobsQFjyR/bDDbX24BkSaRyLzW+8SydKhkBc/mBCUV8h4SBZSlJMJnqhrxjFCZ1uJcqNg==
64 | dependencies:
65 | "@types/async-retry" "1.2.1"
66 | "@vercel/fetch-cached-dns" "^2.0.1"
67 | "@vercel/fetch-retry" "^5.0.2"
68 | agentkeepalive "3.4.1"
69 | debug "3.1.0"
70 |
71 | "@zeit/dns-cached-resolve@2.1.0":
72 | version "2.1.0"
73 | resolved "https://registry.yarnpkg.com/@zeit/dns-cached-resolve/-/dns-cached-resolve-2.1.0.tgz#78583010df1683fdb7b05949b75593c9a8641bc1"
74 | integrity sha512-KD2zyRZEBNs9PJ3/ob7zx0CvR4wM0oV4G5s5gFfPwmM74GpFbUN2pAAivP2AXnUrJ14Nkh8NumNKOzOyc4LbFQ==
75 | dependencies:
76 | "@types/async-retry" "1.2.1"
77 | "@types/lru-cache" "4.1.1"
78 | "@types/node" "10.12.18"
79 | async-retry "1.2.3"
80 | lru-cache "5.1.1"
81 |
82 | agentkeepalive@3.4.1:
83 | version "3.4.1"
84 | resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c"
85 | integrity sha512-MPIwsZU9PP9kOrZpyu2042kYA8Fdt/AedQYkYXucHgF9QoD9dXVp0ypuGnHXSR0hTstBxdt85Xkh4JolYfK5wg==
86 | dependencies:
87 | humanize-ms "^1.2.1"
88 |
89 | async-retry@1.2.3:
90 | version "1.2.3"
91 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0"
92 | integrity sha512-tfDb02Th6CE6pJUF2gjW5ZVjsgwlucVXOEQMvEX9JgSJMs9gAX+Nz3xRuJBKuUYjTSYORqvDBORdAQ3LU59g7Q==
93 | dependencies:
94 | retry "0.12.0"
95 |
96 | async-retry@^1.3.1:
97 | version "1.3.1"
98 | resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55"
99 | integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA==
100 | dependencies:
101 | retry "0.12.0"
102 |
103 | debug@3.1.0:
104 | version "3.1.0"
105 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
106 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
107 | dependencies:
108 | ms "2.0.0"
109 |
110 | debug@^3.1.0:
111 | version "3.2.6"
112 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
113 | integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
114 | dependencies:
115 | ms "^2.1.1"
116 |
117 | dequal@2.0.2:
118 | version "2.0.2"
119 | resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
120 | integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==
121 |
122 | humanize-ms@^1.2.1:
123 | version "1.2.1"
124 | resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
125 | integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=
126 | dependencies:
127 | ms "^2.0.0"
128 |
129 | js-cookie@^2.2.1:
130 | version "2.2.1"
131 | resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
132 | integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
133 |
134 | lru-cache@5.1.1:
135 | version "5.1.1"
136 | resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920"
137 | integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
138 | dependencies:
139 | yallist "^3.0.2"
140 |
141 | ms@2.0.0:
142 | version "2.0.0"
143 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
144 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
145 |
146 | ms@^2.0.0, ms@^2.1.1:
147 | version "2.1.2"
148 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
149 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
150 |
151 | retry@0.12.0:
152 | version "0.12.0"
153 | resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
154 | integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=
155 |
156 | swr@^0.3.8:
157 | version "0.3.8"
158 | resolved "https://registry.yarnpkg.com/swr/-/swr-0.3.8.tgz#b3c3c7fa278913d22dbe1f3f28c07df9da9be944"
159 | integrity sha512-EHRlaqoBtHsB2wOB+dQJ74DrZvaRGu4BaIQrhkD+/rj8/UGo2iQXN+rCcYnV7/VAreBJBmm9+lDkwZmUqWEkKA==
160 | dependencies:
161 | dequal "2.0.2"
162 |
163 | yallist@^3.0.2:
164 | version "3.1.1"
165 | resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
166 | integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
167 |
--------------------------------------------------------------------------------