From f6ff074977242134e73f98db6909a35efe5b4f97 Mon Sep 17 00:00:00 2001 From: gimenes Date: Sun, 3 Sep 2023 19:33:46 -0300 Subject: [PATCH] migrate shopify --- import_map.json | 6 +- shopify/actions/cart/addItems.ts | 29 ++-- shopify/actions/cart/updateCoupons.ts | 27 ++-- shopify/actions/cart/updateItems.ts | 40 ++--- shopify/hooks/context.ts | 15 +- shopify/hooks/useCart.ts | 46 +++--- shopify/loaders/ProductDetailsPage.ts | 9 +- shopify/loaders/ProductList.ts | 13 +- shopify/loaders/ProductListingPage.ts | 13 +- shopify/loaders/cart.ts | 61 ++++---- shopify/mod.ts | 23 +-- shopify/runtime.ts | 2 +- shopify/utils/cart.ts | 22 +++ shopify/utils/client.ts | 176 ---------------------- shopify/utils/constants.ts | 1 - shopify/utils/fragments/cart.ts | 46 +++++- shopify/utils/fragments/product.ts | 41 ++++- shopify/utils/fragments/productVarian.ts | 42 ------ shopify/utils/fragments/productVariant.ts | 66 ++++++++ shopify/utils/gql.ts | 1 - shopify/utils/queries/addItem.ts | 29 ++++ shopify/utils/queries/cart.ts | 14 ++ shopify/utils/queries/createCart.ts | 17 +++ shopify/utils/queries/product.ts | 16 ++ shopify/utils/queries/products.ts | 27 ++++ shopify/utils/queries/updateCart.ts | 24 +++ shopify/utils/queries/updateCoupon.ts | 26 ++++ shopify/utils/transform.ts | 8 +- shopify/utils/types.ts | 97 ------------ utils/graphql.ts | 46 ++++++ 30 files changed, 524 insertions(+), 459 deletions(-) create mode 100644 shopify/utils/cart.ts delete mode 100644 shopify/utils/client.ts delete mode 100644 shopify/utils/constants.ts delete mode 100644 shopify/utils/fragments/productVarian.ts create mode 100644 shopify/utils/fragments/productVariant.ts delete mode 100644 shopify/utils/gql.ts create mode 100644 shopify/utils/queries/addItem.ts create mode 100644 shopify/utils/queries/cart.ts create mode 100644 shopify/utils/queries/createCart.ts create mode 100644 shopify/utils/queries/product.ts create mode 100644 shopify/utils/queries/products.ts create mode 100644 shopify/utils/queries/updateCart.ts create mode 100644 shopify/utils/queries/updateCoupon.ts create mode 100644 utils/graphql.ts diff --git a/import_map.json b/import_map.json index 80dc0e3df..de4d1f29e 100644 --- a/import_map.json +++ b/import_map.json @@ -1,6 +1,6 @@ { "imports": { - "$live/": "https://deno.land/x/deco@1.34.2/", + "$live/": "https://denopkg.com/deco-cx/deco@1.34.3/", "$fresh/": "https://denopkg.com/denoland/fresh@1.4.2/", "preact": "https://esm.sh/preact@10.15.1", "preact/": "https://esm.sh/preact@10.15.1/", @@ -9,7 +9,7 @@ "@preact/signals-core": "https://esm.sh/@preact/signals-core@1.3.0", "std/": "https://deno.land/std@0.190.0/", "partytown/": "https://deno.land/x/partytown@0.3.4/", - "deco-sites/std/": "https://denopkg.com/deco-sites/std@1.21.6/", - "deco/": "https://deno.land/x/deco@1.34.2/" + "deco-sites/std/": "https://denopkg.com/deco-sites/std@1.21.7/", + "deco/": "https://denopkg.com/deco-cx/deco@1.34.3/" } } diff --git a/shopify/actions/cart/addItems.ts b/shopify/actions/cart/addItems.ts index 54b866e93..305dce726 100644 --- a/shopify/actions/cart/addItems.ts +++ b/shopify/actions/cart/addItems.ts @@ -1,7 +1,7 @@ -import { getCookies } from "std/http/mod.ts"; import { AppContext } from "../../mod.ts"; -import { SHOPIFY_COOKIE_NAME } from "../../utils/constants.ts"; -import type { Cart } from "../../utils/types.ts"; +import { getCartCookie, setCartCookie } from "../../utils/cart.ts"; +import { Data, query, Variables } from "../../utils/queries/addItem.ts"; +import { Data as CartData } from "../../utils/queries/cart.ts"; type UpdateLineProps = { lines: { @@ -13,20 +13,25 @@ type UpdateLineProps = { }; const action = async ( - props: UpdateLineProps, + { lines }: UpdateLineProps, req: Request, ctx: AppContext, -): Promise => { - const { client } = ctx; +): Promise => { + const { storefront } = ctx; + const cartId = getCartCookie(req.headers); - const reqCookies = getCookies(req.headers); - const cartId = reqCookies[SHOPIFY_COOKIE_NAME]; - const response = await client.cart.addItem({ - cartId: cartId, - lines: [props.lines], + if (!cartId) { + throw new Error("Missing cart id"); + } + + const { payload: { cart } } = await storefront.query({ + variables: { cartId, lines }, + query, }); - return response?.cartLinesAdd || { cart: { id: cartId } }; + setCartCookie(ctx.response.headers, cartId); + + return cart; }; export default action; diff --git a/shopify/actions/cart/updateCoupons.ts b/shopify/actions/cart/updateCoupons.ts index a01edbc3b..431567f92 100644 --- a/shopify/actions/cart/updateCoupons.ts +++ b/shopify/actions/cart/updateCoupons.ts @@ -1,7 +1,7 @@ -import { getCookies } from "std/http/mod.ts"; import { AppContext } from "../../mod.ts"; -import { SHOPIFY_COOKIE_NAME } from "../../utils/constants.ts"; -import type { Cart } from "../../utils/types.ts"; +import { getCartCookie, setCartCookie } from "../../utils/cart.ts"; +import { Data as CartData } from "../../utils/queries/cart.ts"; +import { Data, Variables, query } from "../../utils/queries/updateCoupon.ts"; type AddCouponProps = { discountCodes: string[]; @@ -11,17 +11,22 @@ const action = async ( props: AddCouponProps, req: Request, ctx: AppContext, -): Promise => { - const { client } = ctx; +): Promise => { + const { storefront } = ctx; + const cartId = getCartCookie(req.headers); - const reqCookies = getCookies(req.headers); - const cartId = reqCookies[SHOPIFY_COOKIE_NAME]; - const response = await client.cart.addCoupon({ - cartId: cartId, - discountCodes: [...props.discountCodes], + if (!cartId) { + throw new Error("Missing cart id"); + } + + const { payload: { cart } } = await storefront.query({ + variables: { cartId, discountCodes: props.discountCodes }, + query, }); - return response?.cartDiscountCodesUpdate || { cart: { id: cartId } }; + setCartCookie(ctx.response.headers, cartId); + + return cart; }; export default action; diff --git a/shopify/actions/cart/updateItems.ts b/shopify/actions/cart/updateItems.ts index e6407d83c..dadc32941 100644 --- a/shopify/actions/cart/updateItems.ts +++ b/shopify/actions/cart/updateItems.ts @@ -1,35 +1,35 @@ -import { getCookies } from "std/http/mod.ts"; import { AppContext } from "../../mod.ts"; -import { SHOPIFY_COOKIE_NAME } from "../../utils/constants.ts"; -import type { Cart } from "../../utils/types.ts"; - -export interface updateCartQueryProps { - cartLinesUpdate: Cart; -} +import { getCartCookie, setCartCookie } from "../../utils/cart.ts"; +import { Data as CartData } from "../../utils/queries/cart.ts"; +import { Data, query, Variables } from "../../utils/queries/updateCart.ts"; type UpdateLineProps = { - lines: { + lines: Array<{ id: string; quantity?: number; - }; + }>; }; const action = async ( - props: UpdateLineProps, + { lines }: UpdateLineProps, req: Request, ctx: AppContext, -): Promise => { - const { client } = ctx; +): Promise => { + const { storefront } = ctx; + const cartId = getCartCookie(req.headers); + + if (!cartId) { + throw new Error("Missing cart id"); + } + + const { payload: { cart } } = await storefront.query({ + variables: { cartId, lines }, + query, + }); - const reqCookies = getCookies(req.headers); - const cartId = reqCookies[SHOPIFY_COOKIE_NAME]; - const response: updateCartQueryProps | undefined = await client.cart - .updateItems({ - cartId: cartId, - lines: [props.lines], - }); + setCartCookie(ctx.response.headers, cartId); - return response?.cartLinesUpdate || { cart: { id: cartId } }; + return cart; }; export default action; diff --git a/shopify/hooks/context.ts b/shopify/hooks/context.ts index 7e6ea27e8..d80cb1cd2 100644 --- a/shopify/hooks/context.ts +++ b/shopify/hooks/context.ts @@ -1,7 +1,7 @@ import { IS_BROWSER } from "$fresh/runtime.ts"; import { signal } from "@preact/signals"; -import { Runtime } from "../runtime.ts"; -import { Cart } from "../utils/types.ts"; +import { invoke } from "../runtime.ts"; +import { Fragment as Cart } from "../utils/fragments/cart.ts"; interface Context { cart: Cart; @@ -45,13 +45,10 @@ const enqueue = ( return queue; }; -const load = async (signal: AbortSignal) => { - const cart = await Runtime.shopify.loaders.cart({}, {signal}) - - return { - cart, - }; -}; +const load = (signal: AbortSignal) => + invoke({ + cart: invoke.shopify.loaders.cart(), + }, { signal }); if (IS_BROWSER) { enqueue(load); diff --git a/shopify/hooks/useCart.ts b/shopify/hooks/useCart.ts index 1dc62b1ce..9da24b020 100644 --- a/shopify/hooks/useCart.ts +++ b/shopify/hooks/useCart.ts @@ -1,6 +1,8 @@ +import { InvocationFuncFor } from "deco/clients/withManifest.ts"; import type { AnalyticsItem } from "../../commerce/types.ts"; -import { Runtime } from "../runtime.ts"; -import { Cart, Item } from "../utils/types.ts"; +import { Manifest } from "../manifest.gen.ts"; +import { invoke } from "../runtime.ts"; +import { Fragment as Cart, Item } from "../utils/fragments/cart.ts"; import { state as storeState } from "./context.ts"; export const itemToAnalyticsItem = ( @@ -9,7 +11,10 @@ export const itemToAnalyticsItem = ( ): AnalyticsItem => ({ item_id: item.id, item_name: item.merchandise.product.title, - discount: item.cost.compareAtAmountPerQuantity ? item.cost.compareAtAmountPerQuantity.amount - item.cost.amountPerQuantity?.amount : 0, + discount: item.cost.compareAtAmountPerQuantity + ? item.cost.compareAtAmountPerQuantity.amount - + item.cost.amountPerQuantity?.amount + : 0, item_variant: item.merchandise.title, price: item.cost.amountPerQuantity.amount, index, @@ -18,25 +23,30 @@ export const itemToAnalyticsItem = ( const { cart, loading } = storeState; -const wrap = - (action: (p: T, init?: RequestInit | undefined) => Promise) => - (p: T) => - storeState.enqueue(async (signal) => ({ - cart: await action(p, { signal }), - })); +type PropsOf = T extends (props: infer P, r: any, ctx: any) => any ? P + : T extends (props: infer P, r: any) => any ? P + : T extends (props: infer P) => any ? P + : never; + +type Actions = + | "shopify/actions/cart/addItems.ts" + | "shopify/actions/cart/updateItems.ts" + | "shopify/actions/cart/updateCoupons.ts"; + +const action = + (key: Actions) => (props: PropsOf>) => + storeState.enqueue((signal) => + invoke({ cart: { key, props } }, { signal }) satisfies Promise< + { cart: Cart } + > + ); const state = { cart, loading, - addItems: wrap( - Runtime.shopify.actions.cart.addItems, - ), - updateItems: wrap( - Runtime.shopify.actions.cart.updateItems, - ), - addCouponsToCart: wrap( - Runtime.shopify.actions.cart.updateCoupons, - ), + addItems: action("shopify/actions/cart/addItems.ts"), + updateItems: action("shopify/actions/cart/updateItems.ts"), + addCouponsToCart: action("shopify/actions/cart/updateCoupons.ts"), }; export const useCart = () => state; diff --git a/shopify/loaders/ProductDetailsPage.ts b/shopify/loaders/ProductDetailsPage.ts index 7e6da7d42..d8aa78de4 100644 --- a/shopify/loaders/ProductDetailsPage.ts +++ b/shopify/loaders/ProductDetailsPage.ts @@ -2,6 +2,7 @@ import { ProductDetailsPage } from "../../commerce/types.ts"; import { AppContext } from "../../shopify/mod.ts"; import { toProductPage } from "../../shopify/utils/transform.ts"; import type { RequestURLParam } from "../../website/functions/requestToParam.ts"; +import { Data, query, Variables } from "../utils/queries/product.ts"; export interface Props { slug: RequestURLParam; @@ -16,7 +17,7 @@ const loader = async ( _req: Request, ctx: AppContext, ): Promise => { - const { client } = ctx; + const { storefront } = ctx; const { slug } = props; const splitted = slug?.split("-"); @@ -24,8 +25,10 @@ const loader = async ( const handle = splitted.slice(0, maybeSkuId ? -1 : undefined).join("-"); - // search products on Shopify. Feel free to change any of these parameters - const data = await client.product(handle); + const data = await storefront.query({ + query, + variables: { handle }, + }); if (!data?.product) { return null; diff --git a/shopify/loaders/ProductList.ts b/shopify/loaders/ProductList.ts index dbc2ab021..f58cb1f8a 100644 --- a/shopify/loaders/ProductList.ts +++ b/shopify/loaders/ProductList.ts @@ -1,5 +1,10 @@ import type { Product } from "../../commerce/types.ts"; import { AppContext } from "../../shopify/mod.ts"; +import { + Data, + query as productsQuery, + Variables, +} from "../utils/queries/products.ts"; import { toProduct } from "../utils/transform.ts"; export interface Props { @@ -18,15 +23,15 @@ const loader = async ( _req: Request, ctx: AppContext, ): Promise => { - const { client } = ctx; + const { storefront } = ctx; const count = props.count ?? 12; const query = props.query || ""; // search products on Shopify. Feel free to change any of these parameters - const data = await client.products({ - first: count, - query, + const data = await storefront.query({ + query: productsQuery, + variables: { first: count, query }, }); // Transform Shopify product format into schema.org's compatible format diff --git a/shopify/loaders/ProductListingPage.ts b/shopify/loaders/ProductListingPage.ts index c0c578d3f..9446d7b44 100644 --- a/shopify/loaders/ProductListingPage.ts +++ b/shopify/loaders/ProductListingPage.ts @@ -1,5 +1,10 @@ import type { ProductListingPage } from "../../commerce/types.ts"; import { AppContext } from "../../shopify/mod.ts"; +import { + Data, + query as productsQuery, + Variables, +} from "../utils/queries/products.ts"; import { toProduct } from "../utils/transform.ts"; export interface Props { @@ -24,16 +29,16 @@ const loader = async ( ctx: AppContext, ): Promise => { const url = new URL(req.url); - const { client } = ctx; + const { storefront } = ctx; const count = props.count ?? 12; const query = props.query || url.searchParams.get("q") || ""; const page = Number(url.searchParams.get("page")) ?? 0; // search products on Shopify. Feel free to change any of these parameters - const data = await client.products({ - first: count, - query: query, + const data = await storefront.query({ + query: productsQuery, + variables: { first: count, query: query }, }); // Transform Shopify product format into schema.org's compatible format diff --git a/shopify/loaders/cart.ts b/shopify/loaders/cart.ts index 2ccc3e5ed..3401c8527 100644 --- a/shopify/loaders/cart.ts +++ b/shopify/loaders/cart.ts @@ -1,48 +1,41 @@ -import { getCookies, getSetCookies, setCookie } from "std/http/mod.ts"; import { AppContext } from "../mod.ts"; -import { SHOPIFY_COOKIE_NAME } from "../utils/constants.ts"; -import type { Cart } from "../utils/types.ts"; +import { getCartCookie, setCartCookie } from "../utils/cart.ts"; +import { + Data as CartData, + query as getCart, + Variables as CartVariables, +} from "../utils/queries/cart.ts"; +import { + Data as CreateCartData, + query as createCart, + Variables as CreateVariablesData, +} from "../utils/queries/createCart.ts"; const loader = async ( _props: unknown, req: Request, ctx: AppContext, -): Promise => { - const { client } = ctx; +): Promise => { + const { storefront } = ctx; + const maybeCartId = getCartCookie(req.headers); - try { - const r = await client.cart.create(); + const cartId = maybeCartId || + await storefront.query({ + query: createCart, + }).then((data) => data.payload.cart.id); - const reqCookies = getCookies(req.headers); - const cartIdCookie = reqCookies[SHOPIFY_COOKIE_NAME]; - if (cartIdCookie) { - const queryResponse = await client.cart.get(cartIdCookie); - if (!queryResponse?.cart?.id) { - throw new Error("unable to create a cart"); - } - return queryResponse; - } - - if (!r?.payload?.cart.id) { - throw new Error("unable to create a cart"); - } - const { cart } = r.payload; + if (!cartId) { + throw new Error("Missing cart id"); + } - const cookies = getSetCookies(ctx.response.headers); - cookies.push({ name: SHOPIFY_COOKIE_NAME, value: cart.id }); + const cart = await storefront.query({ + query: getCart, + variables: { id: cartId }, + }).then((data) => data.cart); - for (const cookie of cookies) { - setCookie(ctx.response.headers, { - ...cookie, - domain: new URL(req.url).hostname, - }); - } + setCartCookie(ctx.response.headers, cartId); - return { cart: { id: cart.id } }; - } catch (error) { - console.error(error); - throw error; - } + return cart; }; export default loader; diff --git a/shopify/mod.ts b/shopify/mod.ts index 8cb0b96c1..644f87056 100644 --- a/shopify/mod.ts +++ b/shopify/mod.ts @@ -1,6 +1,7 @@ import type { App, FnContext } from "$live/mod.ts"; +import { fetchSafe } from "../utils/fetch.ts"; +import { createGraphqlClient } from "../utils/graphql.ts"; import manifest, { Manifest } from "./manifest.gen.ts"; -import { createClient } from "./utils/client.ts"; export type AppContext = FnContext; @@ -24,18 +25,22 @@ export interface Props { } export interface State extends Props { - client: ReturnType; + storefront: ReturnType; } /** * @title Shopify */ export default function App(props: Props): App { - return { - state: { - ...props, - client: createClient(props), - }, - manifest, - }; + const { storeName, storefrontAccessToken } = props; + const storefront = createGraphqlClient({ + endpoint: `https://${storeName}.myshopify.com/api/2023-07/graphql.json`, + fetcher: fetchSafe, + headers: new Headers({ + "Content-Type": "application/json", + "X-Shopify-Storefront-Access-Token": storefrontAccessToken, + }), + }); + + return { state: { ...props, storefront }, manifest }; } diff --git a/shopify/runtime.ts b/shopify/runtime.ts index a8619e400..b7cde6a84 100644 --- a/shopify/runtime.ts +++ b/shopify/runtime.ts @@ -1,4 +1,4 @@ import { proxy } from "$live/clients/withManifest.ts"; import { Manifest } from "./manifest.gen.ts"; -export const Runtime = proxy(); +export const invoke = proxy(); diff --git a/shopify/utils/cart.ts b/shopify/utils/cart.ts new file mode 100644 index 000000000..64c1852ce --- /dev/null +++ b/shopify/utils/cart.ts @@ -0,0 +1,22 @@ +import { getCookies, setCookie } from "std/http/cookie.ts"; + +const CART_COOKIE = "shopify_cart_id"; + +const ONE_WEEK_MS = 7 * 24 * 3600 * 1_000; + +export const getCartCookie = (headers: Headers): string | undefined => { + const cookies = getCookies(headers); + + return cookies[CART_COOKIE]; +}; + +export const setCartCookie = (headers: Headers, cartId: string) => + setCookie(headers, { + name: CART_COOKIE, + value: cartId, + path: "/", + expires: new Date(Date.now() + ONE_WEEK_MS), + httpOnly: true, + secure: true, + sameSite: "Lax", + }); diff --git a/shopify/utils/client.ts b/shopify/utils/client.ts deleted file mode 100644 index 7f149f71b..000000000 --- a/shopify/utils/client.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { fetchAPI } from "../../utils/fetch.ts"; -import { Props } from "../mod.ts"; -import { cartFragment } from "./fragments/cart.ts"; -import { productFragment } from "./fragments/product.ts"; -import { gql } from "./gql.ts"; -import { Cart, Product } from "./types.ts"; - -export const createClient = ( - { storeName, storefrontAccessToken }: Props, -) => { - const graphql = async ( - query: string, - fragments: string[] = [], - variables: Record = {}, - ) => { - const finalQuery = [query, ...fragments].join("\n"); - const { data, errors } = await fetchAPI<{ data?: T; errors: unknown[] }>( - `https://${storeName}.myshopify.com/api/2022-10/graphql.json`, - { - method: "POST", - body: JSON.stringify({ - query: finalQuery, - variables, - }), - headers: { - "Content-Type": "application/json", - "X-Shopify-Storefront-Access-Token": storefrontAccessToken, - }, - }, - ); - - if (Array.isArray(errors) && errors.length > 0) { - console.error(Deno.inspect(errors, { depth: 100, colors: true })); - - throw new Error( - `Error while running query:\n${finalQuery}\n\n${ - JSON.stringify(variables) - }`, - ); - } - - return data; - }; - - const product = (handle: string) => - graphql<{ product: Product }>( - gql` - query GetProduct($handle: String) { - product(handle: $handle) { - ...ProductFragment - } - } - `, - [productFragment], - { handle }, - ); - - const products = ( - options: { first: number; after?: string; query?: string }, - ) => - graphql< - { products: { nodes: Product[]; pageInfo: { hasNextPage: boolean } } } - >( - gql` - query GetProducts($first: Int, $after: String, $query: String) { - products(first: $first, after: $after, query: $query) { - pageInfo { - hasNextPage - } - nodes { - ...ProductFragment - } - } - } - `, - [productFragment], - options, - ); - - const createCart = () => - graphql<{ - payload: { - cart: { - id: string; - }; - }; - }>(gql` - mutation createCart { - payload: cartCreate { - cart { - id - } - } - }`); - - const getCart = (id: string) => - graphql( - gql` - query($id: ID!) { cart(id: $id) { ...CartFragment } } - `, - [cartFragment], - { id }, - ); - - const addItem = (variables: { - cartId: string; - lines: Array<{ - merchandiseId: string; - attributes?: Array<{ key: string; value: string }>; - quantity?: number; - sellingPlanId?: string; - }>; - }) => - graphql<{ cartLinesAdd: Cart }>( - gql` - mutation add($cartId: ID!, $lines: [CartLineInput!]!) { - cartLinesAdd(cartId: $cartId, lines: $lines) { - cart { ...CartFragment } - } - }`, - [cartFragment], - variables, - ); - - const addCoupon = (variables: { - cartId: string; - discountCodes: string[]; - }) => - graphql<{ cartDiscountCodesUpdate: Cart }>( - gql` - mutation addCoupon($cartId: ID!, $discountCodes: [String!]!) { - cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) { - cart { ...CartFragment } - userErrors { - field - message - } - } - } - `, - [cartFragment], - variables, - ); - - const updateItems = (variables: { - cartId: string; - lines: Array<{ - id: string; - quantity?: number; - }>; - }) => - graphql<{ cartLinesUpdate: Cart }>( - gql` - mutation update($cartId: ID!, $lines: [CartLineUpdateInput!]!) { - cartLinesUpdate(cartId: $cartId, lines: $lines) { - cart { ...CartFragment } - } - } - `, - [cartFragment], - variables, - ); - - return { - product, - products, - cart: { - create: createCart, - get: getCart, - addItem, - addCoupon, - updateItems, - }, - graphql, - }; -}; diff --git a/shopify/utils/constants.ts b/shopify/utils/constants.ts deleted file mode 100644 index 831d3ffe3..000000000 --- a/shopify/utils/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const SHOPIFY_COOKIE_NAME = "shopify_cart_id"; diff --git a/shopify/utils/fragments/cart.ts b/shopify/utils/fragments/cart.ts index a8dd6086b..789e4470a 100644 --- a/shopify/utils/fragments/cart.ts +++ b/shopify/utils/fragments/cart.ts @@ -1,7 +1,47 @@ -import { gql } from "../gql.ts"; +import { gql } from "../../../utils/graphql.ts"; +import { Image, Money } from "../types.ts"; -export const cartFragment = gql` -fragment CartFragment on Cart { +export interface Item { + id: string; + quantity: number; + merchandise: { + id: string; + title: string; + product: { + title: string; + }; + image: Image; + price: Money; + }; + cost: { + totalAmount: Money; + subtotalAmount: Money; + amountPerQuantity: Money; + compareAtAmountPerQuantity: Money; + }; +} + +export interface Fragment { + id: string; + lines?: { + nodes: Item[]; + }; + checkoutUrl?: string; + cost?: { + subtotalAmount: Money; + totalAmount: Money; + checkoutChargeAmount: Money; + }; + discountCodes?: { + code: string; + applicable: boolean; + }[]; + discountAllocations?: { + discountedAmount: Money; + }; +} + +export const fragment = gql`on Cart { id checkoutUrl totalQuantity diff --git a/shopify/utils/fragments/product.ts b/shopify/utils/fragments/product.ts index a4363ad07..24ba32e2f 100644 --- a/shopify/utils/fragments/product.ts +++ b/shopify/utils/fragments/product.ts @@ -1,8 +1,37 @@ -import { gql } from "../gql.ts"; -import { productVariantFragment } from "./productVarian.ts"; +import { gql } from "../../../utils/graphql.ts"; +import { Image, Media, Option, PriceRange, SEO } from "../types.ts"; +import { + Fragment as Variant, + fragment as ProductVariantFragment, +} from "./productVariant.ts"; -export const productFragment = gql` -fragment ProductFragment on Product { +export interface Fragment { + availableForSale: boolean; + createdAt: string; + description: string; + descriptionHtml: string; + featuredImage: Image; + handle: string; + id: string; + images: { nodes: Image[] }; + isGiftCard: boolean; + media: Media; + onlineStoreUrl: null; + options: Option[]; + priceRange: PriceRange; + productType: string; + publishedAt: string; + requiresSellingPlan: boolean; + seo: SEO; + tags: string[]; + title: string; + totalInventory: number; + updatedAt: string; + variants: { nodes: Variant[] }; + vendor: string; +} + +export const fragment = gql`on Product { availableForSale createdAt description @@ -58,9 +87,9 @@ fragment ProductFragment on Product { updatedAt variants(first: 10) { nodes { - ...ProductVariantFragment + ...${ProductVariantFragment} } } vendor } -` + productVariantFragment; +`; diff --git a/shopify/utils/fragments/productVarian.ts b/shopify/utils/fragments/productVarian.ts deleted file mode 100644 index b8dcf0bc7..000000000 --- a/shopify/utils/fragments/productVarian.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { gql } from "../gql.ts"; - -export const productVariantFragment = gql` -fragment ProductVariantFragment on ProductVariant { - availableForSale - barcode - compareAtPrice { - amount - currencyCode - } - currentlyNotInStock - id - image { - altText - url - } - price { - amount - currencyCode - } - quantityAvailable - requiresShipping - selectedOptions { - name - value - } - sku - title - unitPrice { - amount - currencyCode - } - unitPriceMeasurement { - measuredType - quantityValue - referenceUnit - quantityUnit - } - weight - weightUnit -} -`; diff --git a/shopify/utils/fragments/productVariant.ts b/shopify/utils/fragments/productVariant.ts new file mode 100644 index 000000000..c9eab64cc --- /dev/null +++ b/shopify/utils/fragments/productVariant.ts @@ -0,0 +1,66 @@ +import { gql } from "../../../utils/graphql.ts"; +import { + Image, + Price, + SelectedOption, + UnitPriceMeasurement, +} from "../types.ts"; + +export interface Fragment { + availableForSale: boolean; + barcode: string; + compareAtPrice: Price | null; + currentlyNotInStock: boolean; + id: string; + image: Image; + price: Price; + quantityAvailable: number; + requiresShipping: boolean; + selectedOptions: SelectedOption[]; + sku: string; + title: string; + unitPrice: null; + unitPriceMeasurement: UnitPriceMeasurement; + weight: number; + weightUnit: string; +} + +export const fragment = gql`on ProductVariant { + availableForSale + barcode + compareAtPrice { + amount + currencyCode + } + currentlyNotInStock + id + image { + altText + url + } + price { + amount + currencyCode + } + quantityAvailable + requiresShipping + selectedOptions { + name + value + } + sku + title + unitPrice { + amount + currencyCode + } + unitPriceMeasurement { + measuredType + quantityValue + referenceUnit + quantityUnit + } + weight + weightUnit +} +`; diff --git a/shopify/utils/gql.ts b/shopify/utils/gql.ts deleted file mode 100644 index ea9b7abac..000000000 --- a/shopify/utils/gql.ts +++ /dev/null @@ -1 +0,0 @@ -export const gql = (x: TemplateStringsArray) => x.toString().trim(); diff --git a/shopify/utils/queries/addItem.ts b/shopify/utils/queries/addItem.ts new file mode 100644 index 000000000..d4e34f3a8 --- /dev/null +++ b/shopify/utils/queries/addItem.ts @@ -0,0 +1,29 @@ +import { gql } from "../../../utils/graphql.ts"; +import { + Fragment as CartFragment, + fragment as cartFragment, +} from "../fragments/cart.ts"; + +export const query = gql` +mutation add($cartId: ID!, $lines: [CartLineInput!]!) { + payload: cartLinesAdd(cartId: $cartId, lines: $lines) { + cart { ...${cartFragment} } + } +} +`; + +export interface Variables { + cartId: string; + lines: { + merchandiseId: string; + attributes?: Array<{ key: string; value: string }>; + quantity?: number; + sellingPlanId?: string; + }; +} + +export interface Data { + payload: { + cart: CartFragment; + }; +} diff --git a/shopify/utils/queries/cart.ts b/shopify/utils/queries/cart.ts new file mode 100644 index 000000000..79fab8dc4 --- /dev/null +++ b/shopify/utils/queries/cart.ts @@ -0,0 +1,14 @@ +import { gql } from "../../../utils/graphql.ts"; +import { Fragment, fragment } from "../fragments/cart.ts"; + +export const query = gql` +query($id: ID!) { cart(id: $id) { ...${fragment} } } +`; + +export interface Variables { + id: string; +} + +export interface Data { + cart: Fragment; +} diff --git a/shopify/utils/queries/createCart.ts b/shopify/utils/queries/createCart.ts new file mode 100644 index 000000000..5ac79d2eb --- /dev/null +++ b/shopify/utils/queries/createCart.ts @@ -0,0 +1,17 @@ +import { gql } from "../../../utils/graphql.ts"; + +export const query = gql` +mutation createCart { + payload: cartCreate { + cart { + id + } + } +} +`; + +export type Variables = never; + +export interface Data { + payload: { cart: { id: string } }; +} diff --git a/shopify/utils/queries/product.ts b/shopify/utils/queries/product.ts new file mode 100644 index 000000000..f44928dce --- /dev/null +++ b/shopify/utils/queries/product.ts @@ -0,0 +1,16 @@ +import { gql } from "../../../utils/graphql.ts"; +import { Fragment as Product, fragment } from "../fragments/product.ts"; + +export const query = gql` +query GetProduct($handle: String) { + product(handle: $handle) { ...${fragment} } +} +`; + +export interface Variables { + handle: string; +} + +export interface Data { + product: Product; +} diff --git a/shopify/utils/queries/products.ts b/shopify/utils/queries/products.ts new file mode 100644 index 000000000..a39762d08 --- /dev/null +++ b/shopify/utils/queries/products.ts @@ -0,0 +1,27 @@ +import { gql } from "../../../utils/graphql.ts"; +import { Fragment as Product, fragment } from "../fragments/product.ts"; + +export const query = gql` +query GetProducts($first: Int, $after: String, $query: String) { + products(first: $first, after: $after, query: $query) { + pageInfo { + hasNextPage + } + nodes { + ...${fragment} + } + } +} +`; + +export interface Variables { + first: number; + query: string; +} + +export interface Data { + products: { + pageInfo: { hasNextPage: boolean }; + nodes: Product[]; + }; +} diff --git a/shopify/utils/queries/updateCart.ts b/shopify/utils/queries/updateCart.ts new file mode 100644 index 000000000..a4f19cb33 --- /dev/null +++ b/shopify/utils/queries/updateCart.ts @@ -0,0 +1,24 @@ +import { gql } from "../../../utils/graphql.ts"; +import { + Fragment as CartFragment, + fragment as cartFragment, +} from "../fragments/cart.ts"; + +export const query = gql` +mutation update($cartId: ID!, $lines: [CartLineUpdateInput!]!) { + payload: cartLinesUpdate(cartId: $cartId, lines: $lines) { + cart { ...${cartFragment} } + } +} +`; + +export interface Variables { + cartId: string; + lines: Array<{ id: string; quantity?: number }>; +} + +export interface Data { + payload: { + cart: CartFragment; + }; +} diff --git a/shopify/utils/queries/updateCoupon.ts b/shopify/utils/queries/updateCoupon.ts new file mode 100644 index 000000000..a04973c88 --- /dev/null +++ b/shopify/utils/queries/updateCoupon.ts @@ -0,0 +1,26 @@ +import { gql } from "../../../utils/graphql.ts"; +import { + Fragment as CartFragment, + fragment as cartFragment, +} from "../fragments/cart.ts"; + +export const query = gql` +mutation addCoupon($cartId: ID!, $discountCodes: [String!]!) { + payload: cartDiscountCodesUpdate(cartId: $cartId, discountCodes: $discountCodes) { + cart { ...${cartFragment} } + userErrors { + field + message + } + } +} +`; + +export interface Variables { + cartId: string; + discountCodes: string[]; +} + +export interface Data { + payload: { cart: CartFragment }; +} diff --git a/shopify/utils/transform.ts b/shopify/utils/transform.ts index 647e06a2f..6e365ac6e 100644 --- a/shopify/utils/transform.ts +++ b/shopify/utils/transform.ts @@ -6,11 +6,9 @@ import type { PropertyValue, UnitPriceSpecification, } from "../../commerce/types.ts"; -import { - Product as ProductShopify, - SelectedOption as SelectedOptionShopify, - Variant as SkuShopify, -} from "./types.ts"; +import { SelectedOption as SelectedOptionShopify } from "./types.ts"; +import { Fragment as ProductShopify } from "./fragments/product.ts"; +import { Fragment as SkuShopify } from "./fragments/productVariant.ts"; const DEFAULT_IMAGE = { altText: "image", diff --git a/shopify/utils/types.ts b/shopify/utils/types.ts index 89c1b5c6f..ce1c653df 100644 --- a/shopify/utils/types.ts +++ b/shopify/utils/types.ts @@ -140,80 +140,6 @@ export interface Image { altText: string; } -export interface Item{ - id: string; - quantity: number; - merchandise: { - id: string; - title: string; - product: { - title: string; - }; - image: Image; - price: Money; - }; - cost: { - totalAmount: Money; - subtotalAmount: Money; - amountPerQuantity: Money; - compareAtAmountPerQuantity: Money; - }; -} - -export interface CartData { - id: string; - lines?: { - nodes: Item[]; - }; - checkoutUrl?: string; - cost?: { - subtotalAmount: Money; - totalAmount: Money; - checkoutChargeAmount: Money; - }; - discountCodes?: { - code: string; - applicable: boolean; - }[]; - discountAllocations?: { - discountedAmount: Money; - }; -} - -export interface Cart { - cart: CartData; -} - -export interface Product { - availableForSale: boolean; - createdAt: string; - description: string; - descriptionHtml: string; - featuredImage: Image; - handle: string; - id: string; - images: Images; - isGiftCard: boolean; - media: Media; - onlineStoreUrl: null; - options: Option[]; - priceRange: PriceRange; - productType: string; - publishedAt: string; - requiresSellingPlan: boolean; - seo: SEO; - tags: string[]; - title: string; - totalInventory: number; - updatedAt: string; - variants: Variants; - vendor: string; -} - -export interface Images { - nodes: Image[]; -} - export interface Media { nodes: Media[]; } @@ -244,29 +170,6 @@ export interface SEO { description: string; } -export interface Variants { - nodes: Variant[]; -} - -export interface Variant { - availableForSale: boolean; - barcode: string; - compareAtPrice: Price | null; - currentlyNotInStock: boolean; - id: string; - image: Image; - price: Price; - quantityAvailable: number; - requiresShipping: boolean; - selectedOptions: SelectedOption[]; - sku: string; - title: string; - unitPrice: null; - unitPriceMeasurement: UnitPriceMeasurement; - weight: number; - weightUnit: string; -} - export interface SelectedOption { name: string; value: string; diff --git a/utils/graphql.ts b/utils/graphql.ts new file mode 100644 index 000000000..30b367d08 --- /dev/null +++ b/utils/graphql.ts @@ -0,0 +1,46 @@ +// deno-lint-ignore-file no-explicit-any +import { createHttpClient, HttpClientOptions } from "./http.ts"; + +interface GraphqlClientOptions extends Omit { + endpoint: string; +} + +interface GraphQLResponse { + data: D; + errors: unknown[]; +} + +type GraphQLAPI = Record; + body: { + query: string; + variables?: Record; + }; +}>; + +export const gql = (query: TemplateStringsArray, ...fragments: string[]) => + query.reduce((a, c, i) => `${a}${fragments[i - 1]}${c}`); + +export const createGraphqlClient = ( + { endpoint, ...rest }: GraphqlClientOptions, +) => { + const url = new URL(endpoint); + const key = `POST ${url.pathname}`; + const http = createHttpClient({ ...rest, base: url.origin }); + + return { + query: async ( + { query = "", variables }: { query: string; variables?: V }, + ): Promise => { + const { data, errors } = await http[key as any]({}, { + body: { query, variables: variables as any }, + }).then((res) => res.json()); + + if (Array.isArray(errors) && errors.length > 0) { + throw errors; + } + + return data as D; + }, + }; +};