index.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. import { cookies, headers } from "next/headers";
  2. import { revalidatePath } from "next/cache";
  3. import { NextRequest, NextResponse } from "next/server";
  4. import {
  5. BagistoCreateUserOperation,
  6. BagistoProductInfo,
  7. BagistoSession,
  8. BagistoUser,
  9. ImageInfo,
  10. } from "@/types/types";
  11. import {
  12. BAGISTO_SESSION,
  13. HIDDEN_PRODUCT_TAG,
  14. STOREFRONT_KEY,
  15. } from "../constants";
  16. import { getServerSession } from "next-auth";
  17. import {
  18. CUSTOMER_LOGOUT,
  19. CUSTOMER_REGISTRATION,
  20. FORGET_PASSWORD,
  21. } from "@/graphql/customer/mutations";
  22. import { DocumentNode } from "graphql";
  23. import { GRAPHQL_URL, REST_API_URL } from "@/utils/constants";
  24. import {
  25. GET_FOOTER,
  26. GET_THEME_CUSTOMIZATION,
  27. PAGE_BY_URL_KEY,
  28. } from "@/graphql";
  29. import { SUBSCRIBE_TO_NEWSLETTER } from "@/graphql/theme/mutations";
  30. import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
  31. import { authOptions } from "@utils/auth";
  32. import { RegisterInputs } from "@components/customer/RegistrationForm";
  33. import {
  34. GetFooterResponse,
  35. ThemeCustomizationResult,
  36. ThemeCustomizationResponse,
  37. PageData,
  38. } from "@/types/theme/theme-customization";
  39. type ExtractVariables<T> = T extends { variables: object }
  40. ? T["variables"]
  41. : never;
  42. interface PageByUrlKeyResponse {
  43. pageByUrlKeypages?: PageData[];
  44. }
  45. // rest api fetch
  46. export async function restApiFetch<T>({
  47. api,
  48. method,
  49. cache = "force-cache",
  50. headers,
  51. tags,
  52. variables,
  53. isCookies = true,
  54. guestToken,
  55. revalidate = 60,
  56. }: {
  57. api: string;
  58. method: "POST" | "GET";
  59. cache?: RequestCache;
  60. headers?: HeadersInit | Record<string, string>;
  61. tags?: string[];
  62. variables?: ExtractVariables<T>;
  63. isCookies?: boolean;
  64. guestToken?: string;
  65. revalidate?: number;
  66. }): Promise<{ status: number; body: T } | never> {
  67. try {
  68. const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
  69. let bagistoCartId = "";
  70. let accessToken: string | undefined = undefined;
  71. if (isCookies) {
  72. const cookieStore = await cookies();
  73. bagistoCartId = cookieStore.get(BAGISTO_SESSION)?.value ?? "";
  74. const sessions = (await getServerSession(
  75. authOptions,
  76. )) as BagistoSession | null;
  77. accessToken = sessions?.user?.accessToken;
  78. }
  79. const baseHeaders: Record<string, string> = {
  80. "Content-Type": "application/json",
  81. "X-STOREFRONT-KEY": STOREFRONT_KEY,
  82. };
  83. if (accessToken) {
  84. baseHeaders.Authorization = `Bearer ${accessToken}`;
  85. } else if (guestToken) {
  86. baseHeaders.Authorization = `Bearer ${guestToken}`;
  87. }
  88. if (bagistoCartId) {
  89. baseHeaders.Cookie = `${BAGISTO_SESSION}=${bagistoCartId}`;
  90. }
  91. if (isCookies && headers) {
  92. if (headers instanceof Headers) {
  93. headers.forEach((value, key) => (baseHeaders[key] = value));
  94. } else {
  95. Object.assign(baseHeaders, headers);
  96. }
  97. }
  98. console.log('restApiFetch --- baseHeaders:', baseHeaders)
  99. const param: RequestInit = {
  100. method: method,
  101. headers: baseHeaders,
  102. cache,
  103. next: {
  104. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  105. ...(tags && { tags }),
  106. },
  107. };
  108. if(variables) {
  109. param.body = JSON.stringify({...variables});
  110. }
  111. const result = await fetch(apiUrl, param);
  112. console.log('restApiFetch --- result:', result)
  113. const body = await result.json();
  114. if (body.errors) throw body.errors[0];
  115. return { status: result.status, body };
  116. } catch (e) {
  117. throw e;
  118. }
  119. }
  120. export async function bagistoFetch<T>({
  121. cache = "force-cache",
  122. headers,
  123. query,
  124. tags,
  125. variables,
  126. isCookies = true,
  127. guestToken,
  128. revalidate = 60,
  129. operationName = ''
  130. }: {
  131. cache?: RequestCache;
  132. headers?: HeadersInit | Record<string, string>;
  133. query: string | DocumentNode;
  134. tags?: string[];
  135. variables?: ExtractVariables<T>;
  136. isCookies?: boolean;
  137. guestToken?: string;
  138. revalidate?: number;
  139. operationName?: string;
  140. }): Promise<{ status: number; body: T } | never> {
  141. try {
  142. const queryString =
  143. typeof query === "string" ? query : (query.loc?.source?.body ?? "");
  144. let bagistoCartId = "";
  145. let accessToken: string | undefined = undefined;
  146. if (isCookies) {
  147. const cookieStore = await cookies();
  148. bagistoCartId = cookieStore.get(BAGISTO_SESSION)?.value ?? "";
  149. const sessions = (await getServerSession(
  150. authOptions,
  151. )) as BagistoSession | null;
  152. accessToken = sessions?.user?.accessToken;
  153. }
  154. const baseHeaders: Record<string, string> = {
  155. "Content-Type": "application/json",
  156. "X-STOREFRONT-KEY": STOREFRONT_KEY,
  157. };
  158. if (accessToken) {
  159. baseHeaders.Authorization = `Bearer ${accessToken}`;
  160. } else if (guestToken) {
  161. baseHeaders.Authorization = `Bearer ${guestToken}`;
  162. }
  163. if (bagistoCartId) {
  164. baseHeaders.Cookie = `${BAGISTO_SESSION}=${bagistoCartId}`;
  165. }
  166. if (isCookies && headers) {
  167. if (headers instanceof Headers) {
  168. headers.forEach((value, key) => (baseHeaders[key] = value));
  169. } else {
  170. Object.assign(baseHeaders, headers);
  171. }
  172. }
  173. // let cc = await cookies();
  174. // console.log('bagistoFetch --- url:', GRAPHQL_URL,isCookies);
  175. // console.log('bagistoFetch --- queryString:', queryString);
  176. console.log('bagistoFetch --- baseHeaders:', baseHeaders);
  177. // console.log('bagistoFetch --- cookies:', cc.getAll());
  178. // console.log('bagistoFetch --- variables:', variables);
  179. const result = await fetch(GRAPHQL_URL, {
  180. method: "POST",
  181. headers: baseHeaders,
  182. body: JSON.stringify({
  183. query: queryString,
  184. ...(variables && { variables }),
  185. }),
  186. cache,
  187. next: {
  188. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  189. ...(tags && { tags }),
  190. },
  191. });
  192. const body = await result.json();
  193. console.log('bagistoFetch --- body:',body);
  194. if (body.errors) {
  195. if(operationName === 'GetCartItem' && body.errors[0].message === 'Cart not found') {
  196. return { status: result.status, body };
  197. }
  198. throw body.errors[0]
  199. }
  200. return { status: result.status, body };
  201. } catch (e) {
  202. throw e;
  203. }
  204. }
  205. export async function bagistoFetchNoSession<T>({
  206. query,
  207. tags,
  208. variables,
  209. headers,
  210. cache = "force-cache",
  211. revalidate = 60,
  212. }: {
  213. query: string;
  214. tags?: string[];
  215. variables?: ExtractVariables<T>;
  216. headers?: HeadersInit | Record<string, string>;
  217. cache?: RequestCache;
  218. isCookies?: boolean;
  219. revalidate?: number;
  220. }): Promise<{ status: number; body: T } | never> {
  221. try {
  222. const result = await fetch(GRAPHQL_URL, {
  223. method: "POST",
  224. headers: {
  225. "Content-Type": "application/json",
  226. "X-STOREFRONT-KEY": STOREFRONT_KEY,
  227. "x-locale": "en",
  228. "x-currency": "USD",
  229. ...headers,
  230. },
  231. body: JSON.stringify({
  232. ...(query && { query }),
  233. ...(variables && { variables }),
  234. }),
  235. cache,
  236. next: {
  237. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  238. ...(tags && { tags }),
  239. },
  240. });
  241. const body = await result.json();
  242. if (body.errors) {
  243. throw body.errors[0];
  244. }
  245. return {
  246. status: result.status,
  247. body,
  248. };
  249. } catch (e) {
  250. throw { error: e, query };
  251. }
  252. }
  253. export const removeEdgesAndNodes = <T>(array: Array<T>) => {
  254. return array?.map((edge) => edge);
  255. };
  256. const reshapeImages = (images: Array<ImageInfo>, productTitle: string) => {
  257. const flattened = removeEdgesAndNodes(images);
  258. return flattened.map((image) => {
  259. const filename = image?.url.match(/.*\/(.*)\..*/)?.[1];
  260. return {
  261. ...image,
  262. altText: image?.altText || `${productTitle} - ${filename}`,
  263. };
  264. });
  265. };
  266. const reshapeProduct = (
  267. product: BagistoProductInfo,
  268. filterHiddenProducts: boolean = true,
  269. ) => {
  270. if (
  271. !product ||
  272. (filterHiddenProducts && product.tags?.includes(HIDDEN_PRODUCT_TAG))
  273. ) {
  274. return undefined;
  275. }
  276. const { images, variants, ...rest } = product;
  277. return {
  278. ...rest,
  279. images: reshapeImages(images, product.title),
  280. variants: removeEdgesAndNodes(variants),
  281. };
  282. };
  283. export const reshapeProducts = (products: BagistoProductInfo[]) => {
  284. const reshapedProducts = [];
  285. for (const product of products) {
  286. if (product) {
  287. const reshapedProduct = reshapeProduct(product);
  288. if (reshapedProduct) {
  289. reshapedProducts.push(reshapedProduct);
  290. }
  291. }
  292. }
  293. return reshapedProducts;
  294. };
  295. export async function createUserToLogin(
  296. input: RegisterInputs,
  297. ): Promise<BagistoUser> {
  298. try {
  299. const { passwordConfirmation, ...userInput } = input;
  300. const res = await bagistoFetch<BagistoCreateUserOperation>({
  301. query: CUSTOMER_REGISTRATION,
  302. variables: {
  303. input: {
  304. ...userInput,
  305. confirmPassword: passwordConfirmation,
  306. status: "1",
  307. isVerified: "1",
  308. isSuspended: "0",
  309. subscribedToNewsLetter: true,
  310. },
  311. },
  312. cache: "no-store",
  313. revalidate: 3600,
  314. });
  315. return res.body.data.createCustomer.customer;
  316. } catch (error: any) {
  317. throw new Error(error?.message || "Registration failed");
  318. }
  319. }
  320. export async function logoutUser() {
  321. try {
  322. const session = await getServerSession(authOptions);
  323. const token = session?.user?.accessToken;
  324. if (!token) {
  325. return {
  326. success: false,
  327. message: "User token missing",
  328. };
  329. }
  330. /**
  331. * @todo CUSTOMER_LOGOUT 接口会报错,待修复(确定是php后端接口报错)
  332. */
  333. const res = await bagistoFetch<{
  334. data: { createLogout: { logout: { success: boolean; message: string } } };
  335. variables: { input: { token: string } };
  336. }>({
  337. query: CUSTOMER_LOGOUT,
  338. isCookies: true,
  339. revalidate: 3600,
  340. });
  341. const success = res?.body?.data?.createLogout?.logout?.success ?? false;
  342. const message =
  343. res?.body?.data?.createLogout?.logout?.message ?? "Logout executed";
  344. const cookieStore = await cookies();
  345. cookieStore.delete(BAGISTO_SESSION);
  346. return {
  347. success,
  348. message,
  349. };
  350. } catch (error: unknown) {
  351. return {
  352. success: false,
  353. message: error instanceof Error ? error.message : "Something went wrong",
  354. };
  355. }
  356. }
  357. export async function recoverUserLogin(
  358. input: Record<string, unknown>,
  359. ): Promise<unknown> {
  360. try {
  361. return await bagistoFetch<{
  362. data: unknown;
  363. variables: Record<string, unknown>;
  364. }>({
  365. query: FORGET_PASSWORD,
  366. variables: {
  367. ...input,
  368. },
  369. cache: "no-store",
  370. revalidate: 3600,
  371. });
  372. } catch (error) {
  373. return error;
  374. }
  375. }
  376. export async function subscribeUser(
  377. input: Record<string, unknown>,
  378. ): Promise<unknown> {
  379. try {
  380. return await bagistoFetch<{
  381. data: unknown;
  382. variables: Record<string, unknown>;
  383. }>({
  384. query: SUBSCRIBE_TO_NEWSLETTER,
  385. variables: {
  386. ...input,
  387. },
  388. cache: "no-store",
  389. });
  390. } catch (error) {
  391. return error;
  392. }
  393. }
  394. export async function getThemeCustomization(): Promise<ThemeCustomizationResult> {
  395. try {
  396. const [{data: footerRes}, {data:servicesRes}] = await Promise.all([
  397. cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
  398. type: "footer_links",
  399. }),
  400. cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
  401. type: "services_content",
  402. }),
  403. ]);
  404. return {
  405. footer_links: footerRes,
  406. services_content: servicesRes,
  407. };
  408. } catch (err) {
  409. console.error("ThemeCustomization Error:", err);
  410. }
  411. return {
  412. footer_links: null,
  413. services_content: null,
  414. };
  415. }
  416. export async function revalidate(req: NextRequest): Promise<NextResponse> {
  417. const collectionWebhooks = [
  418. "collections/create",
  419. "collections/delete",
  420. "collections/update",
  421. ];
  422. const productWebhooks = [
  423. "products/create",
  424. "products/delete",
  425. "products/update",
  426. ];
  427. const topic = (await headers()).get("x-bagisto-topic") || "unknown";
  428. const secret = req.nextUrl.searchParams.get("secret");
  429. const isCollectionUpdate = collectionWebhooks.includes(topic);
  430. const isProductUpdate = productWebhooks.includes(topic);
  431. if (!secret || secret !== process.env.BAGISTO_REVALIDATION_SECRET) {
  432. return NextResponse.json({ status: 200 });
  433. }
  434. if (!isCollectionUpdate && !isProductUpdate) {
  435. return NextResponse.json({ status: 200, message: "No action needed" });
  436. }
  437. if (isProductUpdate) {
  438. revalidatePath("/", "layout");
  439. } else if (isCollectionUpdate) {
  440. revalidatePath("/", "layout");
  441. }
  442. return NextResponse.json({
  443. status: 200,
  444. revalidated: true,
  445. topic,
  446. now: Date.now(),
  447. });
  448. }
  449. export async function getHomePageData(): Promise<ThemeCustomizationResponse> {
  450. const res = await bagistoFetch<{
  451. data: ThemeCustomizationResponse;
  452. variables: { first: number };
  453. }>({
  454. query: GET_THEME_CUSTOMIZATION,
  455. variables: { first: 20 },
  456. tags: ["theme-customization"],
  457. revalidate: 60,
  458. });
  459. return res.body.data;
  460. }
  461. export async function getPage(input: { urlKey: string }): Promise<PageData[]> {
  462. const res = await bagistoFetch<{
  463. data: PageByUrlKeyResponse;
  464. variables: { pageByUrlKey: string };
  465. }>({
  466. query: PAGE_BY_URL_KEY,
  467. cache: "no-store",
  468. isCookies: false,
  469. variables: { pageByUrlKey: input.urlKey },
  470. });
  471. return res.body.data?.pageByUrlKeypages || [];
  472. }