mirror of
https://github.com/vercel/commerce.git
synced 2025-07-31 14:01:24 +00:00
use cache
This commit is contained in:
@@ -1,8 +1,16 @@
|
||||
import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants';
|
||||
import {
|
||||
HIDDEN_PRODUCT_TAG,
|
||||
SHOPIFY_GRAPHQL_API_ENDPOINT,
|
||||
TAGS
|
||||
} from 'lib/constants';
|
||||
import { isShopifyError } from 'lib/type-guards';
|
||||
import { ensureStartsWith } from 'lib/utils';
|
||||
import { revalidateTag } from 'next/cache';
|
||||
import { headers } from 'next/headers';
|
||||
import {
|
||||
revalidateTag,
|
||||
unstable_cacheTag as cacheTag,
|
||||
unstable_cacheLife as cacheLife
|
||||
} from 'next/cache';
|
||||
import { cookies, headers } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import {
|
||||
addToCartMutation,
|
||||
@@ -56,19 +64,17 @@ const domain = process.env.SHOPIFY_STORE_DOMAIN
|
||||
const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`;
|
||||
const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!;
|
||||
|
||||
type ExtractVariables<T> = T extends { variables: object } ? T['variables'] : never;
|
||||
type ExtractVariables<T> = T extends { variables: object }
|
||||
? T['variables']
|
||||
: never;
|
||||
|
||||
export async function shopifyFetch<T>({
|
||||
cache = 'force-cache',
|
||||
headers,
|
||||
query,
|
||||
tags,
|
||||
variables
|
||||
}: {
|
||||
cache?: RequestCache;
|
||||
headers?: HeadersInit;
|
||||
query: string;
|
||||
tags?: string[];
|
||||
variables?: ExtractVariables<T>;
|
||||
}): Promise<{ status: number; body: T } | never> {
|
||||
try {
|
||||
@@ -82,9 +88,7 @@ export async function shopifyFetch<T>({
|
||||
body: JSON.stringify({
|
||||
...(query && { query }),
|
||||
...(variables && { variables })
|
||||
}),
|
||||
cache,
|
||||
...(tags && { next: { tags } })
|
||||
})
|
||||
});
|
||||
|
||||
const body = await result.json();
|
||||
@@ -132,7 +136,9 @@ const reshapeCart = (cart: ShopifyCart): Cart => {
|
||||
};
|
||||
};
|
||||
|
||||
const reshapeCollection = (collection: ShopifyCollection): Collection | undefined => {
|
||||
const reshapeCollection = (
|
||||
collection: ShopifyCollection
|
||||
): Collection | undefined => {
|
||||
if (!collection) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -171,8 +177,14 @@ const reshapeImages = (images: Connection<Image>, productTitle: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => {
|
||||
if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) {
|
||||
const reshapeProduct = (
|
||||
product: ShopifyProduct,
|
||||
filterHiddenProducts: boolean = true
|
||||
) => {
|
||||
if (
|
||||
!product ||
|
||||
(filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -203,66 +215,64 @@ const reshapeProducts = (products: ShopifyProduct[]) => {
|
||||
|
||||
export async function createCart(): Promise<Cart> {
|
||||
const res = await shopifyFetch<ShopifyCreateCartOperation>({
|
||||
query: createCartMutation,
|
||||
cache: 'no-store'
|
||||
query: createCartMutation
|
||||
});
|
||||
|
||||
return reshapeCart(res.body.data.cartCreate.cart);
|
||||
}
|
||||
|
||||
export async function addToCart(
|
||||
cartId: string,
|
||||
lines: { merchandiseId: string; quantity: number }[]
|
||||
): Promise<Cart> {
|
||||
const cartId = (await cookies()).get('cartId')?.value!;
|
||||
const res = await shopifyFetch<ShopifyAddToCartOperation>({
|
||||
query: addToCartMutation,
|
||||
variables: {
|
||||
cartId,
|
||||
lines
|
||||
},
|
||||
cache: 'no-store'
|
||||
}
|
||||
});
|
||||
return reshapeCart(res.body.data.cartLinesAdd.cart);
|
||||
}
|
||||
|
||||
export async function removeFromCart(cartId: string, lineIds: string[]): Promise<Cart> {
|
||||
export async function removeFromCart(lineIds: string[]): Promise<Cart> {
|
||||
const cartId = (await cookies()).get('cartId')?.value!;
|
||||
const res = await shopifyFetch<ShopifyRemoveFromCartOperation>({
|
||||
query: removeFromCartMutation,
|
||||
variables: {
|
||||
cartId,
|
||||
lineIds
|
||||
},
|
||||
cache: 'no-store'
|
||||
}
|
||||
});
|
||||
|
||||
return reshapeCart(res.body.data.cartLinesRemove.cart);
|
||||
}
|
||||
|
||||
export async function updateCart(
|
||||
cartId: string,
|
||||
lines: { id: string; merchandiseId: string; quantity: number }[]
|
||||
): Promise<Cart> {
|
||||
const cartId = (await cookies()).get('cartId')?.value!;
|
||||
const res = await shopifyFetch<ShopifyUpdateCartOperation>({
|
||||
query: editCartItemsMutation,
|
||||
variables: {
|
||||
cartId,
|
||||
lines
|
||||
},
|
||||
cache: 'no-store'
|
||||
}
|
||||
});
|
||||
|
||||
return reshapeCart(res.body.data.cartLinesUpdate.cart);
|
||||
}
|
||||
|
||||
export async function getCart(cartId: string | undefined): Promise<Cart | undefined> {
|
||||
export async function getCart(): Promise<Cart | undefined> {
|
||||
const cartId = (await cookies()).get('cartId')?.value;
|
||||
|
||||
if (!cartId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await shopifyFetch<ShopifyCartOperation>({
|
||||
query: getCartQuery,
|
||||
variables: { cartId },
|
||||
tags: [TAGS.cart]
|
||||
variables: { cartId }
|
||||
});
|
||||
|
||||
// Old carts becomes `null` when you checkout.
|
||||
@@ -273,10 +283,15 @@ export async function getCart(cartId: string | undefined): Promise<Cart | undefi
|
||||
return reshapeCart(res.body.data.cart);
|
||||
}
|
||||
|
||||
export async function getCollection(handle: string): Promise<Collection | undefined> {
|
||||
export async function getCollection(
|
||||
handle: string
|
||||
): Promise<Collection | undefined> {
|
||||
'use cache';
|
||||
cacheTag(TAGS.collections);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await shopifyFetch<ShopifyCollectionOperation>({
|
||||
query: getCollectionQuery,
|
||||
tags: [TAGS.collections],
|
||||
variables: {
|
||||
handle
|
||||
}
|
||||
@@ -294,9 +309,12 @@ export async function getCollectionProducts({
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
}): Promise<Product[]> {
|
||||
'use cache';
|
||||
cacheTag(TAGS.collections, TAGS.products);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await shopifyFetch<ShopifyCollectionProductsOperation>({
|
||||
query: getCollectionProductsQuery,
|
||||
tags: [TAGS.collections, TAGS.products],
|
||||
variables: {
|
||||
handle: collection,
|
||||
reverse,
|
||||
@@ -309,13 +327,18 @@ export async function getCollectionProducts({
|
||||
return [];
|
||||
}
|
||||
|
||||
return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products));
|
||||
return reshapeProducts(
|
||||
removeEdgesAndNodes(res.body.data.collection.products)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCollections(): Promise<Collection[]> {
|
||||
'use cache';
|
||||
cacheTag(TAGS.collections);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await shopifyFetch<ShopifyCollectionsOperation>({
|
||||
query: getCollectionsQuery,
|
||||
tags: [TAGS.collections]
|
||||
query: getCollectionsQuery
|
||||
});
|
||||
const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections);
|
||||
const collections = [
|
||||
@@ -341,9 +364,12 @@ export async function getCollections(): Promise<Collection[]> {
|
||||
}
|
||||
|
||||
export async function getMenu(handle: string): Promise<Menu[]> {
|
||||
'use cache';
|
||||
cacheTag(TAGS.collections);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await shopifyFetch<ShopifyMenuOperation>({
|
||||
query: getMenuQuery,
|
||||
tags: [TAGS.collections],
|
||||
variables: {
|
||||
handle
|
||||
}
|
||||
@@ -352,7 +378,10 @@ export async function getMenu(handle: string): Promise<Menu[]> {
|
||||
return (
|
||||
res.body?.data?.menu?.items.map((item: { title: string; url: string }) => ({
|
||||
title: item.title,
|
||||
path: item.url.replace(domain, '').replace('/collections', '/search').replace('/pages', '')
|
||||
path: item.url
|
||||
.replace(domain, '')
|
||||
.replace('/collections', '/search')
|
||||
.replace('/pages', '')
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
@@ -360,7 +389,6 @@ export async function getMenu(handle: string): Promise<Menu[]> {
|
||||
export async function getPage(handle: string): Promise<Page> {
|
||||
const res = await shopifyFetch<ShopifyPageOperation>({
|
||||
query: getPageQuery,
|
||||
cache: 'no-store',
|
||||
variables: { handle }
|
||||
});
|
||||
|
||||
@@ -369,17 +397,19 @@ export async function getPage(handle: string): Promise<Page> {
|
||||
|
||||
export async function getPages(): Promise<Page[]> {
|
||||
const res = await shopifyFetch<ShopifyPagesOperation>({
|
||||
query: getPagesQuery,
|
||||
cache: 'no-store'
|
||||
query: getPagesQuery
|
||||
});
|
||||
|
||||
return removeEdgesAndNodes(res.body.data.pages);
|
||||
}
|
||||
|
||||
export async function getProduct(handle: string): Promise<Product | undefined> {
|
||||
'use cache';
|
||||
cacheTag(TAGS.products);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await shopifyFetch<ShopifyProductOperation>({
|
||||
query: getProductQuery,
|
||||
tags: [TAGS.products],
|
||||
variables: {
|
||||
handle
|
||||
}
|
||||
@@ -388,10 +418,15 @@ export async function getProduct(handle: string): Promise<Product | undefined> {
|
||||
return reshapeProduct(res.body.data.product, false);
|
||||
}
|
||||
|
||||
export async function getProductRecommendations(productId: string): Promise<Product[]> {
|
||||
export async function getProductRecommendations(
|
||||
productId: string
|
||||
): Promise<Product[]> {
|
||||
'use cache';
|
||||
cacheTag(TAGS.products);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await shopifyFetch<ShopifyProductRecommendationsOperation>({
|
||||
query: getProductRecommendationsQuery,
|
||||
tags: [TAGS.products],
|
||||
variables: {
|
||||
productId
|
||||
}
|
||||
@@ -409,9 +444,12 @@ export async function getProducts({
|
||||
reverse?: boolean;
|
||||
sortKey?: string;
|
||||
}): Promise<Product[]> {
|
||||
'use cache';
|
||||
cacheTag(TAGS.products);
|
||||
cacheLife('days');
|
||||
|
||||
const res = await shopifyFetch<ShopifyProductsOperation>({
|
||||
query: getProductsQuery,
|
||||
tags: [TAGS.products],
|
||||
variables: {
|
||||
query,
|
||||
reverse,
|
||||
@@ -426,8 +464,16 @@ export async function getProducts({
|
||||
export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
||||
// We always need to respond with a 200 status code to Shopify,
|
||||
// otherwise it will continue to retry the request.
|
||||
const collectionWebhooks = ['collections/create', 'collections/delete', 'collections/update'];
|
||||
const productWebhooks = ['products/create', 'products/delete', 'products/update'];
|
||||
const collectionWebhooks = [
|
||||
'collections/create',
|
||||
'collections/delete',
|
||||
'collections/update'
|
||||
];
|
||||
const productWebhooks = [
|
||||
'products/create',
|
||||
'products/delete',
|
||||
'products/update'
|
||||
];
|
||||
const topic = (await headers()).get('x-shopify-topic') || 'unknown';
|
||||
const secret = req.nextUrl.searchParams.get('secret');
|
||||
const isCollectionUpdate = collectionWebhooks.includes(topic);
|
||||
|
18
lib/utils.ts
18
lib/utils.ts
@@ -1,6 +1,13 @@
|
||||
import { ReadonlyURLSearchParams } from 'next/navigation';
|
||||
|
||||
export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => {
|
||||
export const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL
|
||||
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
|
||||
: 'http://localhost:3000';
|
||||
|
||||
export const createUrl = (
|
||||
pathname: string,
|
||||
params: URLSearchParams | ReadonlyURLSearchParams
|
||||
) => {
|
||||
const paramsString = params.toString();
|
||||
const queryString = `${paramsString.length ? '?' : ''}${paramsString}`;
|
||||
|
||||
@@ -8,10 +15,15 @@ export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyUR
|
||||
};
|
||||
|
||||
export const ensureStartsWith = (stringToCheck: string, startsWith: string) =>
|
||||
stringToCheck.startsWith(startsWith) ? stringToCheck : `${startsWith}${stringToCheck}`;
|
||||
stringToCheck.startsWith(startsWith)
|
||||
? stringToCheck
|
||||
: `${startsWith}${stringToCheck}`;
|
||||
|
||||
export const validateEnvironmentVariables = () => {
|
||||
const requiredEnvironmentVariables = ['SHOPIFY_STORE_DOMAIN', 'SHOPIFY_STOREFRONT_ACCESS_TOKEN'];
|
||||
const requiredEnvironmentVariables = [
|
||||
'SHOPIFY_STORE_DOMAIN',
|
||||
'SHOPIFY_STOREFRONT_ACCESS_TOKEN'
|
||||
];
|
||||
const missingEnvironmentVariables = [] as string[];
|
||||
|
||||
requiredEnvironmentVariables.forEach((envVar) => {
|
||||
|
Reference in New Issue
Block a user