import { cookies, headers } from "next/headers"; import { revalidatePath } from "next/cache"; import { NextRequest, NextResponse } from "next/server"; import { BagistoCreateUserOperation, BagistoProductInfo, BagistoSession, BagistoUser, ImageInfo, } from "@/types/types"; import { BAGISTO_SESSION, HIDDEN_PRODUCT_TAG, STOREFRONT_KEY, } from "../constants"; import { getServerSession } from "next-auth"; import { CUSTOMER_LOGOUT, CUSTOMER_REGISTRATION, FORGET_PASSWORD, } from "@/graphql/customer/mutations"; import { DocumentNode } from "graphql"; import { GRAPHQL_URL, REST_API_URL } from "@/utils/constants"; import { GET_FOOTER, GET_THEME_CUSTOMIZATION, PAGE_BY_URL_KEY, } from "@/graphql"; import { SUBSCRIBE_TO_NEWSLETTER } from "@/graphql/theme/mutations"; import { cachedGraphQLRequest } from "@/utils/hooks/useCache"; import { authOptions } from "@utils/auth"; import { RegisterInputs } from "@components/customer/RegistrationForm"; import { GetFooterResponse, ThemeCustomizationResult, ThemeCustomizationResponse, PageData, } from "@/types/theme/theme-customization"; type ExtractVariables = T extends { variables: object } ? T["variables"] : never; interface PageByUrlKeyResponse { pageByUrlKeypages?: PageData[]; } // rest api fetch export async function restApiFetch({ api, method, cache = "force-cache", headers, tags, variables, isCookies = true, guestToken, revalidate = 60, }: { api: string; method: "POST" | "GET"; cache?: RequestCache; headers?: HeadersInit | Record; tags?: string[]; variables?: ExtractVariables; isCookies?: boolean; guestToken?: string; revalidate?: number; }): Promise<{ status: number; body: T } | never> { try { const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`; let bagistoCartId = ""; let accessToken: string | undefined = undefined; if (isCookies) { const cookieStore = await cookies(); bagistoCartId = cookieStore.get(BAGISTO_SESSION)?.value ?? ""; const sessions = (await getServerSession( authOptions, )) as BagistoSession | null; accessToken = sessions?.user?.accessToken; } const baseHeaders: Record = { "Content-Type": "application/json", "X-STOREFRONT-KEY": STOREFRONT_KEY, }; if (accessToken) { baseHeaders.Authorization = `Bearer ${accessToken}`; } else if (guestToken) { baseHeaders.Authorization = `Bearer ${guestToken}`; } if (bagistoCartId) { baseHeaders.Cookie = `${BAGISTO_SESSION}=${bagistoCartId}`; } if (isCookies && headers) { if (headers instanceof Headers) { headers.forEach((value, key) => (baseHeaders[key] = value)); } else { Object.assign(baseHeaders, headers); } } console.log('restApiFetch --- baseHeaders:', baseHeaders) const param: RequestInit = { method: method, headers: baseHeaders, cache, next: { revalidate: cache === "no-store" ? 0 : revalidate || 60, ...(tags && { tags }), }, }; if(variables) { param.body = JSON.stringify({...variables}); } const result = await fetch(apiUrl, param); console.log('restApiFetch --- result:', result) const body = await result.json(); if (body.errors) throw body.errors[0]; return { status: result.status, body }; } catch (e) { throw e; } } export async function bagistoFetch({ cache = "force-cache", headers, query, tags, variables, isCookies = true, guestToken, revalidate = 60, operationName = '' }: { cache?: RequestCache; headers?: HeadersInit | Record; query: string | DocumentNode; tags?: string[]; variables?: ExtractVariables; isCookies?: boolean; guestToken?: string; revalidate?: number; operationName?: string; }): Promise<{ status: number; body: T } | never> { try { const queryString = typeof query === "string" ? query : (query.loc?.source?.body ?? ""); let bagistoCartId = ""; let accessToken: string | undefined = undefined; if (isCookies) { const cookieStore = await cookies(); bagistoCartId = cookieStore.get(BAGISTO_SESSION)?.value ?? ""; const sessions = (await getServerSession( authOptions, )) as BagistoSession | null; accessToken = sessions?.user?.accessToken; } const baseHeaders: Record = { "Content-Type": "application/json", "X-STOREFRONT-KEY": STOREFRONT_KEY, }; if (accessToken) { baseHeaders.Authorization = `Bearer ${accessToken}`; } else if (guestToken) { baseHeaders.Authorization = `Bearer ${guestToken}`; } if (bagistoCartId) { baseHeaders.Cookie = `${BAGISTO_SESSION}=${bagistoCartId}`; } if (isCookies && headers) { if (headers instanceof Headers) { headers.forEach((value, key) => (baseHeaders[key] = value)); } else { Object.assign(baseHeaders, headers); } } // let cc = await cookies(); // console.log('bagistoFetch --- url:', GRAPHQL_URL,isCookies); // console.log('bagistoFetch --- queryString:', queryString); console.log('bagistoFetch --- baseHeaders:', baseHeaders); // console.log('bagistoFetch --- cookies:', cc.getAll()); // console.log('bagistoFetch --- variables:', variables); const result = await fetch(GRAPHQL_URL, { method: "POST", headers: baseHeaders, body: JSON.stringify({ query: queryString, ...(variables && { variables }), }), cache, next: { revalidate: cache === "no-store" ? 0 : revalidate || 60, ...(tags && { tags }), }, }); const body = await result.json(); console.log('bagistoFetch --- body:',body); if (body.errors) { if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') { return { status: result.status, body }; } throw body.errors[0] } return { status: result.status, body }; } catch (e) { throw e; } } export async function bagistoFetchNoSession({ query, tags, variables, headers, cache = "force-cache", revalidate = 60, }: { query: string; tags?: string[]; variables?: ExtractVariables; headers?: HeadersInit | Record; cache?: RequestCache; isCookies?: boolean; revalidate?: number; }): Promise<{ status: number; body: T } | never> { try { const result = await fetch(GRAPHQL_URL, { method: "POST", headers: { "Content-Type": "application/json", "X-STOREFRONT-KEY": STOREFRONT_KEY, "x-locale": "en", "x-currency": "USD", ...headers, }, body: JSON.stringify({ ...(query && { query }), ...(variables && { variables }), }), cache, next: { revalidate: cache === "no-store" ? 0 : revalidate || 60, ...(tags && { tags }), }, }); const body = await result.json(); if (body.errors) { throw body.errors[0]; } return { status: result.status, body, }; } catch (e) { throw { error: e, query }; } } export const removeEdgesAndNodes = (array: Array) => { return array?.map((edge) => edge); }; const reshapeImages = (images: Array, productTitle: string) => { const flattened = removeEdgesAndNodes(images); return flattened.map((image) => { const filename = image?.url.match(/.*\/(.*)\..*/)?.[1]; return { ...image, altText: image?.altText || `${productTitle} - ${filename}`, }; }); }; const reshapeProduct = ( product: BagistoProductInfo, filterHiddenProducts: boolean = true, ) => { if ( !product || (filterHiddenProducts && product.tags?.includes(HIDDEN_PRODUCT_TAG)) ) { return undefined; } const { images, variants, ...rest } = product; return { ...rest, images: reshapeImages(images, product.title), variants: removeEdgesAndNodes(variants), }; }; export const reshapeProducts = (products: BagistoProductInfo[]) => { const reshapedProducts = []; for (const product of products) { if (product) { const reshapedProduct = reshapeProduct(product); if (reshapedProduct) { reshapedProducts.push(reshapedProduct); } } } return reshapedProducts; }; export async function createUserToLogin( input: RegisterInputs, ): Promise { try { const { passwordConfirmation, ...userInput } = input; const res = await bagistoFetch({ query: CUSTOMER_REGISTRATION, variables: { input: { ...userInput, confirmPassword: passwordConfirmation, status: "1", isVerified: "1", isSuspended: "0", subscribedToNewsLetter: true, }, }, cache: "no-store", revalidate: 3600, }); return res.body.data.createCustomer.customer; } catch (error: any) { throw new Error(error?.message || "Registration failed"); } } export async function logoutUser() { try { const session = await getServerSession(authOptions); const token = session?.user?.accessToken; if (!token) { return { success: false, message: "User token missing", }; } /** * @todo CUSTOMER_LOGOUT 接口会报错,待修复(确定是php后端接口报错) */ const res = await bagistoFetch<{ data: { createLogout: { logout: { success: boolean; message: string } } }; variables: { input: { token: string } }; }>({ query: CUSTOMER_LOGOUT, isCookies: true, revalidate: 3600, }); const success = res?.body?.data?.createLogout?.logout?.success ?? false; const message = res?.body?.data?.createLogout?.logout?.message ?? "Logout executed"; const cookieStore = await cookies(); cookieStore.delete(BAGISTO_SESSION); return { success, message, }; } catch (error: unknown) { return { success: false, message: error instanceof Error ? error.message : "Something went wrong", }; } } export async function recoverUserLogin( input: Record, ): Promise { try { return await bagistoFetch<{ data: unknown; variables: Record; }>({ query: FORGET_PASSWORD, variables: { ...input, }, cache: "no-store", revalidate: 3600, }); } catch (error) { return error; } } export async function subscribeUser( input: Record, ): Promise { try { return await bagistoFetch<{ data: unknown; variables: Record; }>({ query: SUBSCRIBE_TO_NEWSLETTER, variables: { ...input, }, cache: "no-store", }); } catch (error) { return error; } } export async function getThemeCustomization(): Promise { try { const [{data: footerRes}, {data:servicesRes}] = await Promise.all([ cachedGraphQLRequest("static", GET_FOOTER, { type: "footer_links", }), cachedGraphQLRequest("static", GET_FOOTER, { type: "services_content", }), ]); return { footer_links: footerRes, services_content: servicesRes, }; } catch (err) { console.error("ThemeCustomization Error:", err); } return { footer_links: null, services_content: null, }; } export async function revalidate(req: NextRequest): Promise { const collectionWebhooks = [ "collections/create", "collections/delete", "collections/update", ]; const productWebhooks = [ "products/create", "products/delete", "products/update", ]; const topic = (await headers()).get("x-bagisto-topic") || "unknown"; const secret = req.nextUrl.searchParams.get("secret"); const isCollectionUpdate = collectionWebhooks.includes(topic); const isProductUpdate = productWebhooks.includes(topic); if (!secret || secret !== process.env.BAGISTO_REVALIDATION_SECRET) { return NextResponse.json({ status: 200 }); } if (!isCollectionUpdate && !isProductUpdate) { return NextResponse.json({ status: 200, message: "No action needed" }); } if (isProductUpdate) { revalidatePath("/", "layout"); } else if (isCollectionUpdate) { revalidatePath("/", "layout"); } return NextResponse.json({ status: 200, revalidated: true, topic, now: Date.now(), }); } export async function getHomePageData(): Promise { const res = await bagistoFetch<{ data: ThemeCustomizationResponse; variables: { first: number }; }>({ query: GET_THEME_CUSTOMIZATION, variables: { first: 20 }, tags: ["theme-customization"], revalidate: 60, }); return res.body.data; } export async function getPage(input: { urlKey: string }): Promise { const res = await bagistoFetch<{ data: PageByUrlKeyResponse; variables: { pageByUrlKey: string }; }>({ query: PAGE_BY_URL_KEY, cache: "no-store", isCookies: false, variables: { pageByUrlKey: input.urlKey }, }); return res.body.data?.pageByUrlKeypages || []; }