Skip to content

Commit

Permalink
chore: sync scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
ddaoxuan committed Nov 6, 2024
1 parent fcd7629 commit 1c097fd
Show file tree
Hide file tree
Showing 26 changed files with 1,052 additions and 299 deletions.
26 changes: 9 additions & 17 deletions starters/shopify-algolia/app/api/feed/sync/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { PlatformProduct } from "lib/shopify/types"
import { env } from "env.mjs"
import { compareHmac } from "utils/compare-hmac"
import { enrichProduct } from "utils/enrich-product"
import { ProductEnrichmentBuilder } from "utils/enrich-product"
import { deleteCategories, deleteProducts, updateCategories, updateProducts } from "lib/algolia"
import { getCollection, getHierarchicalCollections, getProduct } from "lib/shopify"
import { makeShopifyId } from "lib/shopify/utils"
import { HIERARCHICAL_SEPARATOR } from "constants/index"
import { isOptIn } from "utils/opt-in"
import { getAllProductReviews } from "lib/reviews"

type SupportedTopic = "products/update" | "products/delete" | "products/create" | "collections/update" | "collections/delete" | "collections/create"

Expand Down Expand Up @@ -55,7 +58,7 @@ async function handleCollectionTopics(topic: SupportedTopic, { id }: Record<stri
return new Response(JSON.stringify({ message: "Collection not found" }), { status: 404, headers: { "Content-Type": "application/json" } })
}

await updateCategories([{ ...collection, id: `${id}` }])
await updateCategories([collection])

break

Expand All @@ -76,15 +79,16 @@ async function handleProductTopics(topic: SupportedTopic, { id }: Record<string,
case "products/create":
const product = await getProduct(makeShopifyId(`${id}`, "Product"))
const items = env.SHOPIFY_HIERARCHICAL_NAV_HANDLE ? (await getHierarchicalCollections(env.SHOPIFY_HIERARCHICAL_NAV_HANDLE)).items : []
const allReviews = isOptIn("reviews") ? await getAllProductReviews() : []

if (!product) {
console.error(`Product ${id} not found`)
return new Response(JSON.stringify({ message: "Product not found" }), { status: 404, headers: { "Content-Type": "application/json" } })
}

const enrichedProduct = await enrichProduct(product, items)
const enrichedProduct = (await new ProductEnrichmentBuilder(product).withAltTags()).withHierarchicalCategories(items, HIERARCHICAL_SEPARATOR).withReviews(allReviews).build()

await updateProducts([normalizeProduct(enrichedProduct, id)])
await updateProducts([enrichedProduct])

break
case "products/delete":
Expand All @@ -97,15 +101,3 @@ async function handleProductTopics(topic: SupportedTopic, { id }: Record<string,

return new Response(JSON.stringify({ message: "Success" }), { status: 200, headers: { "Content-Type": "application/json" } })
}

/* Extract into utils */
function normalizeProduct(product: PlatformProduct, originalId: string): PlatformProduct {
return {
...product,
id: originalId,
}
}

function makeShopifyId(id: string, type: "Product" | "Collection") {
return id.startsWith("gid://shopify/") ? id : `gid://shopify/${type}/${id}`
}
2 changes: 1 addition & 1 deletion starters/shopify-algolia/components/demo-mode-alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function DemoModeAlert() {
<p>Filtering, searching, and adding to cart is disabled.</p>
<div className="mt-2">
To enable,{" "}
<a className="underline" target="_blank" href="https://docs.commerce.blazity.com/setup#manual">
<a className="underline" target="_blank" rel="noreferrer" href="https://docs.commerce.blazity.com/setup#manual">
setup environment variables
</a>
</div>
Expand Down
164 changes: 68 additions & 96 deletions starters/shopify-algolia/lib/algolia/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
type BatchProps,
algoliasearch,
type BrowseProps,
type DeleteObjectsOptions,
Expand All @@ -7,139 +8,110 @@ import {
type SearchMethodParams,
type SearchResponse,
type SearchSingleIndexProps,
} from "algoliasearch";
} from "algoliasearch"

import { env } from "env.mjs";
import { env } from "env.mjs"

import { FilterBuilder } from "./filter-builder";
import { FilterBuilder } from "./filter-builder"

const algoliaClient = (args: { applicationId: string; apiKey: string }) => {
return algoliasearch(args.applicationId, args.apiKey);
};
return algoliasearch(args.applicationId, args.apiKey)
}

export const algolia = (args: { applicationId: string; apiKey: string }) => {
const client = algoliaClient(args);
const recommendationClient = client.initRecommend();
const client = algoliaClient(args)
const recommendationClient = client.initRecommend()

return {
search: async <T extends Record<string, any>>(
args: SearchSingleIndexProps
) => search<T>(args, client),
getAllResults: async <T extends Record<string, any>>(args: BrowseProps) =>
getAllResults<T>(client, args),
update: async (args: PartialUpdateObjectsOptions) =>
updateObjects(args, client),
search: async <T extends Record<string, any>>(args: SearchSingleIndexProps) => search<T>(args, client),
getAllResults: async <T extends Record<string, any>>(args: BrowseProps) => getAllResults<T>(client, args),
update: async (args: PartialUpdateObjectsOptions) => updateObjects(args, client),
batchUpdate: async (args: BatchProps) => batchUpdate(args, client),
delete: async (args: DeleteObjectsOptions) => deleteObjects(args, client),
create: async (args: PartialUpdateObjectsOptions) =>
createObjects(args, client),
multiSearch: async <T extends Record<string, any>>(
args: SearchMethodParams
) => multiSearch<T>(args, client),
getRecommendations: async (args: GetRecommendationsParams) =>
getRecommendations(recommendationClient, args),
create: async (args: PartialUpdateObjectsOptions) => createObjects(args, client),
multiSearch: async <T extends Record<string, any>>(args: SearchMethodParams) => multiSearch<T>(args, client),
getRecommendations: async (args: GetRecommendationsParams) => getRecommendations(recommendationClient, args),
filterBuilder: () => new FilterBuilder(),
mapIndexToSort,
};
};
}
}

const search = async <T extends Record<string, any>>(
args: SearchSingleIndexProps,
client: ReturnType<typeof algoliaClient>
) => {
return client.searchSingleIndex<T>(args);
};
const search = async <T extends Record<string, any>>(args: SearchSingleIndexProps, client: ReturnType<typeof algoliaClient>) => {
return client.searchSingleIndex<T>(args)
}

// agregator as temp fix for now
const getAllResults = async <T extends Record<string, any>>(
client: ReturnType<typeof algoliaClient>,
args: BrowseProps
) => {
const allHits: T[] = [];
let totalPages: number;
let currentPage = 0;
const getAllResults = async <T extends Record<string, any>>(client: ReturnType<typeof algoliaClient>, args: BrowseProps) => {
const allHits: T[] = []
let totalPages: number
let currentPage = 0

do {
const { hits, nbPages } = await client.browseObjects<T>({
const { hits, nbPages } = await client.browse<T>({
...args,
browseParams: {
...args.browseParams,
hitsPerPage: 1000,
page: currentPage,
},
aggregator: () => null,
});
allHits.push(...hits);
totalPages = nbPages || 0;
currentPage++;
} while (currentPage < totalPages);

return { hits: allHits, totalPages };
};

const updateObjects = async (
args: PartialUpdateObjectsOptions,
client: ReturnType<typeof algoliaClient>
) => {
return client.partialUpdateObjects(args);
};

const deleteObjects = async (
args: DeleteObjectsOptions,
client: ReturnType<typeof algoliaClient>
) => {
return client.deleteObjects(args);
};

const createObjects = async (
args: PartialUpdateObjectsOptions,
client: ReturnType<typeof algoliaClient>
) => {
})
allHits.push(...hits)
totalPages = nbPages || 0
currentPage++
} while (currentPage < totalPages)

return { hits: allHits, totalPages }
}

const batchUpdate = async (args: BatchProps, client: ReturnType<typeof algoliaClient>) => {
return client.batch(args)
}

const updateObjects = async (args: PartialUpdateObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
return client.partialUpdateObjects(args)
}

const deleteObjects = async (args: DeleteObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
return client.deleteObjects(args)
}

const createObjects = async (args: PartialUpdateObjectsOptions, client: ReturnType<typeof algoliaClient>) => {
return client.partialUpdateObjects({
...args,
createIfNotExists: true,
});
};

const multiSearch = async <T extends Record<string, any>>(
args: SearchMethodParams,
client: ReturnType<typeof algoliaClient>
) => {
return client.search<T>(args) as Promise<{ results: SearchResponse<T>[] }>;
};

const getRecommendations = async (
client: ReturnType<ReturnType<typeof algoliaClient>["initRecommend"]>,
args: GetRecommendationsParams
) => {
return client.getRecommendations(args);
};

export type SortType =
| "minPrice:desc"
| "minPrice:asc"
| "avgRating:desc"
| "updatedAtTimestamp:asc"
| "updatedAtTimestamp:desc";
})
}

const multiSearch = async <T extends Record<string, any>>(args: SearchMethodParams, client: ReturnType<typeof algoliaClient>) => {
return client.search<T>(args) as Promise<{ results: SearchResponse<T>[] }>
}

const getRecommendations = async (client: ReturnType<ReturnType<typeof algoliaClient>["initRecommend"]>, args: GetRecommendationsParams) => {
return client.getRecommendations(args)
}

export type SortType = "minPrice:desc" | "minPrice:asc" | "avgRating:desc" | "updatedAtTimestamp:asc" | "updatedAtTimestamp:desc"

const mapIndexToSort = (index: string, sortOption: SortType) => {
switch (sortOption) {
case "minPrice:desc":
return `${index}_price_desc`;
return `${index}_price_desc`
case "minPrice:asc":
return `${index}_price_asc`;
return `${index}_price_asc`
case "avgRating:desc":
return `${index}_rating_desc`;
return `${index}_rating_desc`
case "updatedAtTimestamp:asc":
return `${index}_updated_asc`;
return `${index}_updated_asc`
case "updatedAtTimestamp:desc":
return `${index}_updated_desc`;
return `${index}_updated_desc`

default:
return index;
return index
}
};
}

export const searchClient: ReturnType<typeof algolia> = algolia({
applicationId: env.ALGOLIA_APP_ID || "",
// Make sure write api key never leaks to the client
apiKey: env.ALGOLIA_WRITE_API_KEY || "",
});
})
52 changes: 46 additions & 6 deletions starters/shopify-algolia/lib/shopify/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { getMenuQuery, type MenuQuery } from "./queries/menu.storefront"
import { getPageQuery, getPagesQuery } from "./queries/page.storefront"
import { getLatestProductFeedQuery } from "./queries/product-feed.admin"
import { getAdminProductQuery, getProductStatusQuery } from "./queries/product.admin"
import { getProductQuery, getProductsByHandleQuery } from "./queries/product.storefront"
import { getProductQuery, getProductsByHandleQuery, getProductsQuery } from "./queries/product.storefront"

import type {
LatestProductFeedsQuery,
Expand Down Expand Up @@ -57,6 +57,7 @@ import {
} from "./types"

import { env } from "env.mjs"
import { cleanShopifyId, makeShopifyId } from "./utils"

interface CreateShopifyClientProps {
storeDomain: string
Expand Down Expand Up @@ -105,6 +106,8 @@ export function createShopifyClient({ storefrontAccessToken, adminAccessToken, s
updateUser: async (accessToken: string, input: Omit<PlatformUserCreateInput, "password">) => updateUser(client!, accessToken, input),
createUserAccessToken: async (input: Pick<PlatformUserCreateInput, "password" | "email">) => createUserAccessToken(client!, input),
getHierarchicalCollections: async (handle: string, depth?: number) => getHierarchicalCollections(client!, handle, depth),
getAllProducts: async () => getAllProducts(client!),
getAllCollections: async () => getAllCollections(client!),
}
}

Expand All @@ -116,7 +119,7 @@ async function getMenu(client: StorefrontApiClient, handle: string = "main-menu"
return { items: mappedItems || [] }
}

async function getHierarchicalCollections(client: StorefrontApiClient, handle: string, depth = 3): Promise<PlatformMenu> {
async function getHierarchicalCollections(client: StorefrontApiClient, handle: string, depth = 3) {
const query = getMenuQuery(depth)
const response = await client.request<MenuQuery>(query, { variables: { handle } })
const mappedItems = response.data?.menu.items.filter((item) => item.resource?.__typename === "Collection")
Expand All @@ -127,7 +130,7 @@ async function getHierarchicalCollections(client: StorefrontApiClient, handle: s
}

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

return normalizeProduct(product)
Expand Down Expand Up @@ -176,7 +179,7 @@ async function getAllPages(client: StorefrontApiClient): Promise<PlatformPage[]
}

async function getProductStatus(client: AdminApiClient, id: string): Promise<PlatformProductStatus | undefined | null> {
const status = await client.request<ProductStatusQuery>(getProductStatusQuery, { variables: { id } })
const status = await client.request<ProductStatusQuery>(getProductStatusQuery, { variables: { id: makeShopifyId(id, "Product") } })

return status.data?.product
}
Expand Down Expand Up @@ -224,7 +227,7 @@ async function getCollection(client: StorefrontApiClient, handle: string): Promi
}

async function getCollectionById(client: StorefrontApiClient, id: string): Promise<PlatformCollection | undefined | null> {
const collection = await client.request<SingleCollectionByIdQuery>(getCollectionByIdQuery, { variables: { id } })
const collection = await client.request<SingleCollectionByIdQuery>(getCollectionByIdQuery, { variables: { id: makeShopifyId(id, "Collection") } })

return normalizeCollection(collection.data?.collection)
}
Expand Down Expand Up @@ -255,7 +258,7 @@ async function updateUser(client: StorefrontApiClient, customerAccessToken: stri

async function getAdminProduct(client: AdminApiClient, id: string) {
const response = await client.request<SingleAdminProductQuery>(getAdminProductQuery, {
variables: { id: id.startsWith("gid://shopify/Product/") ? id : `gid://shopify/Product/${id}` },
variables: { id: makeShopifyId(id, "Product") },
})

if (!response.data?.product) return null
Expand All @@ -266,6 +269,43 @@ async function getAdminProduct(client: AdminApiClient, id: string) {
return normalizeProduct({ ...response.data?.product, variants })
}

async function getAllProducts(client: StorefrontApiClient, limit: number = 250): Promise<PlatformProduct[]> {
const products: PlatformProduct[] = []
let hasNextPage = true
let cursor: string | null = null

while (hasNextPage) {
const response = await client.request(getProductsQuery, {
variables: { numProducts: limit, cursor },
})

const fetchedProducts = response.data?.products?.edges || []
products.push(...fetchedProducts.map((edge) => normalizeProduct(edge.node)))

hasNextPage = response.data?.products?.pageInfo?.hasNextPage || false
cursor = hasNextPage ? response.data?.products?.pageInfo?.endCursor : null
}

return products.map((product) => ({ ...product, id: cleanShopifyId(product.id, "Product") }))
}

async function getAllCollections(client: StorefrontApiClient, limit?: number) {
const collections: PlatformCollection[] = []
let hasNextPage = true
let cursor: string | null = null

while (hasNextPage) {
const response = await client.request(getCollectionsQuery, { variables: { first: limit, after: cursor } })
const fetchedCollections = response.data?.collections?.edges || []
collections.push(...fetchedCollections.map((edge) => normalizeCollection(edge.node)))

hasNextPage = response.data?.collections?.pageInfo?.hasNextPage || false
cursor = hasNextPage ? response?.data?.collections?.pageInfo?.endCursor : null
}

return collections.map((collection) => ({ ...collection, id: cleanShopifyId(collection.id, "Collection") }))
}

export const storefrontClient = createShopifyClient({
storeDomain: env.SHOPIFY_STORE_DOMAIN || "",
storefrontAccessToken: env.SHOPIFY_STOREFRONT_ACCESS_TOKEN || "",
Expand Down
Loading

0 comments on commit 1c097fd

Please sign in to comment.