Skip to content

Commit

Permalink
refactor: move reviews lib
Browse files Browse the repository at this point in the history
  • Loading branch information
ddaoxuan committed Oct 31, 2024
1 parent 46eafd4 commit d734c91
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 122 deletions.
4 changes: 2 additions & 2 deletions starters/shopify-meilisearch/app/actions/reviews.actions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"use server"

import { reviewsClient } from "clients/reviews"
import { createProductReview } from "lib/reviews"
import type { ProductReviewBody } from "lib/reviews/types"

import { headers } from "next/headers"

export const submitReview = async (payload: Omit<ProductReviewBody, "ip_addr">) => {
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)
}
Expand Down
4 changes: 2 additions & 2 deletions starters/shopify-meilisearch/app/api/reviews/sync/route.ts
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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"],
}),
Expand Down
15 changes: 0 additions & 15 deletions starters/shopify-meilisearch/clients/reviews.ts

This file was deleted.

120 changes: 120 additions & 0 deletions starters/shopify-meilisearch/lib/reviews/client.ts
Original file line number Diff line number Diff line change
@@ -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<GetProductReviewsResponse> {
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<GetProductReviewsResponse, "per_page" | "reviews" | "current_page">

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,
})
})()
109 changes: 6 additions & 103 deletions starters/shopify-meilisearch/lib/reviews/index.ts
Original file line number Diff line number Diff line change
@@ -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<GetProductReviewsResponse> {
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<GetProductReviewsResponse, "per_page" | "reviews" | "current_page">

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)
}

0 comments on commit d734c91

Please sign in to comment.