index.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. import { cookies } from "next/headers";
  2. import { auth } from "@/utils/auth/auth-helper";
  3. import { print, DocumentNode } from "graphql";
  4. import {
  5. GRAPHQL_URL,
  6. REST_API_URL,
  7. } from "@/utils/constants/server";
  8. import {
  9. GUEST_CART_TOKEN,
  10. CURRENT_CURRENCY,
  11. CURRENT_LOCAL,
  12. CURRENT_CHANNEL,
  13. } from "@/utils/constants";
  14. import {
  15. GET_FOOTER,
  16. PAGE_BY_URL_KEY,
  17. } from "@/graphql";
  18. import { SUBSCRIBE_TO_NEWSLETTER } from "@/graphql/theme/mutations";
  19. import { cachedGraphQLRequest } from "@/utils/hooks/useCache";
  20. import {
  21. GetFooterResponse,
  22. ThemeCustomizationResult,
  23. PageData,
  24. } from "@/types/theme/theme-customization";
  25. import {FetchGraphqlResult} from "@/types/graphqlFetch/type";
  26. type ExtractVariables<T> = T extends { variables: object }
  27. ? T["variables"]
  28. : any;
  29. type ExtractRestFulData<T> = T extends { data: infer D } ? D : any;
  30. type ExtractGraphqlData<T> = T extends { data: infer D } ? { data: D } : any;
  31. interface PageByUrlKeyResponse {
  32. pageByUrlKeypages?: PageData[];
  33. }
  34. const STOREFRONT_KEY = process.env.NEXT_PUBLIC_BAGISTO_STOREFRONT_KEY;
  35. async function getBaseHeader() {
  36. /**
  37. * Headers:
  38. * X-LOCALE — locale code, e.g. "en", "fr", "ar"
  39. * X-CHANNEL — channel code, e.g. "default"
  40. * X-CURRENCY — currency code, e.g. "USD", "EUR", "INR"
  41. * */
  42. if(!STOREFRONT_KEY) {
  43. throw new Error("STOREFRONT_KEY must be configed!");
  44. }
  45. const cookieStore = await cookies();
  46. const xCurrency = cookieStore.get(CURRENT_CURRENCY)?.value; // 在proxy.ts中已经设置过值了
  47. const xLocal = cookieStore.get(CURRENT_LOCAL)?.value; // 在proxy.ts中已经设置过值了
  48. const xChannel = cookieStore.get(CURRENT_CHANNEL)?.value; // 在proxy.ts中已经设置过值了
  49. const baseHeaders: Record<string, string> = {
  50. "Content-Type": "application/json",
  51. "X-STOREFRONT-KEY": STOREFRONT_KEY,
  52. };
  53. if(xCurrency) {
  54. baseHeaders["X-Currency"] = xCurrency;
  55. }
  56. if(xLocal) {
  57. baseHeaders["X-Local"] = xLocal;
  58. }
  59. if(xChannel) {
  60. baseHeaders["X-Channel"] = xChannel;
  61. }
  62. return baseHeaders;
  63. }
  64. export async function getAuthorizationToken(): Promise<{token: string | null; isGuest: boolean;}> {
  65. // 登录用户从nextAuth里获取
  66. /*
  67. const tokenCookie =
  68. cookieStore.get("next-auth.session-token")
  69. ?? cookieStore.get("__Secure-next-auth.session-token");
  70. if (tokenCookie?.value) {
  71. const token = await decode({
  72. token: tokenCookie.value,
  73. secret: process.env.NEXTAUTH_SECRET!,
  74. });
  75. console.log('getAuthorizationToken decode ===============',token);
  76. return {
  77. token: token?.accessToken ?? '',
  78. isGuest: false,
  79. };
  80. }
  81. */
  82. const authSession = await auth();
  83. const accessToken = authSession?.user?.accessToken;
  84. if (accessToken) {
  85. return {
  86. token: accessToken,
  87. isGuest: false,
  88. };
  89. }
  90. // 游客从cookie里获取
  91. const cookieStore = await cookies();
  92. const guestToken = cookieStore.get(GUEST_CART_TOKEN)?.value;
  93. return {
  94. token: guestToken || null,
  95. isGuest: true
  96. };
  97. }
  98. // rest api fetch
  99. export async function restApiFetch<T>({
  100. api,
  101. method,
  102. cache = "force-cache",
  103. headers,
  104. tags,
  105. variables,
  106. isRoute = true,
  107. revalidate = 60,
  108. takeAuthorization = true, // 是否携带token,默认携带
  109. }: {
  110. api: string;
  111. method: "POST" | "GET" | "PUT" | "DELETE";
  112. cache?: RequestCache;
  113. headers?: HeadersInit | Record<string, string>;
  114. tags?: string[];
  115. variables?: ExtractVariables<T>;
  116. isRoute?: boolean; // 是否是在route handle中调用
  117. guestToken?: string;
  118. revalidate?: number;
  119. takeAuthorization?: boolean;
  120. }): Promise<{ status: number; body: ExtractRestFulData<T> } | never> {
  121. try {
  122. const apiUrl = api.startsWith("http") ? api : `${REST_API_URL}${api}`;
  123. const url = new URL(apiUrl);
  124. const headerRes = await getBaseHeader();
  125. const baseHeaders: Record<string, string> = {...headerRes};
  126. if(takeAuthorization) {
  127. const tokenRes = await getAuthorizationToken();
  128. if (tokenRes.token) {
  129. baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
  130. }
  131. }
  132. if (headers) {
  133. if (headers instanceof Headers) {
  134. headers.forEach((value, key) => (baseHeaders[key] = value));
  135. } else {
  136. Object.assign(baseHeaders, headers);
  137. }
  138. }
  139. console.log('restApiFetch --- baseHeaders:', baseHeaders)
  140. const param: RequestInit = {
  141. method: method,
  142. headers: baseHeaders,
  143. cache,
  144. next: {
  145. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  146. ...(tags && { tags }),
  147. },
  148. };
  149. if(variables && method === "POST") {
  150. param.body = JSON.stringify({...variables});
  151. }
  152. if(variables && method === "GET") {
  153. const entries = Object.entries(variables as Record<string, unknown>);
  154. for (const [key, val] of entries) {
  155. if (val !== undefined && val !== null) {
  156. url.searchParams.set(key, String(val));
  157. }
  158. }
  159. }
  160. const result = await fetch(url, param);
  161. const body = await result.json();
  162. console.log('restApiFetch --- body:', body)
  163. if(!isRoute) {
  164. if(result.status === 401) {
  165. const err = new Error('Authorization Bearer token is required. Please Login.');//new Error(body.message || 'Authorization Bearer token is required');
  166. err.name = "UnauthorizedError";
  167. throw err;
  168. }
  169. }
  170. return { status: result.status, body };
  171. } catch (e) {
  172. throw e;
  173. }
  174. }
  175. // graphql fetch use for server side
  176. export async function serverGraphqlFetch<
  177. TData,
  178. TVariables = Record<string, never>
  179. >({
  180. cache = "no-store",
  181. headers,
  182. query,
  183. tags,
  184. variables,
  185. revalidate = 0,
  186. takeAuthorization = true, // 是否携带token,默认携带
  187. }: {
  188. cache?: RequestCache;
  189. headers?: HeadersInit | Record<string, string>;
  190. query: string | DocumentNode;
  191. tags?: string[];
  192. variables?: TVariables;
  193. revalidate?: number;
  194. takeAuthorization?: boolean;
  195. }): Promise<FetchGraphqlResult<TData>> {
  196. try {
  197. const queryString = typeof query === "string" ? query : print(query);
  198. const headerRes = await getBaseHeader();
  199. const baseHeaders: Record<string, string> = {...headerRes};
  200. if(takeAuthorization) {
  201. const tokenRes = await getAuthorizationToken();
  202. if (tokenRes.token) {
  203. baseHeaders['Authorization'] = `Bearer ${tokenRes.token}`;
  204. }
  205. }
  206. if (headers) {
  207. if (headers instanceof Headers) {
  208. headers.forEach((value, key) => (baseHeaders[key] = value));
  209. } else {
  210. Object.assign(baseHeaders, headers);
  211. }
  212. }
  213. console.log('serverGraphqlFetch --- baseHeaders:', baseHeaders);
  214. const result = await fetch(GRAPHQL_URL, {
  215. method: "POST",
  216. headers: baseHeaders,
  217. body: JSON.stringify({
  218. query: queryString,
  219. ...(variables && { variables }),
  220. }),
  221. cache,
  222. next: {
  223. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  224. ...(tags && { tags }),
  225. },
  226. });
  227. const body = await result.json();
  228. const err = body.errors?.[0] ?? null;
  229. if(err && err.extensions.status === 401) {
  230. // "UNAUTHENTICATED"
  231. const err = new Error('Authorization Bearer token is required. Please Login.');
  232. err.name = "UnauthorizedError";
  233. throw err;
  234. }
  235. return {
  236. status:result.status,
  237. data:body.data || null,
  238. error: err
  239. }
  240. } catch (e) {
  241. console.error( "GraphQL request failed", e );
  242. throw e;
  243. }
  244. }
  245. // graphql fetch use for route handler
  246. export async function bagistoFetch<T>({
  247. cache = "force-cache",
  248. headers,
  249. query,
  250. tags,
  251. variables,
  252. // isCookies = true,
  253. // guestToken,
  254. takeAuthorization = true, // 是否携带token,默认携带
  255. revalidate = 60,
  256. }: {
  257. cache?: RequestCache;
  258. headers?: HeadersInit | Record<string, string>;
  259. query: string | DocumentNode;
  260. tags?: string[];
  261. variables?: ExtractVariables<T>;
  262. // isCookies?: boolean;
  263. // guestToken?: string;
  264. takeAuthorization?: boolean;
  265. revalidate?: number;
  266. }): Promise<{ status: number; body: ExtractGraphqlData<T> } | never> {
  267. try {
  268. const queryString =
  269. typeof query === "string" ? query : print(query);
  270. const headerRes = await getBaseHeader();
  271. const baseHeaders: Record<string, string> = {...headerRes};
  272. if(takeAuthorization) {
  273. const tokenRes = await getAuthorizationToken();
  274. if (tokenRes.token) {
  275. baseHeaders.Authorization = `Bearer ${tokenRes.token}`;
  276. }
  277. }
  278. if (headers) {
  279. if (headers instanceof Headers) {
  280. headers.forEach((value, key) => (baseHeaders[key] = value));
  281. } else {
  282. Object.assign(baseHeaders, headers);
  283. }
  284. }
  285. // let cc = await cookies();
  286. // console.log('bagistoFetch --- url:', GRAPHQL_URL,isCookies);
  287. // console.log('bagistoFetch --- queryString:', queryString);
  288. console.log('bagistoFetch --- baseHeaders:', baseHeaders);
  289. // console.log('bagistoFetch --- cookies:', cc.getAll());
  290. // console.log('bagistoFetch --- variables:', variables);
  291. const result = await fetch(GRAPHQL_URL, {
  292. method: "POST",
  293. headers: baseHeaders,
  294. body: JSON.stringify({
  295. query: queryString,
  296. ...(variables && { variables }),
  297. }),
  298. cache,
  299. next: {
  300. revalidate: cache === "no-store" ? 0 : revalidate || 60,
  301. ...(tags && { tags }),
  302. },
  303. });
  304. const body = await result.json();
  305. console.log('bagistoFetch --- body:',body);
  306. return { status: result.status, body };
  307. } catch (e) {
  308. throw e;
  309. }
  310. }
  311. export async function subscribeUser(
  312. input: Record<string, unknown>,
  313. ): Promise<unknown> {
  314. try {
  315. return await bagistoFetch<{
  316. data: unknown;
  317. variables: Record<string, unknown>;
  318. }>({
  319. query: SUBSCRIBE_TO_NEWSLETTER,
  320. variables: {
  321. ...input,
  322. },
  323. cache: "no-store",
  324. });
  325. } catch (error) {
  326. return error;
  327. }
  328. }
  329. export async function getThemeCustomization(): Promise<ThemeCustomizationResult> {
  330. try {
  331. const [{data: footerRes}, {data:servicesRes}] = await Promise.all([
  332. cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
  333. type: "footer_links",
  334. }),
  335. cachedGraphQLRequest<GetFooterResponse>("static", GET_FOOTER, {
  336. type: "services_content",
  337. }),
  338. ]);
  339. return {
  340. footer_links: footerRes,
  341. services_content: servicesRes,
  342. };
  343. } catch (err) {
  344. console.error("ThemeCustomization Error:", err);
  345. }
  346. return {
  347. footer_links: null,
  348. services_content: null,
  349. };
  350. }
  351. export async function getPage(input: { urlKey: string }): Promise<PageData[]> {
  352. const res = await bagistoFetch<{
  353. data: PageByUrlKeyResponse;
  354. variables: { pageByUrlKey: string };
  355. }>({
  356. query: PAGE_BY_URL_KEY,
  357. cache: "no-store",
  358. // isCookies: false,
  359. variables: { pageByUrlKey: input.urlKey },
  360. });
  361. return res.body.data?.pageByUrlKeypages || [];
  362. }