Skip to content

Commit

Permalink
refactor: make platform more agnostic by defining standard response t…
Browse files Browse the repository at this point in the history
…ypes
  • Loading branch information
bmstefanski committed Feb 17, 2024
1 parent c2c0229 commit cabca3a
Show file tree
Hide file tree
Showing 11 changed files with 25,397 additions and 25,911 deletions.
28 changes: 2 additions & 26 deletions apps/web/app/api/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,9 @@ export async function POST(req: Request) {

if (metadata.action === "UPDATE" || metadata.action === "CREATE") {
const originalProduct = await storefrontClient.getProduct(product.id)
const normalizedProduct = normalizeProduct(originalProduct.data?.product)

if (normalizedProduct) {
await index.updateDocuments([normalizedProduct], {
if (originalProduct) {
await index.updateDocuments([originalProduct], {
primaryKey: "id",
})
}
Expand All @@ -43,29 +42,6 @@ export async function POST(req: Request) {
return Response.json({ status: "ok" })
}

// TODO: provide agnostic type
function normalizeProduct(product: any | undefined | null) {
if (!product) return product

return {
...product,
id: normalizeId(product.id),
title: product.title,
descriptionHtml: product.descriptionHtml,
priceRange: product.priceRange,
featuredImage: product.featuredImage,
seo: product.seo,
updatedAt: product.updatedAt,
handle: product.handle,
description: product.description,
options: product.options,
tags: product.tags,
images: product?.images?.edges?.map((image) => image?.node),
variants: product?.variants?.edges?.map((variant) => variant?.node),
collections: product?.collections?.nodes?.map((collection) => collection),
}
}

function normalizeId(id: string) {
const shopifyIdPrefix = "gid://shopify/Product/"
return id.replace(shopifyIdPrefix, "")
Expand Down
4 changes: 1 addition & 3 deletions apps/web/components/ui/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,7 @@ const getCachedMenuItems = unstable_cache(async () => getMenuItems(), ["menu"],

async function getMenuItems() {
const menu = await storefrontClient.getMenu()
const items = menu.data?.menu?.items

if (!items || menu.errors?.graphQLErrors) return []
const items = menu.items

return items?.map((singleItem) => ({
...singleItem,
Expand Down
36 changes: 17 additions & 19 deletions apps/web/views/Product/ProductView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@
* This code was generated by v0 by Vercel.
* @see https://v0.dev/t/J1swfBP3ZeH
*/
import { storefrontClient } from "client/storefrontClient"
import { Button } from "components/ui/Button"
import { Label } from "components/ui/Label"
import { RadioGroup, RadioGroupItem } from "components/ui/RadioGroup"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "components/ui/Select"
import { notFound } from "next/navigation"
import { storefrontClient } from "client/storefrontClient"

export async function ProductView({ slug }) {
const queriedProducts = await storefrontClient.getProductsByHandle(slug)
const product = await storefrontClient.getProductByHandle(slug)

if (queriedProducts.data?.products.edges.length === 0) {
if (!product) {
return notFound()
}

const product = queriedProducts.data?.products.edges[0].node

return (
<div className="mx-auto grid max-w-6xl items-start gap-6 px-4 py-6 md:grid-cols-2 lg:gap-12">
<div className="grid items-start gap-3 md:grid-cols-5">
Expand All @@ -29,20 +27,20 @@ export async function ProductView({ slug }) {
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-0.5">
<StarIcon className="fill-primary h-5 w-5" />
<StarIcon className="fill-primary h-5 w-5" />
<StarIcon className="fill-primary h-5 w-5" />
<StarIcon className="fill-muted stroke-muted-foreground h-5 w-5" />
<StarIcon className="fill-muted stroke-muted-foreground h-5 w-5" />
<StarIcon className="fill-primary size-5" />
<StarIcon className="fill-primary size-5" />
<StarIcon className="fill-primary size-5" />
<StarIcon className="fill-muted stroke-muted-foreground size-5" />
<StarIcon className="fill-muted stroke-muted-foreground size-5" />
</div>
</div>
</div>
<div className="ml-auto text-4xl font-bold">$99</div>
</div>
<div className="hidden flex-col items-start gap-3 md:flex">
{product?.images.edges.map((image, index) => (
<button key={image.node.url} className="overflow-hidden rounded-lg border transition-colors hover:border-gray-900 dark:hover:border-gray-50">
<img alt={image.node.altText || ""} className="aspect-[5/6] object-cover" height={120} src={image.node.url} width={100} />
{product?.images.map((image, index) => (
<button key={image.url} className="overflow-hidden rounded-lg border transition-colors hover:border-gray-900 dark:hover:border-gray-50">
<img alt={image.altText || ""} className="aspect-[5/6] object-cover" height={120} src={image.url} width={100} />
<span className="sr-only">View Image {index + 1}</span>
</button>
))}
Expand All @@ -66,15 +64,15 @@ export async function ProductView({ slug }) {
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-0.5">
<StarIcon className="fill-primary h-5 w-5" />
<StarIcon className="fill-primary h-5 w-5" />
<StarIcon className="fill-primary h-5 w-5" />
<StarIcon className="fill-muted stroke-muted-foreground h-5 w-5" />
<StarIcon className="fill-muted stroke-muted-foreground h-5 w-5" />
<StarIcon className="fill-primary size-5" />
<StarIcon className="fill-primary size-5" />
<StarIcon className="fill-primary size-5" />
<StarIcon className="fill-muted stroke-muted-foreground size-5" />
<StarIcon className="fill-muted stroke-muted-foreground size-5" />
</div>
</div>
</div>
<div className="ml-auto text-4xl font-bold">{product?.variants?.edges?.[0]?.node?.price.amount}</div>
<div className="ml-auto text-4xl font-bold">{product?.variants?.[0]?.price.amount}</div>
</div>
<form className="grid gap-4 md:gap-10">
<div className="grid gap-2">
Expand Down
30 changes: 23 additions & 7 deletions packages/core/platform/shopify/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { createStorefrontApiClient, StorefrontApiClient } from "@shopify/storefr

import { createProductFeedMutation, fullSyncProductFeedMutation } from "./mutations/product-feed.admin"
import { subscribeWebhookMutation } from "./mutations/webhook.admin"
import { normalizeProduct } from "./normalize"
import { getMenuQuery } from "./queries/menu.storefront"
import { getLatestProductFeedQuery } from "./queries/product-feed.admin"
import { getProductQuery, getProductsByHandleQuery } from "./queries/product.storefront"

import type { LatestProductFeedsQuery, ProductFeedCreateMutation, ProductFullSyncMutation, WebhookSubscriptionCreateMutation } from "./types/admin/admin.generated"
import type { WebhookSubscriptionTopic } from "./types/admin/admin.types"
import type { MenuQuery, ProductsByHandleQuery, SingleProductQuery } from "./types/storefront.generated"
import { PlatformMenu, PlatformProduct } from "../types"

interface CreateShopifyClientProps {
storeDomain: string
Expand All @@ -36,24 +38,38 @@ export function createShopifyClient({ storefrontAccessToken, adminAccessToken, s
return {
getMenu: async (handle?: string) => getMenu(client!, handle),
getProduct: async (id: string) => getProduct(client!, id),
getProductsByHandle: async (handle: string) => getProductsByHandle(client!, handle),
getProductByHandle: async (handle: string) => getProductByHandle(client!, handle),
subscribeWebhook: async (topic: `${WebhookSubscriptionTopic}`, callbackUrl: string) => subscribeWebhook(adminClient, topic, callbackUrl),
createProductFeed: async () => createProductFeed(adminClient),
fullSyncProductFeed: async (id: string) => fullSyncProductFeed(adminClient, id),
getLatestProductFeed: async () => getLatestProductFeed(adminClient),
}
}

async function getMenu(client: StorefrontApiClient, handle: string = "main-menu") {
return client.request<MenuQuery>(getMenuQuery, { variables: { handle } })
async function getMenu(client: StorefrontApiClient, handle: string = "main-menu"): Promise<PlatformMenu> {
const response = await client.request<MenuQuery>(getMenuQuery, { variables: { handle } })
const mappedItems = response.data?.menu?.items?.map((item) => ({
title: item.title,
url: item.url,
}))

return {
items: mappedItems || [],
}
}

async function getProduct(client: StorefrontApiClient, id: string) {
return client.request<SingleProductQuery>(getProductQuery, { variables: { id } })
async function getProduct(client: StorefrontApiClient, id: string): Promise<PlatformProduct | null> {
const response = await client.request<SingleProductQuery>(getProductQuery, { variables: { id } })
const product = response.data?.product

return normalizeProduct(product)
}

async function getProductsByHandle(client: StorefrontApiClient, handle: string) {
return client.request<ProductsByHandleQuery>(getProductsByHandleQuery, { variables: { query: `'${handle}'` } })
async function getProductByHandle(client: StorefrontApiClient, handle: string) {
const response = await client.request<ProductsByHandleQuery>(getProductsByHandleQuery, { variables: { query: `'${handle}'` } })
const product = response.data?.products?.edges?.find(Boolean)?.node

return normalizeProduct(product)
}

async function subscribeWebhook(client: AdminApiClient, topic: `${WebhookSubscriptionTopic}`, callbackUrl: string) {
Expand Down
24 changes: 24 additions & 0 deletions packages/core/platform/shopify/normalize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { SingleProductQuery } from "./types/storefront.generated"
import { PlatformProduct } from "../types"

export function normalizeProduct(product: SingleProductQuery["product"]): PlatformProduct | null {
if (!product) return null
const { id, handle, title, description, descriptionHtml, options, priceRange, variants, featuredImage, images, tags, updatedAt, collections, seo } = product

return {
id,
handle,
title,
description,
descriptionHtml,
options,
priceRange,
tags,
updatedAt,
featuredImage,
seo,
variants: variants?.edges?.map(({ node }) => node) || [],
images: images?.edges?.map(({ node }) => node) || [],
collections: collections?.nodes || [],
}
}
10 changes: 9 additions & 1 deletion packages/core/platform/shopify/queries/product.storefront.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ export const getProductsByHandleQuery = `#graphql
products(first: 1, query: $query) {
edges {
node {
...singleProduct
...singleProduct
collections(first: 15) {
nodes {
handle
title
description
updatedAt
}
}
}
}
}
Expand Down
64 changes: 41 additions & 23 deletions packages/core/platform/shopify/types/admin/admin.generated.d.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,62 @@
/* eslint-disable eslint-comments/disable-enable-pair */
/* eslint-disable eslint-comments/no-unlimited-disable */
/* eslint-disable */
import * as AdminTypes from './admin.types.d.ts';
import * as AdminTypes from "./admin.types.d.ts"

export type ProductFeedCreateMutationVariables = AdminTypes.Exact<{ [key: string]: never; }>;
export type ProductFeedCreateMutationVariables = AdminTypes.Exact<{ [key: string]: never }>


export type ProductFeedCreateMutation = { productFeedCreate?: AdminTypes.Maybe<{ productFeed?: AdminTypes.Maybe<Pick<AdminTypes.ProductFeed, 'status' | 'id'>>, userErrors: Array<Pick<AdminTypes.ProductFeedCreateUserError, 'field' | 'message'>> }> };
export type ProductFeedCreateMutation = {
productFeedCreate?: AdminTypes.Maybe<{
productFeed?: AdminTypes.Maybe<Pick<AdminTypes.ProductFeed, "status" | "id">>
userErrors: Array<Pick<AdminTypes.ProductFeedCreateUserError, "field" | "message">>
}>
}

export type ProductFullSyncMutationVariables = AdminTypes.Exact<{
id: AdminTypes.Scalars['ID']['input'];
}>;
id: AdminTypes.Scalars["ID"]["input"]
}>


export type ProductFullSyncMutation = { productFullSync?: AdminTypes.Maybe<{ userErrors: Array<Pick<AdminTypes.ProductFullSyncUserError, 'field' | 'message'>> }> };
export type ProductFullSyncMutation = { productFullSync?: AdminTypes.Maybe<{ userErrors: Array<Pick<AdminTypes.ProductFullSyncUserError, "field" | "message">> }> }

export type WebhookSubscriptionCreateMutationVariables = AdminTypes.Exact<{
topic: AdminTypes.WebhookSubscriptionTopic;
webhookSubscription: AdminTypes.WebhookSubscriptionInput;
}>;


export type WebhookSubscriptionCreateMutation = { webhookSubscriptionCreate?: AdminTypes.Maybe<{ userErrors: Array<Pick<AdminTypes.UserError, 'field' | 'message'>>, webhookSubscription?: AdminTypes.Maybe<Pick<AdminTypes.WebhookSubscription, 'id'>> }> };

export type LatestProductFeedsQueryVariables = AdminTypes.Exact<{ [key: string]: never; }>;
topic: AdminTypes.WebhookSubscriptionTopic
webhookSubscription: AdminTypes.WebhookSubscriptionInput
}>

export type WebhookSubscriptionCreateMutation = {
webhookSubscriptionCreate?: AdminTypes.Maybe<{
userErrors: Array<Pick<AdminTypes.UserError, "field" | "message">>
webhookSubscription?: AdminTypes.Maybe<Pick<AdminTypes.WebhookSubscription, "id">>
}>
}

export type LatestProductFeedsQueryVariables = AdminTypes.Exact<{ [key: string]: never }>

export type LatestProductFeedsQuery = { productFeeds: { nodes: Array<Pick<AdminTypes.ProductFeed, 'id' | 'country' | 'status'>> } };
export type LatestProductFeedsQuery = { productFeeds: { nodes: Array<Pick<AdminTypes.ProductFeed, "id" | "country" | "status">> } }

interface GeneratedQueryTypes {
"#graphql\n query LatestProductFeeds {\n productFeeds(reverse: true, first: 1) {\n nodes {\n id\n country\n status\n }\n }\n }\n": {return: LatestProductFeedsQuery, variables: LatestProductFeedsQueryVariables},
"#graphql\n query LatestProductFeeds {\n productFeeds(reverse: true, first: 1) {\n nodes {\n id\n country\n status\n }\n }\n }\n": {
return: LatestProductFeedsQuery
variables: LatestProductFeedsQueryVariables
}
}

interface GeneratedMutationTypes {
"#graphql\n mutation ProductFeedCreate {\n productFeedCreate {\n productFeed {\n status\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n": {return: ProductFeedCreateMutation, variables: ProductFeedCreateMutationVariables},
"#graphql\n mutation productFullSync($id: ID!) {\n productFullSync(id: $id) {\n userErrors {\n field\n message\n }\n }\n }\n": {return: ProductFullSyncMutation, variables: ProductFullSyncMutationVariables},
"#graphql\n mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {\n webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {\n userErrors {\n field\n message\n }\n webhookSubscription {\n id\n }\n }\n }\n": {return: WebhookSubscriptionCreateMutation, variables: WebhookSubscriptionCreateMutationVariables},
"#graphql\n mutation ProductFeedCreate {\n productFeedCreate {\n productFeed {\n status\n id\n }\n userErrors {\n field\n message\n }\n }\n }\n": {
return: ProductFeedCreateMutation
variables: ProductFeedCreateMutationVariables
}
"#graphql\n mutation productFullSync($id: ID!) {\n productFullSync(id: $id) {\n userErrors {\n field\n message\n }\n }\n }\n": {
return: ProductFullSyncMutation
variables: ProductFullSyncMutationVariables
}
"#graphql\n mutation webhookSubscriptionCreate($topic: WebhookSubscriptionTopic!, $webhookSubscription: WebhookSubscriptionInput!) {\n webhookSubscriptionCreate(topic: $topic, webhookSubscription: $webhookSubscription) {\n userErrors {\n field\n message\n }\n webhookSubscription {\n id\n }\n }\n }\n": {
return: WebhookSubscriptionCreateMutation
variables: WebhookSubscriptionCreateMutationVariables
}
}
declare module '@shopify/admin-api-client' {
type InputMaybe<T> = AdminTypes.InputMaybe<T>;
declare module "@shopify/admin-api-client" {
type InputMaybe<T> = AdminTypes.InputMaybe<T>
interface AdminQueries extends GeneratedQueryTypes {}
interface AdminMutations extends GeneratedMutationTypes {}
}
Loading

0 comments on commit cabca3a

Please sign in to comment.