@evenicanpm/storefront-core 2.3.1 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +42 -9
- package/src/api-manager/datasources/d365/d365-address.datasource.ts +148 -61
- package/src/api-manager/datasources/d365/d365-cart.datasource.ts +23 -1
- package/src/api-manager/datasources/d365/d365-order.datasource.ts +6 -1
- package/src/api-manager/datasources/d365/d365-user.datasource.ts +109 -37
- package/src/api-manager/datasources/d365/utils/get-context-cookie.ts +44 -10
- package/src/api-manager/index.ts +2 -1
- package/src/api-manager/lib/get-graphql-client.ts +35 -7
- package/src/api-manager/schemas/product.schema.ts +1 -1
- package/src/api-manager/services/create-query.ts +36 -3
- package/src/api-manager/services/get-query-client.ts +10 -0
- package/src/auth/better-auth.ts +282 -15
- package/src/cms/blocks/block-manager.tsx +1 -1
- package/src/cms/blocks/components/product-section-fullwidth/index.tsx +3 -1
- package/src/cms/endpoints.ts +7 -2
- package/src/components/BazaarMenu.tsx +1 -1
- package/src/components/Typography.tsx +4 -2
- package/src/components/carousel-cards/carousel-card-1/CarouselCard1.stories.tsx +1 -1
- package/src/components/categories/category-list/category-list.tsx +9 -5
- package/src/components/categories/category-menu.tsx +1 -1
- package/src/components/countries-input.tsx +4 -0
- package/src/components/header/__tests__/user.test.tsx +5 -107
- package/src/components/header/components/user.tsx +72 -45
- package/src/components/header/sticky-header.tsx +0 -1
- package/src/components/navbar/mobile-menu/mobile-menu.test.tsx +1 -1
- package/src/components/navbar/utils/transform-nav.test.ts +1 -1
- package/src/components/navbar/utils/transform-nav.ts +1 -1
- package/src/components/product-cards/product-card/product-card.tsx +5 -2
- package/src/components/product-cards/product-card/styles/index.ts +1 -5
- package/src/components/products-view/compound/products-grid-view.tsx +5 -1
- package/src/components/products-view/compound/products-list-view.tsx +5 -1
- package/src/global.d.ts +3 -0
- package/src/hooks/use-nextauth-session.ts +0 -33
- package/src/lib/auth-client.ts +14 -0
- package/src/lib/auth.ts +4 -0
- package/src/lib/entra-native-auth.ts +138 -0
- package/src/lib/native-session.ts +434 -0
- package/src/pages/account/addresses/address-form.tsx +20 -2
- package/src/pages/account/profile/__tests__/profile-form.test.tsx +173 -0
- package/src/pages/account/profile/profile-form.tsx +285 -0
- package/src/pages/account/profile/profile-validation.ts +52 -0
- package/src/pages/account/wishlist/wishlist-item.tsx +1 -2
- package/src/pages/auth/auth-activate-page.tsx +509 -0
- package/src/pages/auth/auth-layout.tsx +73 -0
- package/src/pages/auth/auth-login-page.tsx +241 -0
- package/src/pages/auth/auth-reset-password-page.tsx +487 -0
- package/src/pages/auth/compound/auth-form.tsx +182 -0
- package/src/pages/auth/compound/auth-pages.tsx +21 -0
- package/src/pages/auth/lib/friendly-error.ts +70 -0
- package/src/pages/auth/lib/index.ts +2 -0
- package/src/pages/auth/lib/types.ts +5 -0
- package/src/pages/cart/cart-item.tsx +1 -7
- package/src/pages/checkout/checkout-alt-form/checkout-form.tsx +14 -3
- package/src/pages/checkout/checkout-alt-form/steps/address/address-card.tsx +5 -3
- package/src/pages/checkout/checkout-alt-form/steps/address/delivery-address.tsx +20 -6
- package/src/pages/checkout/checkout-alt-form/steps/address/new-address-form.tsx +1 -1
- package/src/pages/checkout/checkout-alt-form/steps/customer-info/customer-information.tsx +1 -1
- package/src/pages/confirmation/ordered-products.tsx +3 -1
- package/src/pages/product-details/bopis/find-in-store-modal.tsx +4 -4
- package/src/pages/product-details/bopis/pickup-option-select.tsx +2 -2
- package/src/pages/product-details/bopis/search-header.tsx +2 -2
- package/src/pages/product-details/product-intro/compound/context.ts +8 -3
- package/src/pages/product-details/product-intro/compound/product-info.tsx +25 -19
- package/src/pages/product-list/product-list-view.tsx +2 -1
- package/src/providers/nav-provider/index.tsx +1 -1
- package/src/providers/nav-provider/utils/createLink.ts +1 -1
- package/tsconfig.json +20 -15
- package/__mocks__/countries.ts +0 -11
- package/__mocks__/create-mutation.ts +0 -68
- package/__mocks__/create-query.ts +0 -94
- package/__mocks__/data/categories.json +0 -795
- package/__mocks__/get-product-by-id.ts +0 -6
- package/__mocks__/next-auth-react.ts +0 -9
- package/__mocks__/next-font.js +0 -4
- package/__mocks__/next-headers.js +0 -13
- package/__mocks__/next-image.tsx +0 -18
- package/__mocks__/next-link.js +0 -19
- package/__mocks__/next-navigation.ts +0 -29
- package/__mocks__/product-dimensions.ts +0 -635
- package/__mocks__/products.ts +0 -533
- package/__mocks__/query-client.ts +0 -3
- package/__mocks__/wishlists.json +0 -408
- package/src/auth/msal.ts +0 -68
- package/src/cms/blog.ts +0 -229
- package/src/cms/interfaces/navigation.ts +0 -52
- package/src/cms/interfaces/product-data.ts +0 -83
- package/src/cms/pages.ts +0 -149
- package/src/pages/account/profile/profile-button.test.tsx +0 -59
- package/src/pages/account/profile/profile-button.tsx +0 -51
|
@@ -46,12 +46,30 @@ import type {
|
|
|
46
46
|
type IdTokenClaims = jwt.JwtPayload & {
|
|
47
47
|
emails?: string[];
|
|
48
48
|
email?: string;
|
|
49
|
+
preferred_username?: string;
|
|
50
|
+
upn?: string;
|
|
51
|
+
unique_name?: string;
|
|
49
52
|
given_name?: string;
|
|
50
53
|
family_name?: string;
|
|
51
54
|
};
|
|
52
55
|
|
|
56
|
+
function extractEmailFromClaims(token: IdTokenClaims): string | undefined {
|
|
57
|
+
return [
|
|
58
|
+
Array.isArray(token.emails) ? token.emails[0] : undefined,
|
|
59
|
+
token.email,
|
|
60
|
+
token.preferred_username,
|
|
61
|
+
token.upn,
|
|
62
|
+
token.unique_name,
|
|
63
|
+
]
|
|
64
|
+
.map((v) => (typeof v === "string" ? v.trim() : ""))
|
|
65
|
+
.find((v) => v.includes("@"));
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
function decodeIdTokenClaims(token: string): IdTokenClaims {
|
|
54
69
|
const decoded = jwt.decode(token);
|
|
70
|
+
if (!decoded || typeof decoded !== "object") {
|
|
71
|
+
return {} as IdTokenClaims;
|
|
72
|
+
}
|
|
55
73
|
return decoded as IdTokenClaims;
|
|
56
74
|
}
|
|
57
75
|
|
|
@@ -63,39 +81,89 @@ class D365UserDatasource extends D365DatasourceBase implements UserApi {
|
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
async updateUser(user: User): Promise<User> {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
return tracer.startActiveSpan("d365.updateUser", async (span) => {
|
|
85
|
+
try {
|
|
86
|
+
const context = this.context;
|
|
87
|
+
|
|
88
|
+
let existing: Customer;
|
|
89
|
+
try {
|
|
90
|
+
existing = await readAsync(this.getRetailContext(context), "");
|
|
91
|
+
} catch {
|
|
92
|
+
const token = decodeIdTokenClaims(
|
|
93
|
+
context.requestContext.user.token || "",
|
|
94
|
+
);
|
|
95
|
+
const customerEmail = user.Email || extractEmailFromClaims(token);
|
|
96
|
+
|
|
97
|
+
if (!customerEmail) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
"Cannot create D365 customer: no email provided in payload or token claims.",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
existing = await createCustomerAsync(this.getRetailContext(context), {
|
|
104
|
+
AccountNumber: "",
|
|
105
|
+
Email: customerEmail,
|
|
106
|
+
FirstName: user.FirstName || token.given_name,
|
|
107
|
+
LastName: user.LastName || token.family_name || ".",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (user.FirstName !== undefined) existing.FirstName = user.FirstName;
|
|
112
|
+
if (user.LastName !== undefined) existing.LastName = user.LastName;
|
|
113
|
+
|
|
114
|
+
if (user.DefaultShippingAddressId !== undefined) {
|
|
115
|
+
existing.Addresses ??= [];
|
|
116
|
+
existing.Addresses = existing.Addresses.map((addr) => ({
|
|
117
|
+
...addr,
|
|
118
|
+
IsPrimary:
|
|
119
|
+
addr.RecordId?.toString() === user.DefaultShippingAddressId,
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
const propKey = "DefaultShippingAddressId";
|
|
123
|
+
existing.ExtensionProperties = existing.ExtensionProperties ?? [];
|
|
124
|
+
existing.ExtensionProperties = existing.ExtensionProperties.filter(
|
|
125
|
+
(p: CommerceProperty) => p.Key !== propKey,
|
|
126
|
+
);
|
|
127
|
+
existing.ExtensionProperties.push({
|
|
128
|
+
Key: propKey,
|
|
129
|
+
Value: {
|
|
130
|
+
StringValue: user.DefaultShippingAddressId,
|
|
131
|
+
} as NonNullable<CommerceProperty["Value"]>,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const updated: Customer = await updateAsync(
|
|
136
|
+
this.getRetailContext(context),
|
|
137
|
+
existing,
|
|
138
|
+
);
|
|
139
|
+
const mappedAddresses = (updated.Addresses ?? []).map((a) => ({
|
|
140
|
+
...a,
|
|
141
|
+
Id: a.RecordId?.toString() ?? "",
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
span.setAttributes({
|
|
145
|
+
"d365.update_user.account_number": updated.AccountNumber ?? "",
|
|
146
|
+
"d365.update_user.first_name": updated.FirstName ?? "",
|
|
147
|
+
"d365.update_user.last_name": updated.LastName ?? "",
|
|
148
|
+
});
|
|
149
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...updated,
|
|
153
|
+
Addresses: mappedAddresses,
|
|
154
|
+
} as User;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
const message =
|
|
157
|
+
error instanceof Error ? error.message : JSON.stringify(error);
|
|
158
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message });
|
|
159
|
+
span.recordException(
|
|
160
|
+
error instanceof Error ? error : new Error(message),
|
|
161
|
+
);
|
|
162
|
+
throw new Error(`updateUser failed: ${message}`);
|
|
163
|
+
} finally {
|
|
164
|
+
span.end();
|
|
165
|
+
}
|
|
86
166
|
});
|
|
87
|
-
const updated: Customer = await updateAsync(
|
|
88
|
-
this.getRetailContext(context),
|
|
89
|
-
existing,
|
|
90
|
-
);
|
|
91
|
-
const mappedAddresses = (updated.Addresses ?? []).map((a) => ({
|
|
92
|
-
...a,
|
|
93
|
-
Id: a.RecordId?.toString() ?? "",
|
|
94
|
-
}));
|
|
95
|
-
return {
|
|
96
|
-
...updated,
|
|
97
|
-
Addresses: mappedAddresses,
|
|
98
|
-
} as User;
|
|
99
167
|
}
|
|
100
168
|
|
|
101
169
|
async getUser(): Promise<User | null> {
|
|
@@ -115,7 +183,7 @@ class D365UserDatasource extends D365DatasourceBase implements UserApi {
|
|
|
115
183
|
const token = decodeIdTokenClaims(context.requestContext.user.token);
|
|
116
184
|
customer = await createCustomerAsync(this.getRetailContext(context), {
|
|
117
185
|
AccountNumber: "",
|
|
118
|
-
Email:
|
|
186
|
+
Email: extractEmailFromClaims(token),
|
|
119
187
|
FirstName: token.given_name,
|
|
120
188
|
LastName: token.family_name || ".",
|
|
121
189
|
});
|
|
@@ -325,11 +393,15 @@ class D365UserDatasource extends D365DatasourceBase implements UserApi {
|
|
|
325
393
|
async (span) => {
|
|
326
394
|
const context = this.context;
|
|
327
395
|
const id = input.Id;
|
|
328
|
-
|
|
396
|
+
// Get catalogId from apiSettings, with fallback to default catalog ID (1)
|
|
397
|
+
// D365 requires a valid CatalogId when multiple catalogs feature is enabled
|
|
398
|
+
const catalogId =
|
|
399
|
+
this.context.requestContext.apiSettings.catalogId || 1;
|
|
329
400
|
|
|
330
401
|
const productListLines = input.ProductListLines.map((line) => ({
|
|
331
402
|
...line,
|
|
332
|
-
CatalogId
|
|
403
|
+
// Always set CatalogId - use from input if present, otherwise use the resolved catalogId
|
|
404
|
+
CatalogId: line.CatalogId || catalogId,
|
|
333
405
|
}));
|
|
334
406
|
console.log(productListLines);
|
|
335
407
|
try {
|
|
@@ -345,7 +417,7 @@ class D365UserDatasource extends D365DatasourceBase implements UserApi {
|
|
|
345
417
|
};
|
|
346
418
|
return wishlist;
|
|
347
419
|
} catch (error) {
|
|
348
|
-
throw new Error(`Error adding to wishlist: ${error}`);
|
|
420
|
+
throw new Error(`Error adding to wishlist: ${JSON.stringify(error)}`);
|
|
349
421
|
} finally {
|
|
350
422
|
span.end();
|
|
351
423
|
}
|
|
@@ -364,7 +436,7 @@ class D365UserDatasource extends D365DatasourceBase implements UserApi {
|
|
|
364
436
|
productListLines,
|
|
365
437
|
);
|
|
366
438
|
} catch (error) {
|
|
367
|
-
throw new Error(`Error removing from wishlist: ${error}`);
|
|
439
|
+
throw new Error(`Error removing from wishlist: ${JSON.stringify(error)}`);
|
|
368
440
|
}
|
|
369
441
|
}
|
|
370
442
|
|
|
@@ -4,6 +4,29 @@ import { cookies, headers as getHeaders } from "next/headers";
|
|
|
4
4
|
import { getLocale } from "next-intl/server";
|
|
5
5
|
import { auth } from "@/auth/better-auth";
|
|
6
6
|
|
|
7
|
+
function isJwtStillValid(token?: string): boolean {
|
|
8
|
+
if (!token) return false;
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
const parts = token.split(".");
|
|
12
|
+
if (parts.length !== 3) return false;
|
|
13
|
+
|
|
14
|
+
const payload = JSON.parse(
|
|
15
|
+
Buffer.from(parts[1] ?? "", "base64url").toString(),
|
|
16
|
+
) as { exp?: number };
|
|
17
|
+
|
|
18
|
+
if (typeof payload.exp !== "number") {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const nowInSeconds = Math.floor(Date.now() / 1000);
|
|
23
|
+
// small skew buffer to avoid edge-of-expiration failures
|
|
24
|
+
return payload.exp > nowInSeconds + 30;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
/**
|
|
8
31
|
* Retrieves the D365 context from the "D365Context" cookie, enriches it with the current session token,
|
|
9
32
|
* locale, and a new operation ID, and returns it as an `ICallerContext` object.
|
|
@@ -21,19 +44,30 @@ import { auth } from "@/auth/better-auth";
|
|
|
21
44
|
export async function getContextFromCookie(): Promise<ICallerContext> {
|
|
22
45
|
const cookieStore = await cookies();
|
|
23
46
|
const d365Context = cookieStore.get("D365Context");
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
const tokens = await auth.api.getAccessToken({
|
|
27
|
-
headers: await getHeaders(),
|
|
28
|
-
body: { providerId: "entra-external-id" },
|
|
29
|
-
});
|
|
30
|
-
accessToken = tokens?.idToken || tokens?.accessToken || "";
|
|
31
|
-
} catch {
|
|
32
|
-
// User may not be authenticated — continue with empty token
|
|
33
|
-
}
|
|
47
|
+
|
|
34
48
|
const locale = await getLocale();
|
|
35
49
|
if (d365Context) {
|
|
36
50
|
const context = JSON.parse(d365Context.value) as ICallerContext;
|
|
51
|
+
const existingToken = context.requestContext?.user?.token || "";
|
|
52
|
+
let sessionToken = "";
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const session = await auth.api.getSession({
|
|
56
|
+
headers: await getHeaders(),
|
|
57
|
+
});
|
|
58
|
+
sessionToken =
|
|
59
|
+
(session as { session?: { accessToken?: string } } | null)?.session
|
|
60
|
+
?.accessToken || "";
|
|
61
|
+
} catch {
|
|
62
|
+
// Fall back to the context cookie token when session lookup is unavailable.
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let accessToken = "";
|
|
66
|
+
if (isJwtStillValid(sessionToken)) {
|
|
67
|
+
accessToken = sessionToken;
|
|
68
|
+
} else if (isJwtStillValid(existingToken)) {
|
|
69
|
+
accessToken = existingToken;
|
|
70
|
+
}
|
|
37
71
|
context.requestContext.operationId = crypto.randomUUID();
|
|
38
72
|
context.requestContext.user = {
|
|
39
73
|
isAuthenticated: !!accessToken,
|
package/src/api-manager/index.ts
CHANGED
|
@@ -29,9 +29,9 @@ async function createApiManager(): Promise<Datasource> {
|
|
|
29
29
|
datasource = E4Datasource;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
// biome-ignore lint: api datasources should not be typed
|
|
32
33
|
const storefrontOverrides: { [key: string]: any } = {};
|
|
33
34
|
|
|
34
|
-
// @ts-expect-error
|
|
35
35
|
function importAll(r) {
|
|
36
36
|
r.keys().forEach((key: string) => {
|
|
37
37
|
storefrontOverrides[key] = r(key);
|
|
@@ -63,6 +63,7 @@ async function createApiManager(): Promise<Datasource> {
|
|
|
63
63
|
Object.assign(datasource[entityName] as object, entityOverrides);
|
|
64
64
|
} else {
|
|
65
65
|
// Non-core entity added entirely from overrides
|
|
66
|
+
// biome-ignore lint: api datasources should not be typed
|
|
66
67
|
(datasource as any)[entityName] = entityOverrides;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -5,6 +5,23 @@ import { GraphQLClient } from "graphql-request";
|
|
|
5
5
|
import { cookies, headers as getHeaders } from "next/headers";
|
|
6
6
|
import { auth } from "@/auth/better-auth";
|
|
7
7
|
|
|
8
|
+
const decodeJwtExpMs = (token?: string): number | null => {
|
|
9
|
+
if (!token) return null;
|
|
10
|
+
const parts = token.split(".");
|
|
11
|
+
if (parts.length !== 3 || !parts[1]) return null;
|
|
12
|
+
try {
|
|
13
|
+
const payload = JSON.parse(
|
|
14
|
+
Buffer.from(parts[1], "base64url").toString(),
|
|
15
|
+
) as {
|
|
16
|
+
exp?: number;
|
|
17
|
+
};
|
|
18
|
+
if (typeof payload.exp !== "number") return null;
|
|
19
|
+
return payload.exp * 1000;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
8
25
|
/**
|
|
9
26
|
* Initializes and returns a cached GraphQL client
|
|
10
27
|
* @param withAuth (optional) - If true, includes the authorization token
|
|
@@ -28,16 +45,27 @@ const getGraphQLClient = async (withAuth = false) => {
|
|
|
28
45
|
|
|
29
46
|
if (withAuth) {
|
|
30
47
|
try {
|
|
31
|
-
const
|
|
48
|
+
const session = await auth.api.getSession({
|
|
32
49
|
headers: await getHeaders(),
|
|
33
|
-
body: { providerId: "entra-external-id" },
|
|
34
50
|
});
|
|
35
|
-
const accessToken =
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
const accessToken =
|
|
52
|
+
(session as { session?: { accessToken?: string } } | null)?.session
|
|
53
|
+
?.accessToken || "";
|
|
54
|
+
if (!accessToken) {
|
|
55
|
+
throw new Error("Authentication required: provider token missing");
|
|
38
56
|
}
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
|
|
58
|
+
const expMs = decodeJwtExpMs(accessToken);
|
|
59
|
+
if (typeof expMs === "number" && expMs <= Date.now()) {
|
|
60
|
+
throw new Error("Authentication required: provider token expired");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
headers.Authorization = `Bearer ${accessToken}`;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// withAuth=true is an explicit contract that a valid token is required.
|
|
66
|
+
throw error instanceof Error
|
|
67
|
+
? error
|
|
68
|
+
: new Error("Authentication required: unable to resolve access token");
|
|
41
69
|
}
|
|
42
70
|
}
|
|
43
71
|
|
|
@@ -41,7 +41,7 @@ export const productSchema = z.object({
|
|
|
41
41
|
ItemId: z.string(), // sku?
|
|
42
42
|
RecordId: z.number(),
|
|
43
43
|
Name: z.string(),
|
|
44
|
-
Price: z.number()
|
|
44
|
+
Price: z.number(),
|
|
45
45
|
BasePrice: z.number().optional(),
|
|
46
46
|
AdjustedPrice: z.number().optional(),
|
|
47
47
|
Description: z.string().optional(),
|
|
@@ -11,6 +11,22 @@ import signOut from "@/auth/signout";
|
|
|
11
11
|
|
|
12
12
|
const QUERY_ROUTE = "/api";
|
|
13
13
|
|
|
14
|
+
class ApiManagerRequestError extends Error {
|
|
15
|
+
status: number;
|
|
16
|
+
path: ApiPath;
|
|
17
|
+
payload?: unknown;
|
|
18
|
+
|
|
19
|
+
constructor(status: number, path: ApiPath, payload?: unknown) {
|
|
20
|
+
super(
|
|
21
|
+
`API request failed for ${JSON.stringify(path)} with status ${status}`,
|
|
22
|
+
);
|
|
23
|
+
this.name = "ApiManagerRequestError";
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.path = path;
|
|
26
|
+
this.payload = payload;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
14
30
|
export type ApiPath<T extends keyof DatasourceApis = keyof DatasourceApis> = [
|
|
15
31
|
T,
|
|
16
32
|
keyof DatasourceApis[T],
|
|
@@ -33,10 +49,24 @@ const doFetch = async <TInput>(
|
|
|
33
49
|
body: JSON.stringify({ input, apiManagerPath }),
|
|
34
50
|
headers,
|
|
35
51
|
});
|
|
52
|
+
|
|
53
|
+
let payload: unknown;
|
|
54
|
+
try {
|
|
55
|
+
payload = await result.json();
|
|
56
|
+
} catch {
|
|
57
|
+
payload = undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
if (result.status === 401) {
|
|
37
61
|
signOut();
|
|
62
|
+
throw new ApiManagerRequestError(result.status, apiManagerPath, payload);
|
|
38
63
|
}
|
|
39
|
-
|
|
64
|
+
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
throw new ApiManagerRequestError(result.status, apiManagerPath, payload);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return payload;
|
|
40
70
|
};
|
|
41
71
|
|
|
42
72
|
// --- Factory ---
|
|
@@ -58,10 +88,13 @@ export const createQuery = <TInput, TOutput>(
|
|
|
58
88
|
queryFn: isServer
|
|
59
89
|
? async () => {
|
|
60
90
|
const apiManager = await createApiManager();
|
|
61
|
-
return getApiFromPath
|
|
91
|
+
return getApiFromPath<TInput, TOutput>(
|
|
92
|
+
apiManager,
|
|
93
|
+
apiManagerPath,
|
|
94
|
+
)(input);
|
|
62
95
|
}
|
|
63
96
|
: async () => {
|
|
64
|
-
return await doFetch(input, apiManagerPath);
|
|
97
|
+
return (await doFetch(input, apiManagerPath)) as TOutput;
|
|
65
98
|
},
|
|
66
99
|
enabled: config?.disableProperties
|
|
67
100
|
? config.disableProperties.every((prop) => !!input?.[prop])
|
|
@@ -10,6 +10,16 @@ function makeQueryClient() {
|
|
|
10
10
|
defaultOptions: {
|
|
11
11
|
queries: {
|
|
12
12
|
staleTime: 60 * 1000,
|
|
13
|
+
refetchOnWindowFocus: false,
|
|
14
|
+
retry: (failureCount, error) => {
|
|
15
|
+
const status =
|
|
16
|
+
typeof error === "object" && error !== null && "status" in error
|
|
17
|
+
? Number((error as { status?: number }).status)
|
|
18
|
+
: undefined;
|
|
19
|
+
|
|
20
|
+
if (status === 401) return false;
|
|
21
|
+
return failureCount < 1;
|
|
22
|
+
},
|
|
13
23
|
},
|
|
14
24
|
dehydrate: {
|
|
15
25
|
serializeData: (data) => JSON.stringify(data),
|