Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: sync scripts #92

Merged
merged 2 commits into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading