diff --git a/lib/shopify/fragments/cart.ts b/lib/shopify/fragments/cart.ts deleted file mode 100644 index fc5c838dd..000000000 --- a/lib/shopify/fragments/cart.ts +++ /dev/null @@ -1,53 +0,0 @@ -import productFragment from './product'; - -const cartFragment = /* GraphQL */ ` - fragment cart on Cart { - id - checkoutUrl - cost { - subtotalAmount { - amount - currencyCode - } - totalAmount { - amount - currencyCode - } - totalTaxAmount { - amount - currencyCode - } - } - lines(first: 100) { - edges { - node { - id - quantity - cost { - totalAmount { - amount - currencyCode - } - } - merchandise { - ... on ProductVariant { - id - title - selectedOptions { - name - value - } - product { - ...product - } - } - } - } - } - } - totalQuantity - } - ${productFragment} -`; - -export default cartFragment; diff --git a/lib/shopify/fragments/image.ts b/lib/shopify/fragments/image.ts deleted file mode 100644 index 5d002f175..000000000 --- a/lib/shopify/fragments/image.ts +++ /dev/null @@ -1,10 +0,0 @@ -const imageFragment = /* GraphQL */ ` - fragment image on Image { - url - altText - width - height - } -`; - -export default imageFragment; diff --git a/lib/shopify/fragments/product.ts b/lib/shopify/fragments/product.ts deleted file mode 100644 index be14dedca..000000000 --- a/lib/shopify/fragments/product.ts +++ /dev/null @@ -1,64 +0,0 @@ -import imageFragment from './image'; -import seoFragment from './seo'; - -const productFragment = /* GraphQL */ ` - fragment product on Product { - id - handle - availableForSale - title - description - descriptionHtml - options { - id - name - values - } - priceRange { - maxVariantPrice { - amount - currencyCode - } - minVariantPrice { - amount - currencyCode - } - } - variants(first: 250) { - edges { - node { - id - title - availableForSale - selectedOptions { - name - value - } - price { - amount - currencyCode - } - } - } - } - featuredImage { - ...image - } - images(first: 20) { - edges { - node { - ...image - } - } - } - seo { - ...seo - } - tags - updatedAt - } - ${imageFragment} - ${seoFragment} -`; - -export default productFragment; diff --git a/lib/shopify/fragments/seo.ts b/lib/shopify/fragments/seo.ts deleted file mode 100644 index 2d4786c4f..000000000 --- a/lib/shopify/fragments/seo.ts +++ /dev/null @@ -1,8 +0,0 @@ -const seoFragment = /* GraphQL */ ` - fragment seo on SEO { - description - title - } -`; - -export default seoFragment; diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts deleted file mode 100644 index a8804d045..000000000 --- a/lib/shopify/index.ts +++ /dev/null @@ -1,445 +0,0 @@ -import { HIDDEN_PRODUCT_TAG, SHOPIFY_GRAPHQL_API_ENDPOINT, TAGS } from 'lib/constants'; -import { isShopifyError } from 'lib/type-guards'; -import { revalidateTag } from 'next/cache'; -import { headers } from 'next/headers'; -import { NextRequest, NextResponse } from 'next/server'; -import { - addToCartMutation, - createCartMutation, - editCartItemsMutation, - removeFromCartMutation -} from './mutations/cart'; -import { getCartQuery } from './queries/cart'; -import { - getCollectionProductsQuery, - getCollectionQuery, - getCollectionsQuery -} from './queries/collection'; -import { getMenuQuery } from './queries/menu'; -import { getPageQuery, getPagesQuery } from './queries/page'; -import { - getProductQuery, - getProductRecommendationsQuery, - getProductsQuery -} from './queries/product'; -import { - Cart, - Collection, - Connection, - Image, - Menu, - Page, - Product, - ShopifyAddToCartOperation, - ShopifyCart, - ShopifyCartOperation, - ShopifyCollection, - ShopifyCollectionOperation, - ShopifyCollectionProductsOperation, - ShopifyCollectionsOperation, - ShopifyCreateCartOperation, - ShopifyMenuOperation, - ShopifyPageOperation, - ShopifyPagesOperation, - ShopifyProduct, - ShopifyProductOperation, - ShopifyProductRecommendationsOperation, - ShopifyProductsOperation, - ShopifyRemoveFromCartOperation, - ShopifyUpdateCartOperation -} from './types'; - -const domain = `https://${process.env.SHOPIFY_STORE_DOMAIN!}`; -const endpoint = `${domain}${SHOPIFY_GRAPHQL_API_ENDPOINT}`; -const key = process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN!; - -type ExtractVariables = T extends { variables: object } ? T['variables'] : never; - -export async function shopifyFetch({ - cache = 'force-cache', - headers, - query, - tags, - variables -}: { - cache?: RequestCache; - headers?: HeadersInit; - query: string; - tags?: string[]; - variables?: ExtractVariables; -}): Promise<{ status: number; body: T } | never> { - try { - const result = await fetch(endpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Shopify-Storefront-Access-Token': key, - ...headers - }, - body: JSON.stringify({ - ...(query && { query }), - ...(variables && { variables }) - }), - cache, - ...(tags && { next: { tags } }) - }); - - const body = await result.json(); - - if (body.errors) { - throw body.errors[0]; - } - - return { - status: result.status, - body - }; - } catch (e) { - if (isShopifyError(e)) { - throw { - status: e.status || 500, - message: e.message, - query - }; - } - - throw { - error: e, - query - }; - } -} - -const removeEdgesAndNodes = (array: Connection) => { - return array.edges.map((edge) => edge?.node); -}; - -const reshapeCart = (cart: ShopifyCart): Cart => { - if (!cart.cost?.totalTaxAmount) { - cart.cost.totalTaxAmount = { - amount: '0.0', - currencyCode: 'USD' - }; - } - - return { - ...cart, - lines: removeEdgesAndNodes(cart.lines) - }; -}; - -const reshapeCollection = (collection: ShopifyCollection): Collection | undefined => { - if (!collection) { - return undefined; - } - - return { - ...collection, - path: `/search/${collection.handle}` - }; -}; - -const reshapeCollections = (collections: ShopifyCollection[]) => { - const reshapedCollections = []; - - for (const collection of collections) { - if (collection) { - const reshapedCollection = reshapeCollection(collection); - - if (reshapedCollection) { - reshapedCollections.push(reshapedCollection); - } - } - } - - return reshapedCollections; -}; - -const reshapeImages = (images: Connection, productTitle: string) => { - const flattened = removeEdgesAndNodes(images); - - return flattened.map((image) => { - const filename = image.url.match(/.*\/(.*)\..*/)[1]; - return { - ...image, - altText: image.altText || `${productTitle} - ${filename}` - }; - }); -}; - -const reshapeProduct = (product: ShopifyProduct, filterHiddenProducts: boolean = true) => { - if (!product || (filterHiddenProducts && product.tags.includes(HIDDEN_PRODUCT_TAG))) { - return undefined; - } - - const { images, variants, ...rest } = product; - - return { - ...rest, - images: reshapeImages(images, product.title), - variants: removeEdgesAndNodes(variants) - }; -}; - -const reshapeProducts = (products: ShopifyProduct[]) => { - const reshapedProducts = []; - - for (const product of products) { - if (product) { - const reshapedProduct = reshapeProduct(product); - - if (reshapedProduct) { - reshapedProducts.push(reshapedProduct); - } - } - } - - return reshapedProducts; -}; - -export async function createCart(): Promise { - const res = await shopifyFetch({ - query: createCartMutation, - cache: 'no-store' - }); - - return reshapeCart(res.body.data.cartCreate.cart); -} - -export async function addToCart( - cartId: string, - lines: { merchandiseId: string; quantity: number }[] -): Promise { - const res = await shopifyFetch({ - query: addToCartMutation, - variables: { - cartId, - lines - }, - cache: 'no-store' - }); - return reshapeCart(res.body.data.cartLinesAdd.cart); -} - -export async function removeFromCart(cartId: string, lineIds: string[]): Promise { - const res = await shopifyFetch({ - 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 { - const res = await shopifyFetch({ - query: editCartItemsMutation, - variables: { - cartId, - lines - }, - cache: 'no-store' - }); - - return reshapeCart(res.body.data.cartLinesUpdate.cart); -} - -export async function getCart(cartId: string): Promise { - const res = await shopifyFetch({ - query: getCartQuery, - variables: { cartId }, - cache: 'no-store' - }); - - // Old carts becomes `null` when you checkout. - if (!res.body.data.cart) { - return undefined; - } - - return reshapeCart(res.body.data.cart); -} - -export async function getCollection(handle: string): Promise { - const res = await shopifyFetch({ - query: getCollectionQuery, - tags: [TAGS.collections], - variables: { - handle - } - }); - - return reshapeCollection(res.body.data.collection); -} - -export async function getCollectionProducts({ - collection, - reverse, - sortKey -}: { - collection: string; - reverse?: boolean; - sortKey?: string; -}): Promise { - const res = await shopifyFetch({ - query: getCollectionProductsQuery, - tags: [TAGS.collections, TAGS.products], - variables: { - handle: collection, - reverse, - sortKey: sortKey === 'CREATED_AT' ? 'CREATED' : sortKey - } - }); - - if (!res.body.data.collection) { - console.log(`No collection found for \`${collection}\``); - return []; - } - - return reshapeProducts(removeEdgesAndNodes(res.body.data.collection.products)); -} - -export async function getCollections(): Promise { - const res = await shopifyFetch({ - query: getCollectionsQuery, - tags: [TAGS.collections] - }); - const shopifyCollections = removeEdgesAndNodes(res.body?.data?.collections); - const collections = [ - { - handle: '', - title: 'All', - description: 'All products', - seo: { - title: 'All', - description: 'All products' - }, - path: '/search', - updatedAt: new Date().toISOString() - }, - // Filter out the `hidden` collections. - // Collections that start with `hidden-*` need to be hidden on the search page. - ...reshapeCollections(shopifyCollections).filter( - (collection) => !collection.handle.startsWith('hidden') - ) - ]; - - return collections; -} - -export async function getMenu(handle: string): Promise { - const res = await shopifyFetch({ - query: getMenuQuery, - tags: [TAGS.collections], - variables: { - handle - } - }); - - 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', '') - })) || [] - ); -} - -export async function getPage(handle: string): Promise { - const res = await shopifyFetch({ - query: getPageQuery, - variables: { handle } - }); - - return res.body.data.pageByHandle; -} - -export async function getPages(): Promise { - const res = await shopifyFetch({ - query: getPagesQuery - }); - - return removeEdgesAndNodes(res.body.data.pages); -} - -export async function getProduct(handle: string): Promise { - const res = await shopifyFetch({ - query: getProductQuery, - tags: [TAGS.products], - variables: { - handle - } - }); - - return reshapeProduct(res.body.data.product, false); -} - -export async function getProductRecommendations(productId: string): Promise { - const res = await shopifyFetch({ - query: getProductRecommendationsQuery, - tags: [TAGS.products], - variables: { - productId - } - }); - - return reshapeProducts(res.body.data.productRecommendations); -} - -export async function getProducts({ - query, - reverse, - sortKey -}: { - query?: string; - reverse?: boolean; - sortKey?: string; -}): Promise { - const res = await shopifyFetch({ - query: getProductsQuery, - tags: [TAGS.products], - variables: { - query, - reverse, - sortKey - } - }); - - return reshapeProducts(removeEdgesAndNodes(res.body.data.products)); -} - -// This is called from `app/api/revalidate.ts` so providers can control revalidation logic. -export async function revalidate(req: NextRequest): Promise { - // 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 topic = headers().get('x-shopify-topic') || 'unknown'; - const secret = req.nextUrl.searchParams.get('secret'); - const isCollectionUpdate = collectionWebhooks.includes(topic); - const isProductUpdate = productWebhooks.includes(topic); - - if (!secret || secret !== process.env.SHOPIFY_REVALIDATION_SECRET) { - console.error('Invalid revalidation secret.'); - return NextResponse.json({ status: 200 }); - } - - if (!isCollectionUpdate && !isProductUpdate) { - // We don't need to revalidate anything for any other topics. - return NextResponse.json({ status: 200 }); - } - - if (isCollectionUpdate) { - revalidateTag(TAGS.collections); - } - - if (isProductUpdate) { - revalidateTag(TAGS.products); - } - - return NextResponse.json({ status: 200, revalidated: true, now: Date.now() }); -} diff --git a/lib/shopify/mutations/cart.ts b/lib/shopify/mutations/cart.ts deleted file mode 100644 index 4cc1b5ac6..000000000 --- a/lib/shopify/mutations/cart.ts +++ /dev/null @@ -1,45 +0,0 @@ -import cartFragment from '../fragments/cart'; - -export const addToCartMutation = /* GraphQL */ ` - mutation addToCart($cartId: ID!, $lines: [CartLineInput!]!) { - cartLinesAdd(cartId: $cartId, lines: $lines) { - cart { - ...cart - } - } - } - ${cartFragment} -`; - -export const createCartMutation = /* GraphQL */ ` - mutation createCart($lineItems: [CartLineInput!]) { - cartCreate(input: { lines: $lineItems }) { - cart { - ...cart - } - } - } - ${cartFragment} -`; - -export const editCartItemsMutation = /* GraphQL */ ` - mutation editCartItems($cartId: ID!, $lines: [CartLineUpdateInput!]!) { - cartLinesUpdate(cartId: $cartId, lines: $lines) { - cart { - ...cart - } - } - } - ${cartFragment} -`; - -export const removeFromCartMutation = /* GraphQL */ ` - mutation removeFromCart($cartId: ID!, $lineIds: [ID!]!) { - cartLinesRemove(cartId: $cartId, lineIds: $lineIds) { - cart { - ...cart - } - } - } - ${cartFragment} -`; diff --git a/lib/shopify/queries/cart.ts b/lib/shopify/queries/cart.ts deleted file mode 100644 index 044e47f66..000000000 --- a/lib/shopify/queries/cart.ts +++ /dev/null @@ -1,10 +0,0 @@ -import cartFragment from '../fragments/cart'; - -export const getCartQuery = /* GraphQL */ ` - query getCart($cartId: ID!) { - cart(id: $cartId) { - ...cart - } - } - ${cartFragment} -`; diff --git a/lib/shopify/queries/collection.ts b/lib/shopify/queries/collection.ts deleted file mode 100644 index 6396ff8eb..000000000 --- a/lib/shopify/queries/collection.ts +++ /dev/null @@ -1,56 +0,0 @@ -import productFragment from '../fragments/product'; -import seoFragment from '../fragments/seo'; - -const collectionFragment = /* GraphQL */ ` - fragment collection on Collection { - handle - title - description - seo { - ...seo - } - updatedAt - } - ${seoFragment} -`; - -export const getCollectionQuery = /* GraphQL */ ` - query getCollection($handle: String!) { - collection(handle: $handle) { - ...collection - } - } - ${collectionFragment} -`; - -export const getCollectionsQuery = /* GraphQL */ ` - query getCollections { - collections(first: 100, sortKey: TITLE) { - edges { - node { - ...collection - } - } - } - } - ${collectionFragment} -`; - -export const getCollectionProductsQuery = /* GraphQL */ ` - query getCollectionProducts( - $handle: String! - $sortKey: ProductCollectionSortKeys - $reverse: Boolean - ) { - collection(handle: $handle) { - products(sortKey: $sortKey, reverse: $reverse, first: 100) { - edges { - node { - ...product - } - } - } - } - } - ${productFragment} -`; diff --git a/lib/shopify/queries/menu.ts b/lib/shopify/queries/menu.ts deleted file mode 100644 index d05b09949..000000000 --- a/lib/shopify/queries/menu.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const getMenuQuery = /* GraphQL */ ` - query getMenu($handle: String!) { - menu(handle: $handle) { - items { - title - url - } - } - } -`; diff --git a/lib/shopify/queries/page.ts b/lib/shopify/queries/page.ts deleted file mode 100644 index ac6f6f986..000000000 --- a/lib/shopify/queries/page.ts +++ /dev/null @@ -1,41 +0,0 @@ -import seoFragment from '../fragments/seo'; - -const pageFragment = /* GraphQL */ ` - fragment page on Page { - ... on Page { - id - title - handle - body - bodySummary - seo { - ...seo - } - createdAt - updatedAt - } - } - ${seoFragment} -`; - -export const getPageQuery = /* GraphQL */ ` - query getPage($handle: String!) { - pageByHandle(handle: $handle) { - ...page - } - } - ${pageFragment} -`; - -export const getPagesQuery = /* GraphQL */ ` - query getPages { - pages(first: 100) { - edges { - node { - ...page - } - } - } - } - ${pageFragment} -`; diff --git a/lib/shopify/queries/product.ts b/lib/shopify/queries/product.ts deleted file mode 100644 index d3f12bd0f..000000000 --- a/lib/shopify/queries/product.ts +++ /dev/null @@ -1,32 +0,0 @@ -import productFragment from '../fragments/product'; - -export const getProductQuery = /* GraphQL */ ` - query getProduct($handle: String!) { - product(handle: $handle) { - ...product - } - } - ${productFragment} -`; - -export const getProductsQuery = /* GraphQL */ ` - query getProducts($sortKey: ProductSortKeys, $reverse: Boolean, $query: String) { - products(sortKey: $sortKey, reverse: $reverse, query: $query, first: 100) { - edges { - node { - ...product - } - } - } - } - ${productFragment} -`; - -export const getProductRecommendationsQuery = /* GraphQL */ ` - query getProductRecommendations($productId: ID!) { - productRecommendations(productId: $productId) { - ...product - } - } - ${productFragment} -`; diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts deleted file mode 100644 index 23dc02d46..000000000 --- a/lib/shopify/types.ts +++ /dev/null @@ -1,265 +0,0 @@ -export type Maybe = T | null; - -export type Connection = { - edges: Array>; -}; - -export type Edge = { - node: T; -}; - -export type Cart = Omit & { - lines: CartItem[]; -}; - -export type CartItem = { - id: string; - quantity: number; - cost: { - totalAmount: Money; - }; - merchandise: { - id: string; - title: string; - selectedOptions: { - name: string; - value: string; - }[]; - product: Product; - }; -}; - -export type Collection = ShopifyCollection & { - path: string; -}; - -export type Image = { - url: string; - altText: string; - width: number; - height: number; -}; - -export type Menu = { - title: string; - path: string; -}; - -export type Money = { - amount: string; - currencyCode: string; -}; - -export type Page = { - id: string; - title: string; - handle: string; - body: string; - bodySummary: string; - seo?: SEO; - createdAt: string; - updatedAt: string; -}; - -export type Product = Omit & { - variants: ProductVariant[]; - images: Image[]; -}; - -export type ProductOption = { - id: string; - name: string; - values: string[]; -}; - -export type ProductVariant = { - id: string; - title: string; - availableForSale: boolean; - selectedOptions: { - name: string; - value: string; - }[]; - price: Money; -}; - -export type SEO = { - title: string; - description: string; -}; - -export type ShopifyCart = { - id: string; - checkoutUrl: string; - cost: { - subtotalAmount: Money; - totalAmount: Money; - totalTaxAmount: Money; - }; - lines: Connection; - totalQuantity: number; -}; - -export type ShopifyCollection = { - handle: string; - title: string; - description: string; - seo: SEO; - updatedAt: string; -}; - -export type ShopifyProduct = { - id: string; - handle: string; - availableForSale: boolean; - title: string; - description: string; - descriptionHtml: string; - options: ProductOption[]; - priceRange: { - maxVariantPrice: Money; - minVariantPrice: Money; - }; - variants: Connection; - featuredImage: Image; - images: Connection; - seo: SEO; - tags: string[]; - updatedAt: string; -}; - -export type ShopifyCartOperation = { - data: { - cart: ShopifyCart; - }; - variables: { - cartId: string; - }; -}; - -export type ShopifyCreateCartOperation = { - data: { cartCreate: { cart: ShopifyCart } }; -}; - -export type ShopifyAddToCartOperation = { - data: { - cartLinesAdd: { - cart: ShopifyCart; - }; - }; - variables: { - cartId: string; - lines: { - merchandiseId: string; - quantity: number; - }[]; - }; -}; - -export type ShopifyRemoveFromCartOperation = { - data: { - cartLinesRemove: { - cart: ShopifyCart; - }; - }; - variables: { - cartId: string; - lineIds: string[]; - }; -}; - -export type ShopifyUpdateCartOperation = { - data: { - cartLinesUpdate: { - cart: ShopifyCart; - }; - }; - variables: { - cartId: string; - lines: { - id: string; - merchandiseId: string; - quantity: number; - }[]; - }; -}; - -export type ShopifyCollectionOperation = { - data: { - collection: ShopifyCollection; - }; - variables: { - handle: string; - }; -}; - -export type ShopifyCollectionProductsOperation = { - data: { - collection: { - products: Connection; - }; - }; - variables: { - handle: string; - reverse?: boolean; - sortKey?: string; - }; -}; - -export type ShopifyCollectionsOperation = { - data: { - collections: Connection; - }; -}; - -export type ShopifyMenuOperation = { - data: { - menu?: { - items: { - title: string; - url: string; - }[]; - }; - }; - variables: { - handle: string; - }; -}; - -export type ShopifyPageOperation = { - data: { pageByHandle: Page }; - variables: { handle: string }; -}; - -export type ShopifyPagesOperation = { - data: { - pages: Connection; - }; -}; - -export type ShopifyProductOperation = { - data: { product: ShopifyProduct }; - variables: { - handle: string; - }; -}; - -export type ShopifyProductRecommendationsOperation = { - data: { - productRecommendations: ShopifyProduct[]; - }; - variables: { - productId: string; - }; -}; - -export type ShopifyProductsOperation = { - data: { - products: Connection; - }; - variables: { - query?: string; - reverse?: boolean; - sortKey?: string; - }; -};