@decocms/apps-vtex 8.0.0 → 8.1.0-next.0
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/package.json +5 -5
- package/src/__conformance__/upstream.test.ts +158 -0
- package/src/__tests__/client-set-cookie-forward.test.ts +4 -4
- package/src/client.ts +61 -4
- package/src/index.ts +17 -2
- package/src/loaders/__tests__/legacyProductList.test.ts +157 -0
- package/src/loaders/autocomplete.ts +2 -4
- package/src/loaders/intelligentSearch/productDetailsPage.ts +2 -4
- package/src/loaders/intelligentSearch/productList.ts +21 -5
- package/src/loaders/intelligentSearch/productListingPage.ts +2 -3
- package/src/loaders/intelligentSearch/suggestions.ts +2 -4
- package/src/loaders/legacy/relatedProductsLoader.ts +2 -4
- package/src/loaders/legacy.ts +77 -2
- package/src/loaders/productListFull.ts +2 -4
- package/src/loaders/workflow/products.ts +2 -4
- package/src/middleware.cacheHeaders.test.ts +44 -0
- package/src/mod.ts +18 -2
- package/src/utils/__tests__/cookieSanitizer.test.ts +1 -1
- package/src/utils/__tests__/storefrontBaseUrl.test.ts +61 -0
- package/src/utils/__tests__/transform.test.ts +349 -7
- package/src/utils/buildOfferShelf.test.ts +59 -0
- package/src/utils/enrichment.ts +3 -7
- package/src/utils/fetch.ts +5 -1
- package/src/utils/fetchCache.ts +24 -168
- package/src/utils/instrumentedFetch.ts +2 -1
- package/src/utils/minicart.ts +1 -1
- package/src/utils/operationRouter.ts +3 -9
- package/src/utils/proxy.ts +64 -4
- package/src/utils/similars.ts +2 -4
- package/src/utils/sitemap.ts +3 -2
- package/src/utils/transform.ts +222 -53
- package/src/utils/vtexId.ts +15 -2
- package/src/vtexClient.test.ts +204 -0
- package/src/vtexClient.ts +433 -0
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The VTEX upstream client: typed request functions over the framework's
|
|
3
|
+
* instrumented fetch (`@decocms/blocks/fetch`). See /next/upstream-clients.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { createVtexClient } from "@decocms/apps-vtex";
|
|
7
|
+
*
|
|
8
|
+
* export const vtex = createVtexClient({
|
|
9
|
+
* account: process.env.VTEX_ACCOUNT!,
|
|
10
|
+
* appKey: process.env.VTEX_APP_KEY!,
|
|
11
|
+
* appToken: process.env.VTEX_APP_TOKEN!,
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* const result = await vtex.search.products({ query: "linen shirt", count: 12 });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Thin by design: it returns VTEX's own types, takes every setting and every
|
|
18
|
+
* per-shopper input (cookies, region) as arguments, and never reads the
|
|
19
|
+
* incoming request, caches, or converts. Converters, hooks, cart/session/sign-in
|
|
20
|
+
* flows and loaders live in the VTEX platform template and site code; upstream
|
|
21
|
+
* caching lives in the framework binding (/next/caching#upstream-data), keyed
|
|
22
|
+
* by everything a response depends on, such as `regionId`.
|
|
23
|
+
*
|
|
24
|
+
* Retries and a circuit breaker are ON by default for VTEX (and only for
|
|
25
|
+
* VTEX). Retries apply to idempotent requests only, so a checkout POST is
|
|
26
|
+
* never repeated. Pass `retry: false` / `circuitBreaker: false` to turn them off.
|
|
27
|
+
*/
|
|
28
|
+
import { createInstrumentedFetch } from "@decocms/blocks/fetch";
|
|
29
|
+
import { DEFAULT_RESILIENCE_CONFIG } from "./utils/constants";
|
|
30
|
+
import { sanitizeOutboundCookieHeader } from "./utils/cookieSanitizer";
|
|
31
|
+
import { vtexOperationRouter } from "./utils/operationRouter";
|
|
32
|
+
import type {
|
|
33
|
+
Category,
|
|
34
|
+
FacetSearchResult,
|
|
35
|
+
Fuzzy,
|
|
36
|
+
LegacyProduct,
|
|
37
|
+
LegacySort,
|
|
38
|
+
CrossSellingType,
|
|
39
|
+
OrderForm,
|
|
40
|
+
OrderFormItemInput,
|
|
41
|
+
PageType,
|
|
42
|
+
ProductSearchResult,
|
|
43
|
+
SelectedFacet,
|
|
44
|
+
Session,
|
|
45
|
+
SimulationBehavior,
|
|
46
|
+
SimulationOrderForm,
|
|
47
|
+
Sort,
|
|
48
|
+
Suggestion,
|
|
49
|
+
} from "./utils/types";
|
|
50
|
+
|
|
51
|
+
export interface VtexClientConfig {
|
|
52
|
+
/** The VTEX account name, e.g. `mystore`. */
|
|
53
|
+
account: string;
|
|
54
|
+
/** Sent as `X-VTEX-API-AppKey` when set together with `appToken`. */
|
|
55
|
+
appKey?: string;
|
|
56
|
+
/** Sent as `X-VTEX-API-AppToken` when set together with `appKey`. */
|
|
57
|
+
appToken?: string;
|
|
58
|
+
/** @default "vtexcommercestable" */
|
|
59
|
+
environment?: string;
|
|
60
|
+
/** @default "com.br" */
|
|
61
|
+
domain?: string;
|
|
62
|
+
/** Default sales channel (`sc`), overridable per call where VTEX accepts one. */
|
|
63
|
+
salesChannel?: string;
|
|
64
|
+
/** Default Intelligent Search locale, e.g. `pt-BR`. */
|
|
65
|
+
locale?: string;
|
|
66
|
+
/** The fetch underneath; for tests. Defaults to `globalThis.fetch`. */
|
|
67
|
+
fetch?: typeof fetch;
|
|
68
|
+
/** On by default (2 retries, 150ms backoff); idempotent requests only. `false` turns it off. */
|
|
69
|
+
retry?: { attempts: number; backoffMs?: number } | false;
|
|
70
|
+
/** On by default (opens after 5 failures, for 5s). `false` turns it off. */
|
|
71
|
+
circuitBreaker?: { failures: number; cooldownMs: number } | false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const VTEX_DEFAULT_RETRY = {
|
|
75
|
+
attempts: DEFAULT_RESILIENCE_CONFIG.maxRetries,
|
|
76
|
+
backoffMs: DEFAULT_RESILIENCE_CONFIG.backoffBaseMs,
|
|
77
|
+
} as const;
|
|
78
|
+
|
|
79
|
+
const VTEX_DEFAULT_CIRCUIT_BREAKER = {
|
|
80
|
+
failures: DEFAULT_RESILIENCE_CONFIG.breakerConsecutiveFailures,
|
|
81
|
+
cooldownMs: DEFAULT_RESILIENCE_CONFIG.breakerOpenCooldownMs,
|
|
82
|
+
} as const;
|
|
83
|
+
|
|
84
|
+
/** Per-call inputs. Nothing is read from the incoming request. */
|
|
85
|
+
export interface VtexRequestOptions {
|
|
86
|
+
/** The shopper's `Cookie` header to forward (non-ASCII and malformed pairs are dropped). */
|
|
87
|
+
cookie?: string;
|
|
88
|
+
signal?: AbortSignal;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A shopper-scoped response: the body plus VTEX's `Set-Cookie` values, unmodified, for the caller to forward. */
|
|
92
|
+
export interface VtexResponse<T> {
|
|
93
|
+
data: T;
|
|
94
|
+
setCookies: string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A failed VTEX call. Carries the operation and the status, never a body, URL, token or cookie. */
|
|
98
|
+
export class VtexError extends Error {
|
|
99
|
+
constructor(
|
|
100
|
+
readonly operation: string,
|
|
101
|
+
readonly status: number,
|
|
102
|
+
) {
|
|
103
|
+
super(`vtex ${operation} failed with HTTP ${status}`);
|
|
104
|
+
this.name = "VtexError";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Intelligent Search arguments, mirroring its query parameters. */
|
|
109
|
+
export interface VtexSearchArgs extends VtexRequestOptions {
|
|
110
|
+
query?: string;
|
|
111
|
+
/** Selected facets, sent as the path (`category-1/shirts/brand/acme`). */
|
|
112
|
+
facets?: SelectedFacet[];
|
|
113
|
+
/** Intelligent Search's 1-based page. */
|
|
114
|
+
page?: number;
|
|
115
|
+
count?: number;
|
|
116
|
+
sort?: Sort;
|
|
117
|
+
fuzzy?: Fuzzy;
|
|
118
|
+
locale?: string;
|
|
119
|
+
hideUnavailableItems?: boolean;
|
|
120
|
+
simulationBehavior?: SimulationBehavior;
|
|
121
|
+
/** The shopper's region (from the `vtex_segment` cookie); part of the cache key. */
|
|
122
|
+
regionId?: string;
|
|
123
|
+
salesChannel?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Legacy Catalog search arguments (`/api/catalog_system/pub/products/search`). */
|
|
127
|
+
export interface VtexCatalogSearchArgs extends VtexRequestOptions {
|
|
128
|
+
/** Path term, e.g. a category path. */
|
|
129
|
+
term?: string;
|
|
130
|
+
/** Full-text query (`ft`). */
|
|
131
|
+
ft?: string;
|
|
132
|
+
/** Filter queries (`fq`), e.g. `productId:123`. */
|
|
133
|
+
fq?: string[];
|
|
134
|
+
/** `_from`, 0-based inclusive. */
|
|
135
|
+
from?: number;
|
|
136
|
+
/** `_to`, 0-based inclusive. */
|
|
137
|
+
to?: number;
|
|
138
|
+
/** `O`. */
|
|
139
|
+
sort?: LegacySort;
|
|
140
|
+
salesChannel?: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface CallInit extends VtexRequestOptions {
|
|
144
|
+
method?: string;
|
|
145
|
+
body?: unknown;
|
|
146
|
+
headers?: Record<string, string>;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Encodes a caller-supplied path (e.g. the shopper's URL path) segment by
|
|
151
|
+
* segment, so it can't climb out of its endpoint (`..`, `%2e%2e`) or inject a
|
|
152
|
+
* query (`?`, `#`) while the app credentials are attached.
|
|
153
|
+
*/
|
|
154
|
+
function pathSegments(path: string): string {
|
|
155
|
+
return path
|
|
156
|
+
.split("/")
|
|
157
|
+
.filter((segment) => segment !== "")
|
|
158
|
+
.map((segment) => {
|
|
159
|
+
if (/^(\.|%2e){1,2}$/i.test(segment)) throw new Error("vtex: invalid path segment");
|
|
160
|
+
return encodeURIComponent(segment);
|
|
161
|
+
})
|
|
162
|
+
.join("/");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createVtexClient(config: VtexClientConfig) {
|
|
166
|
+
const host = `${config.account}.${config.environment ?? "vtexcommercestable"}.${config.domain ?? "com.br"}`;
|
|
167
|
+
const base = `https://${host}`;
|
|
168
|
+
const request = createInstrumentedFetch({
|
|
169
|
+
provider: "vtex",
|
|
170
|
+
fetch: config.fetch,
|
|
171
|
+
retry: config.retry === false ? undefined : (config.retry ?? VTEX_DEFAULT_RETRY),
|
|
172
|
+
circuitBreaker:
|
|
173
|
+
config.circuitBreaker === false
|
|
174
|
+
? undefined
|
|
175
|
+
: (config.circuitBreaker ?? VTEX_DEFAULT_CIRCUIT_BREAKER),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
async function call(url: string | URL, init: CallInit = {}): Promise<Response> {
|
|
179
|
+
const method = (init.method ?? "GET").toUpperCase();
|
|
180
|
+
const operation = vtexOperationRouter(String(url), method) ?? "unknown";
|
|
181
|
+
const headers = new Headers({ accept: "application/json", ...init.headers });
|
|
182
|
+
if (init.body !== undefined) headers.set("content-type", "application/json");
|
|
183
|
+
// Shopper-scoped calls go out as the shopper only: with app credentials
|
|
184
|
+
// VTEX would answer an orderForm with the profile data unmasked.
|
|
185
|
+
if (config.appKey && config.appToken && !init.cookie) {
|
|
186
|
+
headers.set("x-vtex-api-appkey", config.appKey);
|
|
187
|
+
headers.set("x-vtex-api-apptoken", config.appToken);
|
|
188
|
+
}
|
|
189
|
+
if (init.cookie) {
|
|
190
|
+
const { cookies } = sanitizeOutboundCookieHeader(init.cookie);
|
|
191
|
+
if (cookies) headers.set("cookie", cookies);
|
|
192
|
+
}
|
|
193
|
+
const response = await request(url, {
|
|
194
|
+
method,
|
|
195
|
+
headers,
|
|
196
|
+
body: init.body === undefined ? undefined : JSON.stringify(init.body),
|
|
197
|
+
signal: init.signal,
|
|
198
|
+
operation,
|
|
199
|
+
});
|
|
200
|
+
if (!response.ok) {
|
|
201
|
+
void response.body?.cancel().catch(() => {});
|
|
202
|
+
throw new VtexError(operation, response.status);
|
|
203
|
+
}
|
|
204
|
+
return response;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Only the per-call request options, never the operation's other arguments. */
|
|
208
|
+
const pick = (options: VtexRequestOptions = {}): VtexRequestOptions => ({
|
|
209
|
+
cookie: options.cookie,
|
|
210
|
+
signal: options.signal,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const json = async <T>(url: string | URL, init?: CallInit): Promise<T> =>
|
|
214
|
+
(await call(url, init)).json() as Promise<T>;
|
|
215
|
+
|
|
216
|
+
const withCookies = async <T>(url: string | URL, init?: CallInit): Promise<VtexResponse<T>> => {
|
|
217
|
+
const response = await call(url, init);
|
|
218
|
+
const setCookies =
|
|
219
|
+
typeof response.headers.getSetCookie === "function" ? response.headers.getSetCookie() : [];
|
|
220
|
+
return { data: (await response.json()) as T, setCookies };
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const url = (path: string, params: Record<string, string | number | boolean | undefined> = {}) => {
|
|
224
|
+
const result = new URL(path, base);
|
|
225
|
+
for (const [key, value] of Object.entries(params)) {
|
|
226
|
+
if (value !== undefined && value !== "") result.searchParams.set(key, String(value));
|
|
227
|
+
}
|
|
228
|
+
return result;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const isUrl = (endpoint: string, args: VtexSearchArgs) => {
|
|
232
|
+
const facetPath = (args.facets ?? [])
|
|
233
|
+
.map(({ key, value }) =>
|
|
234
|
+
key ? `${encodeURIComponent(key)}/${encodeURIComponent(value)}` : encodeURIComponent(value),
|
|
235
|
+
)
|
|
236
|
+
.join("/");
|
|
237
|
+
return url(`/api/io/_v/api/intelligent-search/${endpoint}/${facetPath}`, {
|
|
238
|
+
query: args.query,
|
|
239
|
+
page: args.page,
|
|
240
|
+
count: args.count,
|
|
241
|
+
sort: args.sort,
|
|
242
|
+
fuzzy: args.fuzzy,
|
|
243
|
+
locale: args.locale ?? config.locale,
|
|
244
|
+
hideUnavailableItems: args.hideUnavailableItems,
|
|
245
|
+
simulationBehavior: args.simulationBehavior,
|
|
246
|
+
regionId: args.regionId,
|
|
247
|
+
sc: args.salesChannel ?? config.salesChannel,
|
|
248
|
+
});
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const orderFormPath = (orderFormId: string, rest = "") =>
|
|
252
|
+
`/api/checkout/pub/orderForm/${encodeURIComponent(orderFormId)}${rest}`;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
/** Intelligent Search. */
|
|
256
|
+
search: {
|
|
257
|
+
products: (args: VtexSearchArgs = {}) =>
|
|
258
|
+
json<ProductSearchResult>(isUrl("product_search", args), pick(args)),
|
|
259
|
+
facets: (args: VtexSearchArgs = {}) => json<FacetSearchResult>(isUrl("facets", args), pick(args)),
|
|
260
|
+
suggestions: (args: VtexSearchArgs) =>
|
|
261
|
+
json<Suggestion>(isUrl("search_suggestions", { ...args, facets: [] }), pick(args)),
|
|
262
|
+
autocomplete: (args: VtexSearchArgs) =>
|
|
263
|
+
json<Suggestion>(isUrl("autocomplete_suggestions", { ...args, facets: [] }), pick(args)),
|
|
264
|
+
topSearches: (args: VtexSearchArgs = {}) =>
|
|
265
|
+
json<Suggestion>(isUrl("top_searches", { ...args, facets: [] }), pick(args)),
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
/** Legacy Catalog API. */
|
|
269
|
+
catalog: {
|
|
270
|
+
pageType: async (path: string, options?: VtexRequestOptions) =>
|
|
271
|
+
json<PageType>(url(`/api/catalog_system/pub/portal/pagetype/${pathSegments(path)}`), options),
|
|
272
|
+
products: async (args: VtexCatalogSearchArgs = {}) => {
|
|
273
|
+
const target = url(
|
|
274
|
+
`/api/catalog_system/pub/products/search/${pathSegments(args.term ?? "")}`,
|
|
275
|
+
{
|
|
276
|
+
ft: args.ft,
|
|
277
|
+
_from: args.from,
|
|
278
|
+
_to: args.to,
|
|
279
|
+
O: args.sort,
|
|
280
|
+
sc: args.salesChannel ?? config.salesChannel,
|
|
281
|
+
},
|
|
282
|
+
);
|
|
283
|
+
for (const fq of args.fq ?? []) target.searchParams.append("fq", fq);
|
|
284
|
+
return json<LegacyProduct[]>(target, pick(args));
|
|
285
|
+
},
|
|
286
|
+
crossSelling: (type: CrossSellingType, productId: string, options?: VtexRequestOptions) =>
|
|
287
|
+
json<LegacyProduct[]>(
|
|
288
|
+
url(
|
|
289
|
+
`/api/catalog_system/pub/products/crossselling/${type}/${encodeURIComponent(productId)}`,
|
|
290
|
+
),
|
|
291
|
+
options,
|
|
292
|
+
),
|
|
293
|
+
categoryTree: (levels = 3, options?: VtexRequestOptions) =>
|
|
294
|
+
json<Category[]>(url(`/api/catalog_system/pub/category/tree/${levels}`), options),
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
/** Checkout API. Cart calls are shopper-scoped and return VTEX's `Set-Cookie` values. */
|
|
298
|
+
checkout: {
|
|
299
|
+
/**
|
|
300
|
+
* Gets the shopper's cart from their `checkout.vtex.com` cookie, or creates one.
|
|
301
|
+
* Every section by default; `sections` narrows it (`expectedOrderFormSections`).
|
|
302
|
+
*/
|
|
303
|
+
orderForm: (options: VtexRequestOptions & { sections?: string[] } = {}) =>
|
|
304
|
+
withCookies<OrderForm>(url("/api/checkout/pub/orderForm"), {
|
|
305
|
+
...pick(options),
|
|
306
|
+
method: "POST",
|
|
307
|
+
body: options.sections ? { expectedOrderFormSections: options.sections } : {},
|
|
308
|
+
}),
|
|
309
|
+
addItems: (orderFormId: string, orderItems: OrderFormItemInput[], options?: VtexRequestOptions) =>
|
|
310
|
+
withCookies<OrderForm>(url(orderFormPath(orderFormId, "/items")), {
|
|
311
|
+
...options,
|
|
312
|
+
method: "POST",
|
|
313
|
+
body: { orderItems },
|
|
314
|
+
}),
|
|
315
|
+
updateItems: (
|
|
316
|
+
orderFormId: string,
|
|
317
|
+
orderItems: { index: number; quantity: number }[],
|
|
318
|
+
options?: VtexRequestOptions,
|
|
319
|
+
) =>
|
|
320
|
+
withCookies<OrderForm>(url(orderFormPath(orderFormId, "/items/update")), {
|
|
321
|
+
...options,
|
|
322
|
+
method: "POST",
|
|
323
|
+
body: { orderItems },
|
|
324
|
+
}),
|
|
325
|
+
addCoupon: (orderFormId: string, text: string, options?: VtexRequestOptions) =>
|
|
326
|
+
withCookies<OrderForm>(url(orderFormPath(orderFormId, "/coupons")), {
|
|
327
|
+
...options,
|
|
328
|
+
method: "POST",
|
|
329
|
+
body: { text },
|
|
330
|
+
}),
|
|
331
|
+
attachment: (
|
|
332
|
+
orderFormId: string,
|
|
333
|
+
name: string,
|
|
334
|
+
body: Record<string, unknown>,
|
|
335
|
+
options?: VtexRequestOptions,
|
|
336
|
+
) =>
|
|
337
|
+
withCookies<OrderForm>(
|
|
338
|
+
url(orderFormPath(orderFormId, `/attachments/${encodeURIComponent(name)}`)),
|
|
339
|
+
{ ...options, method: "POST", body },
|
|
340
|
+
),
|
|
341
|
+
simulation: (
|
|
342
|
+
body: {
|
|
343
|
+
items: { id: string; quantity: number; seller: string }[];
|
|
344
|
+
postalCode?: string;
|
|
345
|
+
country?: string;
|
|
346
|
+
},
|
|
347
|
+
options: VtexRequestOptions & { salesChannel?: string } = {},
|
|
348
|
+
) =>
|
|
349
|
+
json<SimulationOrderForm>(
|
|
350
|
+
url("/api/checkout/pub/orderForms/simulation", {
|
|
351
|
+
sc: options.salesChannel ?? config.salesChannel,
|
|
352
|
+
}),
|
|
353
|
+
{ ...pick(options), method: "POST", body },
|
|
354
|
+
),
|
|
355
|
+
/** The region (and its sellers) serving a postal code; its `id` is the `regionId`. */
|
|
356
|
+
regions: (
|
|
357
|
+
args: { postalCode: string; country: string; salesChannel?: string },
|
|
358
|
+
options?: VtexRequestOptions,
|
|
359
|
+
) =>
|
|
360
|
+
json<{ id: string; sellers: { id: string; name: string }[] }[]>(
|
|
361
|
+
url("/api/checkout/pub/regions", {
|
|
362
|
+
postalCode: args.postalCode,
|
|
363
|
+
country: args.country,
|
|
364
|
+
sc: args.salesChannel ?? config.salesChannel,
|
|
365
|
+
}),
|
|
366
|
+
options,
|
|
367
|
+
),
|
|
368
|
+
},
|
|
369
|
+
|
|
370
|
+
/** Session API. Shopper-scoped; returns VTEX's `Set-Cookie` values. */
|
|
371
|
+
sessions: {
|
|
372
|
+
get: (options: VtexRequestOptions & { items?: string[] } = {}) =>
|
|
373
|
+
withCookies<Session>(
|
|
374
|
+
url("/api/sessions", { items: options.items?.join(",") }),
|
|
375
|
+
pick(options),
|
|
376
|
+
),
|
|
377
|
+
update: (publicProps: Record<string, { value: string }>, options?: VtexRequestOptions) =>
|
|
378
|
+
withCookies<{ id: string; sessionToken?: string }>(url("/api/sessions"), {
|
|
379
|
+
...options,
|
|
380
|
+
method: "POST",
|
|
381
|
+
body: { public: publicProps },
|
|
382
|
+
}),
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
/** VTEX IO's private GraphQL endpoint (`{account}.myvtex.com`). */
|
|
386
|
+
io: {
|
|
387
|
+
graphql: async <T>(
|
|
388
|
+
body: { query: string; variables?: Record<string, unknown>; operationName?: string },
|
|
389
|
+
options?: VtexRequestOptions,
|
|
390
|
+
): Promise<T> => {
|
|
391
|
+
const result = await json<{ data: T; errors?: unknown[] }>(
|
|
392
|
+
`https://${config.account}.myvtex.com/_v/private/graphql/v1`,
|
|
393
|
+
{ ...options, method: "POST", body },
|
|
394
|
+
);
|
|
395
|
+
// GraphQL errors arrive with HTTP 200; their messages can echo inputs, so they stay out.
|
|
396
|
+
if (result.errors?.length) throw new VtexError("io.graphql", 200);
|
|
397
|
+
return result.data;
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
/** Master Data v1. */
|
|
402
|
+
masterdata: {
|
|
403
|
+
search: <T>(
|
|
404
|
+
entity: string,
|
|
405
|
+
args: VtexRequestOptions & {
|
|
406
|
+
where?: string;
|
|
407
|
+
fields?: string[];
|
|
408
|
+
sort?: string;
|
|
409
|
+
from?: number;
|
|
410
|
+
to?: number;
|
|
411
|
+
} = {},
|
|
412
|
+
) =>
|
|
413
|
+
json<T[]>(
|
|
414
|
+
url(`/api/dataentities/${encodeURIComponent(entity)}/search`, {
|
|
415
|
+
_where: args.where,
|
|
416
|
+
_fields: args.fields?.join(","),
|
|
417
|
+
_sort: args.sort,
|
|
418
|
+
}),
|
|
419
|
+
{
|
|
420
|
+
...pick(args),
|
|
421
|
+
headers: { "rest-range": `resources=${args.from ?? 0}-${args.to ?? 10}` },
|
|
422
|
+
},
|
|
423
|
+
),
|
|
424
|
+
create: (entity: string, document: Record<string, unknown>, options?: VtexRequestOptions) =>
|
|
425
|
+
json<{ Id: string; Href: string; DocumentId: string }>(
|
|
426
|
+
url(`/api/dataentities/${encodeURIComponent(entity)}/documents`),
|
|
427
|
+
{ ...options, method: "POST", body: document },
|
|
428
|
+
),
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export type VtexClient = ReturnType<typeof createVtexClient>;
|