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

refactor: move reviews lib #87

Merged
merged 1 commit into from
Oct 31, 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
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)
}
Loading