@decocms/apps-magento 8.1.0-next.0 → 8.1.0-next.2

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.
Files changed (47) hide show
  1. package/dist/index.d.ts +2 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +3 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/magentoClient.d.ts +62 -0
  6. package/dist/magentoClient.d.ts.map +1 -0
  7. package/dist/magentoClient.js +96 -0
  8. package/dist/magentoClient.js.map +1 -0
  9. package/package.json +19 -27
  10. package/src/index.ts +2 -29
  11. package/src/README.md +0 -65
  12. package/src/__tests__/cart.test.ts +0 -139
  13. package/src/__tests__/client.test.ts +0 -252
  14. package/src/__tests__/features.test.ts +0 -51
  15. package/src/__tests__/graphql.test.ts +0 -183
  16. package/src/__tests__/magentoClient.test.ts +0 -166
  17. package/src/__tests__/newsletter-subscribe.test.ts +0 -86
  18. package/src/__tests__/product-stockAlert.test.ts +0 -80
  19. package/src/__tests__/stringifySearchCriteria.test.ts +0 -56
  20. package/src/__tests__/transform.test.ts +0 -346
  21. package/src/__tests__/user-loader.test.ts +0 -134
  22. package/src/__tests__/wishlist-actions.test.ts +0 -140
  23. package/src/__tests__/wishlist-loader.test.ts +0 -97
  24. package/src/actions/newsletter/subscribe.ts +0 -40
  25. package/src/actions/product/stockAlert.ts +0 -67
  26. package/src/actions/wishlist/addItem.ts +0 -57
  27. package/src/actions/wishlist/removeItem.ts +0 -59
  28. package/src/client.ts +0 -252
  29. package/src/loaders/cart.ts +0 -64
  30. package/src/loaders/features.ts +0 -16
  31. package/src/loaders/user.ts +0 -60
  32. package/src/loaders/wishlist.ts +0 -29
  33. package/src/middleware.ts +0 -19
  34. package/src/types.ts +0 -51
  35. package/src/utils/__tests__/instrumentation.test.ts +0 -59
  36. package/src/utils/cacheTimeControl.ts +0 -69
  37. package/src/utils/client/types.ts +0 -270
  38. package/src/utils/constants.ts +0 -126
  39. package/src/utils/fetchCache.ts +0 -55
  40. package/src/utils/graphql-types.ts +0 -99
  41. package/src/utils/graphql.ts +0 -155
  42. package/src/utils/instrumentedFetch.ts +0 -57
  43. package/src/utils/operationRouter.ts +0 -43
  44. package/src/utils/stringifySearchCriteria.ts +0 -62
  45. package/src/utils/transform.ts +0 -283
  46. package/src/utils/user.ts +0 -16
  47. package/tsconfig.json +0 -7
@@ -1,57 +0,0 @@
1
- /**
2
- * Pre-wired instrumented fetch factory for Magento.
3
- *
4
- * Mirrors `vtex/utils/instrumentedFetch.ts` and `shopify/utils/instrumentedFetch.ts`.
5
- * Bundles:
6
- *
7
- * 1. `createInstrumentedFetch` from `@decocms/blocks/sdk/instrumentedFetch`
8
- * (spans, traceparent injection, URL redaction, cache-header span attrs).
9
- * 2. `magentoOperationRouter` as the URL→operation fallback.
10
- * 3. An `onComplete` that records the canonical
11
- * `http.client.request.duration` histogram via the framework's
12
- * `recordCommerceMetric(...)` helper with `provider: "magento"`.
13
- *
14
- * Sites opt in once at startup:
15
- *
16
- * ```ts
17
- * import { createMagentoFetch, setMagentoFetch } from "@decocms/apps/magento";
18
- * setMagentoFetch(createMagentoFetch());
19
- * ```
20
- *
21
- * With this wired, every Magento egress call (GraphQL + REST) funnels through
22
- * one instrumented boundary, so upstream latency/status lands in ClickHouse.
23
- * SWR hit/miss for cached GETs is emitted separately by
24
- * `@decocms/blocks/sdk/fetchCache` (see `./fetchCache.ts`).
25
- */
26
-
27
- import {
28
- createInstrumentedFetch,
29
- type InstrumentedFetch,
30
- } from "@decocms/blocks/sdk/instrumentedFetch";
31
- import { recordCommerceMetric, statusClassFor } from "@decocms/blocks/sdk/observability";
32
- import { magentoOperationRouter } from "./operationRouter";
33
-
34
- export interface CreateMagentoFetchOptions {
35
- /** Underlying fetch to wrap. Defaults to `globalThis.fetch`. */
36
- baseFetch?: typeof fetch;
37
- /**
38
- * Disable the `http.client.request.duration` histogram for Magento calls.
39
- * Spans + structured logs still emit. Default: false.
40
- */
41
- disableHistogram?: boolean;
42
- }
43
-
44
- /**
45
- * Construct a pre-wired Magento `InstrumentedFetch`. Pass the result to
46
- * `setMagentoFetch(...)`.
47
- */
48
- export function createMagentoFetch(options: CreateMagentoFetchOptions = {}): InstrumentedFetch {
49
- const { baseFetch, disableHistogram = false } = options;
50
- return createInstrumentedFetch({
51
- name: "magento",
52
- baseFetch,
53
- resolveOperation: magentoOperationRouter,
54
- onComplete: disableHistogram ? undefined : (r) =>
55
- recordCommerceMetric(r.durationMs, { provider: "magento", operation: r.operation, status_class: statusClassFor(r.status), cached: r.cached }),
56
- });
57
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * URL-derived operation name router for Magento API calls.
3
- *
4
- * Plugged into `@decocms/blocks/sdk/instrumentedFetch`'s `resolveOperation`
5
- * option. Mirrors the VTEX/Shopify routers. Magento's surface from this repo is
6
- * a mix of GraphQL (`/graphql`) and REST (`/rest/<store>/V1/...`); the URL alone
7
- * can name the REST resource, while GraphQL calls fall back to a single
8
- * `graphql` operation (the semantic name lives in the document body, which
9
- * callers may stamp as `init.operation` to override this router).
10
- */
11
-
12
- type OperationResolver = string | ((match: RegExpMatchArray, method: string) => string);
13
-
14
- interface Matcher {
15
- pattern: RegExp;
16
- operation: OperationResolver;
17
- }
18
-
19
- const m = (pattern: RegExp, operation: OperationResolver): Matcher => ({ pattern, operation });
20
-
21
- const MATCHERS: ReadonlyArray<Matcher> = [
22
- m(/\/graphql\b/, "graphql"),
23
- // REST: /rest/<storeCode>/V1/<resource>/... — name by the first resource segment.
24
- m(/\/rest\/[^/]+\/V1\/([^/?#]+)/, (match) => `rest.${match[1]}`),
25
- m(/\/rest\/V1\/([^/?#]+)/, (match) => `rest.${match[1]}`),
26
- ];
27
-
28
- /**
29
- * Resolve an operation name for a Magento URL. Returns `undefined` when no
30
- * matcher fires, so the framework falls back to `magento.fetch`.
31
- */
32
- import { extractPathname } from "@decocms/blocks/sdk/urlUtils";
33
-
34
- export function magentoOperationRouter(url: string, _method: string): string | undefined {
35
- const pathname = extractPathname(url);
36
-
37
- for (const { pattern, operation } of MATCHERS) {
38
- const match = pathname.match(pattern);
39
- if (!match) continue;
40
- return typeof operation === "function" ? operation(match, _method) : operation;
41
- }
42
- return undefined;
43
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Flatten a Magento REST `searchCriteria` object into a bracketed
3
- * query-string key/value map.
4
- *
5
- * Magento's search-criteria payload is deeply nested (filterGroups →
6
- * filters → field/value), but the REST API accepts it only as flat
7
- * `searchCriteria[filterGroups][0][filters][0][field]=name` style
8
- * query parameters. This helper walks the tree once and emits that
9
- * shape.
10
- *
11
- * Ported verbatim from `deco-cx/apps/magento/utils/stringifySearchCriteria.ts`
12
- * — the Fresh implementation is self-contained (no Deno-isms) and
13
- * produces output that prod consumers already depend on. Behavior
14
- * pinned by `__tests__/stringifySearchCriteria.test.ts`.
15
- *
16
- * @example
17
- * stringifySearchCriteria({
18
- * filterGroups: [{ filters: [{ field: "sku", value: "ABC" }] }],
19
- * })
20
- * // ⇒ { "searchCriteria[filterGroups][0][filters][0][field]": "sku",
21
- * // "searchCriteria[filterGroups][0][filters][0][value]": "ABC" }
22
- */
23
- interface Filter {
24
- field: string;
25
- value: string;
26
- }
27
-
28
- interface FilterGroup {
29
- [key: string]: Filter[];
30
- }
31
-
32
- interface SearchCriteria {
33
- [key: string]: string | number | FilterGroup[];
34
- }
35
-
36
- type Path = string;
37
- type TraverseObj = SearchCriteria | FilterGroup[];
38
-
39
- function traverse(data: TraverseObj, result: Record<Path, string>, path: Path) {
40
- if (Array.isArray(data)) {
41
- data.forEach((item, index) => {
42
- traverse(item as unknown as TraverseObj, result, `${path}[${index}]`);
43
- });
44
- } else if (typeof data === "object" && data !== null) {
45
- for (const key in data) {
46
- if (Object.hasOwn(data, key)) {
47
- // @ts-expect-error recursive function with heterogeneous values
48
- traverse(data[key], result, `${path}[${key}]`);
49
- }
50
- }
51
- } else {
52
- result[path] = data;
53
- }
54
- }
55
-
56
- export default function stringifySearchCriteria(
57
- searchCriteriaObj: SearchCriteria,
58
- ): Record<Path, string> {
59
- const result: Record<Path, string> = {};
60
- traverse(searchCriteriaObj, result, "searchCriteria");
61
- return result;
62
- }
@@ -1,283 +0,0 @@
1
- /**
2
- * Magento product transform helpers — maps the REST/GraphQL payloads
3
- * into schema.org `Product` / `Offer` / `Seo` shapes that
4
- * `@decocms/apps-commerce` consumers expect.
5
- *
6
- * Subset of `deco-cx/apps/magento/utils/transform.ts` — only the
7
- * functions the PDP loader needs (toProduct, toOffer, toImages, toURL,
8
- * toBreadcrumbList, toSeo). The GraphQL-side helpers (toProductGraphQL,
9
- * toAggOfferGraphQL, toProductListingPageGraphQL, …) and the
10
- * storefront-specific helpers (toReviewAmasty, toLiveloPoints) are intentionally
11
- * excluded — they land in separate follow-up PRs alongside the loaders
12
- * that consume them.
13
- *
14
- * Behavior is pinned by `__tests__/transform.test.ts`. Every change to
15
- * this file should ship a regression test.
16
- */
17
-
18
- import type {
19
- ImageObject,
20
- ListItem,
21
- Offer,
22
- Product,
23
- PropertyValue,
24
- Seo,
25
- UnitPriceSpecification,
26
- } from "@decocms/apps-commerce/types";
27
- import type { CustomAttribute, MagentoCategory, MagentoProduct } from "./client/types";
28
- import { IN_STOCK, OUT_OF_STOCK } from "./constants";
29
-
30
- // ---------------------------------------------------------------------------
31
- // Product → schema.org Product
32
- // ---------------------------------------------------------------------------
33
-
34
- export const toProduct = ({
35
- product,
36
- options,
37
- }: {
38
- product: MagentoProduct;
39
- options: {
40
- currencyCode?: string;
41
- imagesUrl?: string;
42
- maxInstallments: number;
43
- minInstallmentValue: number;
44
- };
45
- }): Product => {
46
- const offers = toOffer(product, options.minInstallmentValue, options.maxInstallments);
47
- const sku = product.sku;
48
- const productID = product.id.toString();
49
- const productPrice = product.price_info;
50
-
51
- const additionalProperty: PropertyValue[] = product.custom_attributes?.map((attr) => ({
52
- "@type": "PropertyValue",
53
- name: attr.attribute_code,
54
- value: String(attr.value),
55
- }));
56
-
57
- return {
58
- "@type": "Product",
59
- productID,
60
- sku,
61
- url: product.url,
62
- name: product.name,
63
- gtin: sku,
64
- isVariantOf: {
65
- "@type": "ProductGroup",
66
- productGroupID: productID,
67
- url: product.url,
68
- name: product.name,
69
- model: "",
70
- additionalProperty: additionalProperty,
71
- hasVariant: [
72
- {
73
- "@type": "Product",
74
- productID,
75
- sku,
76
- url: product.url,
77
- name: product.name,
78
- gtin: sku,
79
- offers: {
80
- "@type": "AggregateOffer",
81
- // biome-ignore lint/style/noNonNullAssertion: matches prod fallback to price
82
- highPrice: productPrice?.max_price ?? product.price!,
83
- // biome-ignore lint/style/noNonNullAssertion: matches prod fallback to price
84
- lowPrice: productPrice?.minimal_price ?? product.price!,
85
- offerCount: offers.length,
86
- offers: offers,
87
- },
88
- },
89
- ],
90
- },
91
- additionalProperty: additionalProperty,
92
- image: toImages(product, options.imagesUrl ?? ""),
93
- offers: {
94
- "@type": "AggregateOffer",
95
- // biome-ignore lint/style/noNonNullAssertion: matches prod fallback to price
96
- highPrice: productPrice?.max_price ?? product.price!,
97
- // biome-ignore lint/style/noNonNullAssertion: matches prod fallback to price
98
- lowPrice: productPrice?.minimal_price ?? product.price!,
99
- offerCount: offers.length,
100
- offers: offers,
101
- },
102
- };
103
- };
104
-
105
- // ---------------------------------------------------------------------------
106
- // Product → Offer[]
107
- // ---------------------------------------------------------------------------
108
-
109
- export const toOffer = (
110
- { price_info, extension_attributes, sku, currency_code }: MagentoProduct,
111
- minInstallmentValue: number,
112
- maxInstallments: number,
113
- ): Offer[] => {
114
- if (!price_info) {
115
- return [];
116
- }
117
- const { final_price, max_price, max_regular_price } = price_info;
118
- const { stock_item } = extension_attributes;
119
- const inStock = stock_item?.is_in_stock;
120
- const qtyStock = stock_item?.qty ?? 0;
121
-
122
- return [
123
- {
124
- "@type": "Offer",
125
- availability: inStock ? IN_STOCK : OUT_OF_STOCK,
126
- inventoryLevel: {
127
- value: inStock ? qtyStock || 999 : 0,
128
- },
129
- itemCondition: "https://schema.org/NewCondition",
130
- price: final_price,
131
- priceCurrency: currency_code,
132
- priceSpecification: [
133
- {
134
- "@type": "UnitPriceSpecification",
135
- priceType: "https://schema.org/ListPrice",
136
- price: max_price ?? max_regular_price,
137
- },
138
- {
139
- "@type": "UnitPriceSpecification",
140
- priceType: "https://schema.org/SalePrice",
141
- price: final_price,
142
- },
143
- ...calculateInstallments(final_price, minInstallmentValue, maxInstallments),
144
- ],
145
- sku: sku,
146
- },
147
- ];
148
- };
149
-
150
- /**
151
- * Build the installment ladder for a sale price. `À vista` is always
152
- * the first entry, then 2x..Nx where N is bounded by
153
- * `min(floor(finalPrice/minInstallmentValue), maxInstallments)`.
154
- * Each entry is a `UnitPriceSpecification` so storefront UIs render
155
- * "10x de R$ 12,50 sem juros" without further math.
156
- */
157
- const calculateInstallments = (
158
- finalPrice: number,
159
- minInstallmentValue: number,
160
- maxInstallments: number,
161
- ): UnitPriceSpecification[] => {
162
- const possibleInstallmentsCount = Math.floor(finalPrice / minInstallmentValue) || 1;
163
- const actualInstallmentsCount = Array.from(
164
- { length: Math.min(possibleInstallmentsCount, maxInstallments) },
165
- (_v, i) => +(finalPrice / (i + 1)).toFixed(2),
166
- );
167
-
168
- return actualInstallmentsCount.map<UnitPriceSpecification>((value, i) => {
169
- const [description, billingIncrement] = !i
170
- ? ["À vista", finalPrice]
171
- : [`${i + 1}x sem juros`, value];
172
- return {
173
- "@type": "UnitPriceSpecification",
174
- priceType: "https://schema.org/SalePrice",
175
- priceComponentType: "https://schema.org/Installment",
176
- description,
177
- billingDuration: i + 1,
178
- billingIncrement,
179
- price: finalPrice,
180
- };
181
- });
182
- };
183
-
184
- // ---------------------------------------------------------------------------
185
- // Product → ImageObject[]
186
- // ---------------------------------------------------------------------------
187
-
188
- export const toImages = (product: MagentoProduct, imageUrl: string): ImageObject[] | undefined => {
189
- if (imageUrl) {
190
- return product.media_gallery_entries?.map((img) => ({
191
- "@type": "ImageObject" as const,
192
- encodingFormat: "image",
193
- alternateName: `${img.file}`,
194
- url: `${toURL(imageUrl)}${img.file}`,
195
- disabled: img.disabled || false,
196
- }));
197
- }
198
-
199
- return product.images?.map((img) => ({
200
- "@type": "ImageObject" as const,
201
- encodingFormat: "image",
202
- alternateName: `${img.label}`,
203
- disabled: img.disabled || false,
204
- url: `${img.url}`,
205
- }));
206
- };
207
-
208
- /**
209
- * Protocol-relative URLs (`//cdn.magento.example/...`) come back from
210
- * some Magento installations. Promote to https so the browser doesn't
211
- * try to inherit a non-https scheme.
212
- */
213
- export const toURL = (src: string): string => (src.startsWith("//") ? `https:${src}` : src);
214
-
215
- // ---------------------------------------------------------------------------
216
- // Categories → ListItem[] (breadcrumb)
217
- // ---------------------------------------------------------------------------
218
-
219
- export const toBreadcrumbList = (
220
- categories: (MagentoCategory | null)[],
221
- isBreadcrumbProductName: boolean,
222
- product: Product,
223
- url: URL,
224
- ): ListItem<string>[] => {
225
- if (isBreadcrumbProductName && categories?.length === 0) {
226
- return [
227
- {
228
- "@type": "ListItem",
229
- name: product.name,
230
- position: 1,
231
- item: new URL(`/${product.name}`, url).href,
232
- },
233
- ];
234
- }
235
-
236
- const itemListElement = categories
237
- .map((category) => {
238
- if (!category || !category.name || !category.position) {
239
- return null;
240
- }
241
- return {
242
- "@type": "ListItem",
243
- name: category.name,
244
- position: category.position,
245
- item: new URL(`/${category.name}`, url).href,
246
- };
247
- })
248
- .filter(Boolean) as ListItem<string>[];
249
-
250
- return itemListElement;
251
- };
252
-
253
- // ---------------------------------------------------------------------------
254
- // Custom attributes → Seo
255
- // ---------------------------------------------------------------------------
256
-
257
- /**
258
- * Build a `Seo` block from a product's custom_attributes. Magento
259
- * exposes title/meta_title/meta_description as separate attribute_code
260
- * entries; this maps them with explicit fallback (meta_title wins over
261
- * title; description is meta_description only — there's no fallback
262
- * field in the legacy code).
263
- */
264
- export const toSeo = (customAttributes: CustomAttribute[], productURL: string): Seo => {
265
- const findAttribute = (attrCode: string): string => {
266
- const attribute = customAttributes.find((attr) => attr.attribute_code === attrCode);
267
- if (!attribute) return "";
268
- if (Array.isArray(attribute.value)) {
269
- return attribute.value.join(", ");
270
- }
271
- return attribute.value;
272
- };
273
-
274
- const title = findAttribute("title");
275
- const metaTitle = findAttribute("meta_title");
276
- const metaDescription = findAttribute("meta_description");
277
-
278
- return {
279
- title: metaTitle || title || "",
280
- description: metaDescription || "",
281
- canonical: productURL,
282
- };
283
- };
package/src/utils/user.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Reads the Magento PHP session cookie from a request's headers.
3
- * Returns undefined when the cookie is absent (anonymous visitor).
4
- *
5
- * Mirrors `deco-cx/apps/magento/utils/user.ts` — the Fresh version
6
- * used Deno's `std/http/cookie` getCookies; the port goes through
7
- * `@decocms/blocks/sdk/cookie` so it works on Cloudflare Workers and
8
- * Node alike.
9
- */
10
- import { getCookies } from "@decocms/blocks/sdk/cookie";
11
- import { SESSION_COOKIE } from "./constants";
12
-
13
- export const getUserCookie = (headers: Headers): string | undefined => {
14
- const cookies = getCookies(headers);
15
- return cookies[SESSION_COOKIE];
16
- };
package/tsconfig.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist"
5
- },
6
- "include": ["src/**/*"]
7
- }