From d734c91af8cd0efefe1eaf45404417267fcc9fe1 Mon Sep 17 00:00:00 2001 From: ddaoxuan Date: Thu, 31 Oct 2024 21:32:40 +0100 Subject: [PATCH] refactor: move reviews lib --- .../app/actions/reviews.actions.ts | 4 +- .../app/api/reviews/sync/route.ts | 4 +- .../shopify-meilisearch/clients/reviews.ts | 15 --- .../shopify-meilisearch/lib/reviews/client.ts | 120 ++++++++++++++++++ .../shopify-meilisearch/lib/reviews/index.ts | 109 +--------------- 5 files changed, 130 insertions(+), 122 deletions(-) delete mode 100644 starters/shopify-meilisearch/clients/reviews.ts create mode 100644 starters/shopify-meilisearch/lib/reviews/client.ts diff --git a/starters/shopify-meilisearch/app/actions/reviews.actions.ts b/starters/shopify-meilisearch/app/actions/reviews.actions.ts index ac9c03e3..ed9168d6 100644 --- a/starters/shopify-meilisearch/app/actions/reviews.actions.ts +++ b/starters/shopify-meilisearch/app/actions/reviews.actions.ts @@ -1,6 +1,6 @@ "use server" -import { reviewsClient } from "clients/reviews" +import { createProductReview } from "lib/reviews" import type { ProductReviewBody } from "lib/reviews/types" import { headers } from "next/headers" @@ -8,7 +8,7 @@ import { headers } from "next/headers" export const submitReview = async (payload: Omit) => { try { const ipAddress = headers().get("x-forwarded-for") || null - await reviewsClient.createProductReview({ ...payload, ip_addr: ipAddress }) + await createProductReview({ ...payload, ip_addr: ipAddress }) } catch (err) { throw new Error(err as string) } diff --git a/starters/shopify-meilisearch/app/api/reviews/sync/route.ts b/starters/shopify-meilisearch/app/api/reviews/sync/route.ts index 34c984f4..fdb6fa86 100644 --- a/starters/shopify-meilisearch/app/api/reviews/sync/route.ts +++ b/starters/shopify-meilisearch/app/api/reviews/sync/route.ts @@ -1,10 +1,10 @@ import { unstable_noStore } from "next/cache" -import { reviewsClient } from "clients/reviews" import { env } from "env.mjs" import { authenticate } from "utils/authenticate-api-route" import { isOptIn, notifyOptIn } from "utils/opt-in" import { isDemoMode } from "utils/demo-utils" import { getAllProducts, getAllReviews, updateProducts, updateReviews } from "lib/meilisearch" +import { getAllProductReviews } from "lib/reviews" export const maxDuration = 60 @@ -30,7 +30,7 @@ export async function GET(req: Request) { } const [allReviews, { results: allProducts }, { reviews }] = await Promise.all([ - reviewsClient.getAllProductReviews(), + getAllProductReviews(), getAllProducts({ fields: ["handle", "title", "avgRating", "totalReviews"], }), diff --git a/starters/shopify-meilisearch/clients/reviews.ts b/starters/shopify-meilisearch/clients/reviews.ts deleted file mode 100644 index a83ce96c..00000000 --- a/starters/shopify-meilisearch/clients/reviews.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createJudgeClient } from "lib/reviews" -import { env } from "env.mjs" -import { isOptIn, notifyOptIn } from "utils/opt-in" - -export const reviewsClient = (() => { - if (!isOptIn("reviews")) { - notifyOptIn({ feature: "reviews", source: "clients/reviews" }) - } - - return createJudgeClient({ - baseUrl: env.JUDGE_BASE_URL!, - apiKey: env.JUDGE_API_TOKEN!, - shopDomain: env.SHOPIFY_STORE_DOMAIN, - }) -})() diff --git a/starters/shopify-meilisearch/lib/reviews/client.ts b/starters/shopify-meilisearch/lib/reviews/client.ts new file mode 100644 index 00000000..95e3e0bc --- /dev/null +++ b/starters/shopify-meilisearch/lib/reviews/client.ts @@ -0,0 +1,120 @@ +import { isOptIn, notifyOptIn } from "utils/opt-in" +import type { GetProductReviewsOpts, GetProductReviewsResponse, JudgeMeWebhookKey, ProductReviewArgs, ProductReviewBody, Review } from "./types" +import { env } from "env.mjs" + +type CreateJudgeClientArgs = { + baseUrl: string + apiKey: string + shopDomain: string +} + +export function createJudgeClient({ baseUrl, apiKey, shopDomain }: CreateJudgeClientArgs) { + const url = new URL(baseUrl) + url.searchParams.set("api_token", apiKey) + url.searchParams.set("shop_domain", shopDomain) + + return { + getProductReviews: async (opts: GetProductReviewsOpts = {}) => getProductReviews(url, opts), + getAllProductReviews: async () => getAllProductReviews(url), + createProductReview: async (body: ProductReviewBody) => createProductReview({ baseUrl: url, body }), + createWebhook: async (key: JudgeMeWebhookKey, subscribeUrl: string) => createWebhook(url, key, subscribeUrl), + } +} + +async function getProductReviews( + baseUrl: URL, + opts: GetProductReviewsOpts = { + per_page: 10, + page: 1, + } +): Promise { + const localParams = new URLSearchParams(baseUrl.searchParams) + Object.entries(opts).forEach(([key, value]) => { + localParams.set(key, value.toString()) + }) + + const url = `${baseUrl.origin}${baseUrl.pathname}/reviews?${baseUrl.searchParams.toString()}&${localParams.toString()}` + + const reviewsCountUrl = `${baseUrl.origin}${baseUrl.pathname}/reviews/count?${baseUrl.searchParams.toString()}` + + const reviewsCount = await fetch(reviewsCountUrl) + const { count } = (await reviewsCount.json()) as { count: number } + + const reviews = (await fetch(url).then((res) => res.json())) as Pick + + return { ...reviews, total: count, totalPages: Math.ceil(count / reviews.per_page) } +} + +async function getAllProductReviews(baseUrl: URL) { + const allReviews: Review[] = [] + + const { reviews, totalPages } = await getProductReviews(baseUrl, { per_page: 100 }) + allReviews.push(...reviews) + + for (let page = 2; page <= totalPages; page++) { + const { reviews } = await getProductReviews(baseUrl, { per_page: 100, page }) + allReviews.push(...reviews) + } + + return allReviews +} + +async function createProductReview({ baseUrl, body }: ProductReviewArgs) { + const url = `${baseUrl.origin}${baseUrl.pathname}/reviews` + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + ...body, + shop_domain: baseUrl.searchParams.get("shop_domain"), + platform: "shopify", // needs to be dynamic later on + }), + }) + + if (!res.ok) { + throw new Error("Failed to create review") + } + + const data = await res.json() + + return data +} + +async function createWebhook(baseUrl: URL, key: JudgeMeWebhookKey, subscribeUrl: string) { + const url = `${baseUrl.origin}${baseUrl.pathname}/webhooks?${baseUrl.searchParams.toString()}` + + if (key !== "review/created" && key !== "review/updated" && key !== "review/created_fail") { + throw new Error("Judge me: Invalid key to create a webhook") + } + + const res = await fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + key, + subscribe_url: subscribeUrl, + }), + }) + + if (!res.ok) { + throw new Error("Failed to create webhook") + } + + return await res.json() +} + +export const reviewsClient = (() => { + if (!isOptIn("reviews")) { + notifyOptIn({ feature: "reviews", source: "clients/reviews" }) + } + + return createJudgeClient({ + baseUrl: env.JUDGE_BASE_URL!, + apiKey: env.JUDGE_API_TOKEN!, + shopDomain: env.SHOPIFY_STORE_DOMAIN, + }) +})() diff --git a/starters/shopify-meilisearch/lib/reviews/index.ts b/starters/shopify-meilisearch/lib/reviews/index.ts index 15f89253..30003d0a 100644 --- a/starters/shopify-meilisearch/lib/reviews/index.ts +++ b/starters/shopify-meilisearch/lib/reviews/index.ts @@ -1,106 +1,9 @@ -import type { GetProductReviewsOpts, GetProductReviewsResponse, JudgeMeWebhookKey, ProductReviewArgs, ProductReviewBody, Review } from "./types" +import { unstable_cache } from "next/cache" +import { reviewsClient } from "./client" +import type { ProductReviewBody } from "./types" -type CreateJudgeClientArgs = { - baseUrl: string - apiKey: string - shopDomain: string -} - -export function createJudgeClient({ baseUrl, apiKey, shopDomain }: CreateJudgeClientArgs) { - const url = new URL(baseUrl) - url.searchParams.set("api_token", apiKey) - url.searchParams.set("shop_domain", shopDomain) - - return { - getProductReviews: async (opts: GetProductReviewsOpts = {}) => getProductReviews(url, opts), - getAllProductReviews: async () => getAllProductReviews(url), - createProductReview: async (body: ProductReviewBody) => createProductReview({ baseUrl: url, body }), - createWebhook: async (key: JudgeMeWebhookKey, subscribeUrl: string) => createWebhook(url, key, subscribeUrl), - } -} - -async function getProductReviews( - baseUrl: URL, - opts: GetProductReviewsOpts = { - per_page: 10, - page: 1, - } -): Promise { - const localParams = new URLSearchParams(baseUrl.searchParams) - Object.entries(opts).forEach(([key, value]) => { - localParams.set(key, value.toString()) - }) - - const url = `${baseUrl.origin}${baseUrl.pathname}/reviews?${baseUrl.searchParams.toString()}&${localParams.toString()}` - - const reviewsCountUrl = `${baseUrl.origin}${baseUrl.pathname}/reviews/count?${baseUrl.searchParams.toString()}` - - const reviewsCount = await fetch(reviewsCountUrl) - const { count } = (await reviewsCount.json()) as { count: number } - - const reviews = (await fetch(url).then((res) => res.json())) as Pick - - return { ...reviews, total: count, totalPages: Math.ceil(count / reviews.per_page) } -} - -async function getAllProductReviews(baseUrl: URL) { - const allReviews: Review[] = [] - - const { reviews, totalPages } = await getProductReviews(baseUrl, { per_page: 100 }) - allReviews.push(...reviews) - - for (let page = 2; page <= totalPages; page++) { - const { reviews } = await getProductReviews(baseUrl, { per_page: 100, page }) - allReviews.push(...reviews) - } - - return allReviews -} - -async function createProductReview({ baseUrl, body }: ProductReviewArgs) { - const url = `${baseUrl.origin}${baseUrl.pathname}/reviews` - const res = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - ...body, - shop_domain: baseUrl.searchParams.get("shop_domain"), - platform: "shopify", // needs to be dynamic later on - }), - }) - - if (!res.ok) { - throw new Error("Failed to create review") - } - - const data = await res.json() - - return data -} - -async function createWebhook(baseUrl: URL, key: JudgeMeWebhookKey, subscribeUrl: string) { - const url = `${baseUrl.origin}${baseUrl.pathname}/webhooks?${baseUrl.searchParams.toString()}` - - if (key !== "review/created" && key !== "review/updated" && key !== "review/created_fail") { - throw new Error("Judge me: Invalid key to create a webhook") - } - - const res = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - key, - subscribe_url: subscribeUrl, - }), - }) - - if (!res.ok) { - throw new Error("Failed to create webhook") - } +export const getAllProductReviews = unstable_cache(reviewsClient.getAllProductReviews, ["all-product-reviews"], { revalidate: 86400 }) - return await res.json() +export const createProductReview = async (payload: ProductReviewBody) => { + return await reviewsClient.createProductReview(payload) }