@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.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/magentoClient.d.ts +62 -0
- package/dist/magentoClient.d.ts.map +1 -0
- package/dist/magentoClient.js +96 -0
- package/dist/magentoClient.js.map +1 -0
- package/package.json +19 -27
- package/src/index.ts +2 -29
- package/src/README.md +0 -65
- package/src/__tests__/cart.test.ts +0 -139
- package/src/__tests__/client.test.ts +0 -252
- package/src/__tests__/features.test.ts +0 -51
- package/src/__tests__/graphql.test.ts +0 -183
- package/src/__tests__/magentoClient.test.ts +0 -166
- package/src/__tests__/newsletter-subscribe.test.ts +0 -86
- package/src/__tests__/product-stockAlert.test.ts +0 -80
- package/src/__tests__/stringifySearchCriteria.test.ts +0 -56
- package/src/__tests__/transform.test.ts +0 -346
- package/src/__tests__/user-loader.test.ts +0 -134
- package/src/__tests__/wishlist-actions.test.ts +0 -140
- package/src/__tests__/wishlist-loader.test.ts +0 -97
- package/src/actions/newsletter/subscribe.ts +0 -40
- package/src/actions/product/stockAlert.ts +0 -67
- package/src/actions/wishlist/addItem.ts +0 -57
- package/src/actions/wishlist/removeItem.ts +0 -59
- package/src/client.ts +0 -252
- package/src/loaders/cart.ts +0 -64
- package/src/loaders/features.ts +0 -16
- package/src/loaders/user.ts +0 -60
- package/src/loaders/wishlist.ts +0 -29
- package/src/middleware.ts +0 -19
- package/src/types.ts +0 -51
- package/src/utils/__tests__/instrumentation.test.ts +0 -59
- package/src/utils/cacheTimeControl.ts +0 -69
- package/src/utils/client/types.ts +0 -270
- package/src/utils/constants.ts +0 -126
- package/src/utils/fetchCache.ts +0 -55
- package/src/utils/graphql-types.ts +0 -99
- package/src/utils/graphql.ts +0 -155
- package/src/utils/instrumentedFetch.ts +0 -57
- package/src/utils/operationRouter.ts +0 -43
- package/src/utils/stringifySearchCriteria.ts +0 -62
- package/src/utils/transform.ts +0 -283
- package/src/utils/user.ts +0 -16
- package/tsconfig.json +0 -7
package/src/client.ts
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Magento API client config — module-global, set once at app boot.
|
|
3
|
-
*
|
|
4
|
-
* Mirrors `vtex/client.ts`'s configureVtex/getVtexConfig pattern so the
|
|
5
|
-
* same wiring contract works across commerce apps. Sites should call
|
|
6
|
-
* `configureMagento(...)` once from their setup phase before any
|
|
7
|
-
* loader/action runs; loaders consume `getMagentoConfig()` to pick up
|
|
8
|
-
* baseUrl, auth, and feature toggles.
|
|
9
|
-
*
|
|
10
|
-
* Two reasons we don't pass config explicitly to every loader:
|
|
11
|
-
* 1. CMS-resolved loader instances don't know where the config block
|
|
12
|
-
* lives; the site's `initMagentoFromBlocks(blocks)` adapter is the
|
|
13
|
-
* single source of truth.
|
|
14
|
-
* 2. Matches the rest of @decocms/apps so a site touching VTEX and
|
|
15
|
-
* Magento has consistent muscle memory.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { type FetchFn, withFetchTimeout } from "@decocms/blocks/sdk/fetchTimeout";
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Config shapes
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* URL-search-param filter mapping consumed by GraphQL product loaders.
|
|
26
|
-
* Each entry pairs a Magento attribute slug (`value`) with the
|
|
27
|
-
* comparison operator the storefront's URL filters use (`type`). The
|
|
28
|
-
* default mapping lives in `utils/constants.ts:DEFAULT_GRAPHQL_FILTERS`;
|
|
29
|
-
* sites extend it via the `customFilters` prop on PLP/list loaders.
|
|
30
|
-
*/
|
|
31
|
-
export interface FiltersGraphQL {
|
|
32
|
-
value: string;
|
|
33
|
-
type: "EQUAL" | "MATCH" | "RANGE";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface MagentoFeatures {
|
|
37
|
-
dangerouslyDisableWishlist?: boolean;
|
|
38
|
-
dangerouslyDisableOnLoadUpdate?: boolean;
|
|
39
|
-
dangerouslyReturnNullAfterAction?: boolean;
|
|
40
|
-
dangerouslyDontReturnCartAfterAction?: boolean;
|
|
41
|
-
dangerouslyDisableOnVisibilityChangeUpdate?: boolean;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface MagentoImagesConfig {
|
|
45
|
-
imagesQtd: number;
|
|
46
|
-
imagesUrl: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface MagentoPricingConfig {
|
|
50
|
-
maxInstallments: number;
|
|
51
|
-
minInstallmentValue: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface MagentoCartConfigs {
|
|
55
|
-
countProductImageInCart?: number;
|
|
56
|
-
changeCardIdAfterCheckout?: boolean;
|
|
57
|
-
cartErrorMessages?: string[];
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface MagentoConfig {
|
|
61
|
-
/** Magento storefront base URL, e.g. `https://loja.acme.com.br/` */
|
|
62
|
-
baseUrl: string;
|
|
63
|
-
/** Bearer token for `Authorization` header on admin REST calls */
|
|
64
|
-
apiKey: string;
|
|
65
|
-
/** Store ID used in headers + path prefixes */
|
|
66
|
-
storeId: number;
|
|
67
|
-
/** Site/site-code used in path segments */
|
|
68
|
-
site: string;
|
|
69
|
-
/** Which Store header to send (default: "site") */
|
|
70
|
-
storeHeader?: string;
|
|
71
|
-
/** Optional opaque header value sent as `x-origin-header` */
|
|
72
|
-
originHeader?: string;
|
|
73
|
-
/** Currency code (e.g. "BRL") */
|
|
74
|
-
currencyCode?: string;
|
|
75
|
-
/** Whether to append `_suffix` to admin endpoints */
|
|
76
|
-
useSuffix?: boolean;
|
|
77
|
-
/** Behavior toggles surfaced to client hooks */
|
|
78
|
-
features?: MagentoFeatures;
|
|
79
|
-
/** Cart-specific tunables */
|
|
80
|
-
cartConfigs?: MagentoCartConfigs;
|
|
81
|
-
/** Images CDN config */
|
|
82
|
-
imagesConfig?: MagentoImagesConfig;
|
|
83
|
-
/** Pricing rules for installments display */
|
|
84
|
-
pricingConfig?: MagentoPricingConfig;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Module-global state
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
|
-
let config: MagentoConfig | null = null;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Underlying fetch used by {@link magentoFetch}. Defaults to `globalThis.fetch`;
|
|
95
|
-
* override once at boot with {@link setMagentoFetch} to plug in the instrumented
|
|
96
|
-
* fetch (spans + `http.client.request.duration` histogram, `provider:"magento"`).
|
|
97
|
-
* Mirrors VTEX's `setVtexFetch` / Shopify's `setShopifyFetch` so every commerce
|
|
98
|
-
* app funnels egress through a single instrumented `_fetch`.
|
|
99
|
-
*/
|
|
100
|
-
let _fetch: FetchFn = withFetchTimeout();
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Override the fetch used by every Magento egress call.
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```ts
|
|
107
|
-
* import { createMagentoFetch, setMagentoFetch } from "@decocms/apps/magento";
|
|
108
|
-
* setMagentoFetch(createMagentoFetch());
|
|
109
|
-
* ```
|
|
110
|
-
*/
|
|
111
|
-
export function setMagentoFetch(fetchFn: FetchFn): void {
|
|
112
|
-
_fetch = fetchFn;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function configureMagento(c: MagentoConfig): void {
|
|
116
|
-
config = c;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function getMagentoConfig(): MagentoConfig {
|
|
120
|
-
if (!config) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
"[Magento] configureMagento() must be called before loaders run. " +
|
|
123
|
-
"Wire it in your site's setup, e.g. configureMagento(blocks.magento).",
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
return config;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Best-effort init from a CMS block — mirrors `initVtexFromBlocks`.
|
|
131
|
-
*
|
|
132
|
-
* Resolves secret references stored in the CMS block (`apiKey`,
|
|
133
|
-
* `originHeader`) in this priority:
|
|
134
|
-
* 1. Plain string (dev override)
|
|
135
|
-
* 2. `{ get: () => string }` object (legacy)
|
|
136
|
-
* 3. `{ encrypted: "<hex>" }` decrypted via `DECO_CRYPTO_KEY` (prod)
|
|
137
|
-
* 4. `{ name: "ENV_VAR" }` → `process.env[name]` (fallback)
|
|
138
|
-
*
|
|
139
|
-
* (3) is what the production Deco CMS actually stores — admin
|
|
140
|
-
* encrypts the secret with the site's `DECO_CRYPTO_KEY` so the value
|
|
141
|
-
* never leaves the worker in plain text. Previously this init only
|
|
142
|
-
* read `process.env[name]`, which silently produced `apiKey: ""` for
|
|
143
|
-
* any site that hadn't *also* set the named env var as a CF Worker
|
|
144
|
-
* secret. Result: `Authorization: Bearer ` header missing on every
|
|
145
|
-
* request → Magento 401 → minicart/cart-related loaders dead. The
|
|
146
|
-
* shared `resolveSecret` helper from `@decocms/blocks/sdk/crypto`
|
|
147
|
-
* handles the full chain, matching how VTEX and Shopify configure
|
|
148
|
-
* themselves.
|
|
149
|
-
*
|
|
150
|
-
* Because the AES-CBC decrypt step is async, this function is now
|
|
151
|
-
* `Promise<void>` — site setups must `await` the call before any
|
|
152
|
-
* loader fires.
|
|
153
|
-
*/
|
|
154
|
-
export async function initMagentoFromBlocks(blocks: Record<string, unknown>): Promise<void> {
|
|
155
|
-
// Lazy-imported to keep `@decocms/blocks/sdk/crypto` out of the
|
|
156
|
-
// import graph for sites that wire Magento manually via
|
|
157
|
-
// `configureMagento({ apiKey: "..." })` without ever calling this
|
|
158
|
-
// helper (e.g. unit tests, CLI tools).
|
|
159
|
-
const { resolveSecret } = await import("@decocms/blocks/sdk/crypto");
|
|
160
|
-
|
|
161
|
-
const block = blocks.magento as Record<string, any> | undefined;
|
|
162
|
-
if (!block) {
|
|
163
|
-
console.warn("[Magento] No `magento` block found in CMS; skipping init.");
|
|
164
|
-
return;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const apiConfig = block.apiConfig ?? {};
|
|
168
|
-
|
|
169
|
-
// The env-var fallback names match the Secret block's `name` field
|
|
170
|
-
// when present. `resolveSecret` cycles through the chain documented
|
|
171
|
-
// above; an empty string here means every layer was empty, which we
|
|
172
|
-
// pass through verbatim so `buildHeaders` can detect it.
|
|
173
|
-
const extractEnvName = (value: unknown): string => {
|
|
174
|
-
if (value && typeof value === "object") {
|
|
175
|
-
const name = (value as { name?: unknown }).name;
|
|
176
|
-
if (typeof name === "string") return name;
|
|
177
|
-
}
|
|
178
|
-
return "";
|
|
179
|
-
};
|
|
180
|
-
const apiKeyEnvName = extractEnvName(apiConfig.apiKey);
|
|
181
|
-
const originHeaderEnvName = extractEnvName(apiConfig.originHeader);
|
|
182
|
-
|
|
183
|
-
const apiKey = (await resolveSecret(apiConfig.apiKey, apiKeyEnvName)) ?? "";
|
|
184
|
-
const originHeader = (await resolveSecret(apiConfig.originHeader, originHeaderEnvName)) ?? "";
|
|
185
|
-
|
|
186
|
-
configureMagento({
|
|
187
|
-
baseUrl: apiConfig.baseUrl ?? "",
|
|
188
|
-
apiKey,
|
|
189
|
-
storeId: apiConfig.storeId ?? 1,
|
|
190
|
-
site: apiConfig.site ?? "",
|
|
191
|
-
storeHeader: apiConfig.storeHeader,
|
|
192
|
-
originHeader,
|
|
193
|
-
currencyCode: apiConfig.currencyCode,
|
|
194
|
-
useSuffix: apiConfig.useSuffix,
|
|
195
|
-
features: block.features,
|
|
196
|
-
cartConfigs: block.cartConfigs,
|
|
197
|
-
imagesConfig: block.imagesConfig,
|
|
198
|
-
pricingConfig: block.pricingConfig,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
// HTTP helpers (thin wrappers over fetch with auth pre-applied)
|
|
204
|
-
// ---------------------------------------------------------------------------
|
|
205
|
-
|
|
206
|
-
export interface MagentoFetchOpts extends RequestInit {
|
|
207
|
-
/** Whether to attach the admin Bearer token. Default true. */
|
|
208
|
-
authenticated?: boolean;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function buildHeaders(
|
|
212
|
-
opts: MagentoFetchOpts,
|
|
213
|
-
c: MagentoConfig,
|
|
214
|
-
attachMagentoIdentity: boolean,
|
|
215
|
-
): Headers {
|
|
216
|
-
const headers = new Headers(opts.headers ?? {});
|
|
217
|
-
// `attachMagentoIdentity` is false when the request is going to a host
|
|
218
|
-
// that isn't the configured Magento backend. None of the Magento-only
|
|
219
|
-
// headers below should leak in that case: the Bearer is privileged, the
|
|
220
|
-
// `x-origin-header` is a secret that third parties shouldn't see, and a
|
|
221
|
-
// forced `Referer` would broadcast our Magento storefront URL.
|
|
222
|
-
if (!attachMagentoIdentity) return headers;
|
|
223
|
-
|
|
224
|
-
if (opts.authenticated !== false && c.apiKey) {
|
|
225
|
-
headers.set("Authorization", `Bearer ${c.apiKey}`);
|
|
226
|
-
}
|
|
227
|
-
if (c.originHeader) {
|
|
228
|
-
headers.set("x-origin-header", c.originHeader);
|
|
229
|
-
}
|
|
230
|
-
if (!headers.has("Referer")) {
|
|
231
|
-
headers.set("Referer", c.baseUrl);
|
|
232
|
-
}
|
|
233
|
-
return headers;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export function magentoFetch(path: string, opts: MagentoFetchOpts = {}): Promise<Response> {
|
|
237
|
-
const c = getMagentoConfig();
|
|
238
|
-
const baseUrl = new URL(c.baseUrl);
|
|
239
|
-
const target = path.startsWith("http")
|
|
240
|
-
? new URL(path)
|
|
241
|
-
: new URL(path.startsWith("/") ? path : `/${path}`, baseUrl);
|
|
242
|
-
|
|
243
|
-
// Only attach Magento identity (Bearer, x-origin-header, forced Referer)
|
|
244
|
-
// when the request is going to the configured Magento host. An absolute
|
|
245
|
-
// URL to a different origin would otherwise leak the admin token *and*
|
|
246
|
-
// our origin/Referer secrets to that third party. Callers that genuinely
|
|
247
|
-
// want a third-party call must still pass `authenticated: false` for
|
|
248
|
-
// clarity at the call site.
|
|
249
|
-
const sameOrigin = target.origin === baseUrl.origin;
|
|
250
|
-
|
|
251
|
-
return _fetch(target, { ...opts, headers: buildHeaders(opts, c, sameOrigin) });
|
|
252
|
-
}
|
package/src/loaders/cart.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Magento cart loader — fetches the customer's active cart by cookie.
|
|
3
|
-
*
|
|
4
|
-
* Reads the `dataservices_cart_id` cookie from the request, calls the
|
|
5
|
-
* Magento admin REST endpoint, and returns the cart payload. Returns
|
|
6
|
-
* `null` when no cart cookie is present (anonymous visitor — expected).
|
|
7
|
-
*
|
|
8
|
-
* This is a minimal port of `deco-cx/apps/magento/loaders/cart.ts`.
|
|
9
|
-
* It omits, for now, the image-handling pipeline (`handleCartImages`)
|
|
10
|
-
* and the cart-items-with-images transform. Those depend on
|
|
11
|
-
* `utils/cache.ts` and `utils/cart.ts` from the original — both
|
|
12
|
-
* pending ports (see magento/README.md).
|
|
13
|
-
*/
|
|
14
|
-
import { getCookies } from "@decocms/blocks/sdk/cookie";
|
|
15
|
-
import { getMagentoConfig, magentoFetch } from "../client";
|
|
16
|
-
import type { MagentoCart } from "../types";
|
|
17
|
-
|
|
18
|
-
const CART_COOKIE = "dataservices_cart_id";
|
|
19
|
-
|
|
20
|
-
function readCartIdFromCookie(headers: Headers): string | null {
|
|
21
|
-
const cookies = getCookies(headers);
|
|
22
|
-
const raw = cookies[CART_COOKIE];
|
|
23
|
-
if (!raw) return null;
|
|
24
|
-
// Magento sets the cookie as `"<id>"` (JSON-encoded). Try to parse;
|
|
25
|
-
// fall back to the raw string if it isn't quoted.
|
|
26
|
-
try {
|
|
27
|
-
const parsed = JSON.parse(raw);
|
|
28
|
-
return typeof parsed === "string" ? parsed : raw;
|
|
29
|
-
} catch {
|
|
30
|
-
return raw;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface CartLoaderProps {
|
|
35
|
-
/** Override the cart id (used by checkout flows that already know it). */
|
|
36
|
-
cartId?: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export default async function cart(
|
|
40
|
-
props: CartLoaderProps | undefined,
|
|
41
|
-
req: Request,
|
|
42
|
-
): Promise<MagentoCart | null> {
|
|
43
|
-
const cartId = props?.cartId ?? readCartIdFromCookie(req.headers);
|
|
44
|
-
if (!cartId) return null;
|
|
45
|
-
|
|
46
|
-
const { site } = getMagentoConfig();
|
|
47
|
-
// Magento exposes the cart endpoint at /rest/:site/V1/carts/:cartId — the
|
|
48
|
-
// /rest/ prefix is mandatory and matches the Fresh/Deno original
|
|
49
|
-
// (deco-cx/apps/magento/loaders/cart.ts uses
|
|
50
|
-
// clientAdmin["GET /rest/:site/V1/carts/:cartId"]).
|
|
51
|
-
//
|
|
52
|
-
// cartId comes from the request cookie and is user-controlled. Both
|
|
53
|
-
// `site` and `cartId` are URL-encoded so neither can break out of its
|
|
54
|
-
// path segment and hit a different endpoint while the privileged
|
|
55
|
-
// Bearer token is still attached.
|
|
56
|
-
const path = `/rest/${encodeURIComponent(site)}/V1/carts/${encodeURIComponent(cartId)}`;
|
|
57
|
-
|
|
58
|
-
const res = await magentoFetch(path);
|
|
59
|
-
if (!res.ok) {
|
|
60
|
-
if (res.status === 404) return null; // expired/invalid cart cookie
|
|
61
|
-
throw new Error(`[Magento] cart loader: ${res.status} ${res.statusText}`);
|
|
62
|
-
}
|
|
63
|
-
return (await res.json()) as MagentoCart;
|
|
64
|
-
}
|
package/src/loaders/features.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Magento feature flags — returns the `features` block from the resolved
|
|
3
|
-
* Magento config. Sites read this to gate optional client-side behavior
|
|
4
|
-
* (cart on-load update, wishlist visibility, on-visibility-change update,
|
|
5
|
-
* etc.) without re-deploying.
|
|
6
|
-
*
|
|
7
|
-
* In the legacy deco-cx/apps shape this lived as a 3-arg loader
|
|
8
|
-
* `(_props, _req, ctx) => ctx.features`. The TanStack/Node port uses
|
|
9
|
-
* the module-global config set by `configureMagento(...)` instead of a
|
|
10
|
-
* per-request ctx, which matches the rest of @decocms/apps.
|
|
11
|
-
*/
|
|
12
|
-
import { getMagentoConfig, type MagentoFeatures } from "../client";
|
|
13
|
-
|
|
14
|
-
export default function features(): MagentoFeatures {
|
|
15
|
-
return getMagentoConfig().features ?? {};
|
|
16
|
-
}
|
package/src/loaders/user.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves the current customer into a schema.org `Person` from
|
|
3
|
-
* Magento's `/customer/section/load?sections=customer,carbono-customer`
|
|
4
|
-
* endpoint, scoped by the visitor's PHPSESSID cookie.
|
|
5
|
-
*
|
|
6
|
-
* Ported from `deco-cx/apps/magento/loaders/user.ts`. The Fresh
|
|
7
|
-
* version used `clientAdmin["GET /:site/customer/section/load"]` with
|
|
8
|
-
* typed indexed routes — the TanStack/Node port uses `magentoFetch`
|
|
9
|
-
* (which already applies auth/origin headers for same-origin) plus an
|
|
10
|
-
* explicit `Cookie: PHPSESSID=…` header so Magento associates the
|
|
11
|
-
* request with the logged-in customer.
|
|
12
|
-
*
|
|
13
|
-
* Returns null when:
|
|
14
|
-
* - the session cookie is absent (anonymous visitor — expected),
|
|
15
|
-
* - the customer slice is missing or has no data_id (Magento returns
|
|
16
|
-
* {} for guest sessions),
|
|
17
|
-
* - the HTTP call throws or returns a non-2xx (defensive — the
|
|
18
|
-
* storefront just renders the logged-out UI).
|
|
19
|
-
*/
|
|
20
|
-
import type { Person } from "@decocms/apps-commerce/types";
|
|
21
|
-
import { getMagentoConfig, magentoFetch } from "../client";
|
|
22
|
-
import type { CustomerSectionLoad } from "../utils/client/types";
|
|
23
|
-
import { SESSION_COOKIE } from "../utils/constants";
|
|
24
|
-
import { getUserCookie } from "../utils/user";
|
|
25
|
-
|
|
26
|
-
export default async function user(_props: unknown, req: Request): Promise<Person | null> {
|
|
27
|
-
const sessionId = getUserCookie(req.headers);
|
|
28
|
-
if (!sessionId) return null;
|
|
29
|
-
|
|
30
|
-
const { site } = getMagentoConfig();
|
|
31
|
-
const path = `/${encodeURIComponent(site)}/customer/section/load?sections=customer,carbono-customer`;
|
|
32
|
-
|
|
33
|
-
try {
|
|
34
|
-
const res = await magentoFetch(path, {
|
|
35
|
-
headers: { Cookie: `${SESSION_COOKIE}=${sessionId}` },
|
|
36
|
-
});
|
|
37
|
-
if (!res.ok) return null;
|
|
38
|
-
|
|
39
|
-
const response = (await res.json()) as CustomerSectionLoad;
|
|
40
|
-
const carbono = response["carbono-customer"];
|
|
41
|
-
const customer = response.customer;
|
|
42
|
-
|
|
43
|
-
if (!carbono?.data_id || !customer) return null;
|
|
44
|
-
|
|
45
|
-
const { customerId, email } = carbono;
|
|
46
|
-
const { fullname, firstname } = customer;
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
"@id": customerId,
|
|
50
|
-
email: email,
|
|
51
|
-
givenName: firstname,
|
|
52
|
-
...(firstname &&
|
|
53
|
-
fullname && {
|
|
54
|
-
familyName: fullname.replace(firstname, "").trim(),
|
|
55
|
-
}),
|
|
56
|
-
};
|
|
57
|
-
} catch {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
package/src/loaders/wishlist.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves the visitor's saved wishlist via Magento's
|
|
3
|
-
* `/customer/section/load?sections=wishlist` endpoint, scoped by the
|
|
4
|
-
* PHPSESSID cookie.
|
|
5
|
-
*
|
|
6
|
-
* Ported from `deco-cx/apps/magento/loaders/wishlist.ts`. Returns null
|
|
7
|
-
* when the session cookie is absent or Magento reports no wishlist
|
|
8
|
-
* (e.g. a logged-in customer who never saved an item).
|
|
9
|
-
*/
|
|
10
|
-
import { getMagentoConfig, magentoFetch } from "../client";
|
|
11
|
-
import type { CustomerSectionLoad, Wishlist } from "../utils/client/types";
|
|
12
|
-
import { SESSION_COOKIE } from "../utils/constants";
|
|
13
|
-
import { getUserCookie } from "../utils/user";
|
|
14
|
-
|
|
15
|
-
export default async function wishlist(_props: unknown, req: Request): Promise<Wishlist | null> {
|
|
16
|
-
const sessionId = getUserCookie(req.headers);
|
|
17
|
-
if (!sessionId) return null;
|
|
18
|
-
|
|
19
|
-
const { site } = getMagentoConfig();
|
|
20
|
-
const path = `/${encodeURIComponent(site)}/customer/section/load?sections=wishlist`;
|
|
21
|
-
|
|
22
|
-
const res = await magentoFetch(path, {
|
|
23
|
-
headers: { Cookie: `${SESSION_COOKIE}=${sessionId}` },
|
|
24
|
-
});
|
|
25
|
-
if (!res.ok) return null;
|
|
26
|
-
|
|
27
|
-
const { wishlist } = (await res.json()) as CustomerSectionLoad;
|
|
28
|
-
return wishlist ?? null;
|
|
29
|
-
}
|
package/src/middleware.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Magento middleware — currently a passthrough.
|
|
3
|
-
*
|
|
4
|
-
* The legacy deco-cx/apps middleware reconciled the cart id cookie
|
|
5
|
-
* after checkout (`changeCardIdAfterCheckout`) and seeded the
|
|
6
|
-
* `form_key` for anonymous sessions. Both flows touched response
|
|
7
|
-
* headers and `customer/section/load` endpoints — non-trivial port,
|
|
8
|
-
* deferred to a follow-up PR. Today a production Magento storefront
|
|
9
|
-
* handles cart reconciliation on the client.
|
|
10
|
-
*
|
|
11
|
-
* Shape matches `@decocms/apps-commerce/app-types` so it can be
|
|
12
|
-
* plugged into the autoconfig pipeline once magento is registered
|
|
13
|
-
* there.
|
|
14
|
-
*/
|
|
15
|
-
import type { AppMiddleware } from "@decocms/apps-commerce/app-types";
|
|
16
|
-
|
|
17
|
-
export const magentoMiddleware: AppMiddleware = async (_request, next) => {
|
|
18
|
-
return next();
|
|
19
|
-
};
|
package/src/types.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared types for the Magento app — kept minimal in this initial port.
|
|
3
|
-
* Loader/action ports should extend this file rather than duplicating
|
|
4
|
-
* inline types.
|
|
5
|
-
*/
|
|
6
|
-
import type { MagentoFeatures } from "./client";
|
|
7
|
-
|
|
8
|
-
export interface MagentoCart {
|
|
9
|
-
id: string | null;
|
|
10
|
-
items: MagentoCartItem[];
|
|
11
|
-
totals?: {
|
|
12
|
-
subtotal?: number;
|
|
13
|
-
grand_total?: number;
|
|
14
|
-
discount_amount?: number;
|
|
15
|
-
shipping_amount?: number;
|
|
16
|
-
};
|
|
17
|
-
coupon_code?: string | null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface MagentoCartItem {
|
|
21
|
-
item_id: number;
|
|
22
|
-
sku: string;
|
|
23
|
-
name?: string;
|
|
24
|
-
qty: number;
|
|
25
|
-
price?: number;
|
|
26
|
-
row_total?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type Features = MagentoFeatures;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Magento newsletter subscription response shape — what
|
|
33
|
-
* `actions/newsletter/subscribe` returns to the storefront.
|
|
34
|
-
*/
|
|
35
|
-
export interface NewsletterData {
|
|
36
|
-
success: boolean;
|
|
37
|
-
message: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Stock-alert mutation response from Magento's GraphQL endpoint —
|
|
42
|
-
* `actions/product/stockAlert` returns this (or `{ error }`).
|
|
43
|
-
*/
|
|
44
|
-
export interface ProductStockAlertResponse {
|
|
45
|
-
data?: {
|
|
46
|
-
productStockAlert: {
|
|
47
|
-
message: string;
|
|
48
|
-
status: boolean;
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coverage for the Magento observability wiring added so cache/upstream
|
|
3
|
-
* telemetry flows automatically:
|
|
4
|
-
* - `magentoOperationRouter` names REST resources + GraphQL.
|
|
5
|
-
* - `setMagentoFetch` actually reroutes `magentoFetch`'s egress (the hook
|
|
6
|
-
* that lets `createMagentoFetch()`'s instrumentation take effect).
|
|
7
|
-
*/
|
|
8
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
9
|
-
import { magentoOperationRouter } from "../operationRouter";
|
|
10
|
-
|
|
11
|
-
describe("magentoOperationRouter", () => {
|
|
12
|
-
it("names REST resources by their first V1 segment", () => {
|
|
13
|
-
expect(magentoOperationRouter("https://x.com/rest/default/V1/products/42", "GET")).toBe(
|
|
14
|
-
"rest.products",
|
|
15
|
-
);
|
|
16
|
-
expect(magentoOperationRouter("https://x.com/rest/V1/carts/mine", "POST")).toBe("rest.carts");
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("names GraphQL calls `graphql`", () => {
|
|
20
|
-
expect(magentoOperationRouter("https://x.com/graphql", "POST")).toBe("graphql");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("returns undefined when nothing matches (framework falls back)", () => {
|
|
24
|
-
expect(magentoOperationRouter("https://x.com/media/logo.png", "GET")).toBeUndefined();
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("tolerates non-absolute URLs", () => {
|
|
28
|
-
expect(magentoOperationRouter("/rest/default/V1/orders?x=1", "GET")).toBe("rest.orders");
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("setMagentoFetch routing", () => {
|
|
33
|
-
beforeEach(() => {
|
|
34
|
-
vi.resetModules();
|
|
35
|
-
});
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
vi.restoreAllMocks();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("routes magentoFetch egress through the fetch set via setMagentoFetch", async () => {
|
|
41
|
-
const { configureMagento, setMagentoFetch, magentoFetch } = await import("../../client");
|
|
42
|
-
configureMagento({
|
|
43
|
-
baseUrl: "https://loja.example.com/",
|
|
44
|
-
apiKey: "k",
|
|
45
|
-
storeId: 1,
|
|
46
|
-
site: "example",
|
|
47
|
-
});
|
|
48
|
-
const custom = vi
|
|
49
|
-
.fn()
|
|
50
|
-
.mockResolvedValue(new Response("{}", { status: 200 })) as unknown as typeof fetch;
|
|
51
|
-
const globalSpy = vi.spyOn(globalThis, "fetch");
|
|
52
|
-
setMagentoFetch(custom);
|
|
53
|
-
|
|
54
|
-
await magentoFetch("/rest/default/V1/products/1");
|
|
55
|
-
|
|
56
|
-
expect(custom).toHaveBeenCalledOnce();
|
|
57
|
-
expect(globalSpy).not.toHaveBeenCalled(); // did NOT hit the default fetch
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cache-time-control helpers — verbatim port of
|
|
3
|
-
* `deco-cx/apps/magento/utils/cacheTimeControl.ts`.
|
|
4
|
-
*
|
|
5
|
-
* Magento product loaders pass the request URL through
|
|
6
|
-
* `filterSearchParamsFromURL` before using it as a cache key, so that
|
|
7
|
-
* tracking parameters (utm_*, gclid, fbclid, etc.) don't fragment the
|
|
8
|
-
* cache across thousands of variants for what is conceptually the
|
|
9
|
-
* same page.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export const DEFAULT_CACHE_MAX_AGE = 3600; // 1h
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Pattern list (each entry is a regex source) of URL search-param
|
|
16
|
-
* keys that get stripped before a Magento URL goes into the cache key
|
|
17
|
-
* or downstream search-criteria. Mirrors prod byte-for-byte — the
|
|
18
|
-
* `.*` suffixes intentionally match `utm_source`, `utm_medium`, etc.
|
|
19
|
-
*/
|
|
20
|
-
export const SEARCH_PARAMS_TO_IGNORE = [
|
|
21
|
-
"gclid",
|
|
22
|
-
"gbraid",
|
|
23
|
-
"gdftrk",
|
|
24
|
-
"_ga",
|
|
25
|
-
"mc_.*",
|
|
26
|
-
"trk_.*",
|
|
27
|
-
"utm_.*",
|
|
28
|
-
"sc_.*",
|
|
29
|
-
"dm_i",
|
|
30
|
-
"_ke",
|
|
31
|
-
"fbclid",
|
|
32
|
-
"qitc",
|
|
33
|
-
"queryID",
|
|
34
|
-
"indexName",
|
|
35
|
-
"objectID",
|
|
36
|
-
"utm_source",
|
|
37
|
-
"utm_medium",
|
|
38
|
-
"utm_campaign",
|
|
39
|
-
"gad_.*",
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Filter an array of [key, value] pairs, dropping any whose key matches
|
|
44
|
-
* one of the `SEARCH_PARAMS_TO_IGNORE` regex patterns. Pure function so
|
|
45
|
-
* callers can sort/transform after.
|
|
46
|
-
*/
|
|
47
|
-
export const filterSearchParams = (params: [string, string][]): [string, string][] => {
|
|
48
|
-
return params.filter(
|
|
49
|
-
([key]) => !SEARCH_PARAMS_TO_IGNORE.find((matchKey) => new RegExp(matchKey).test(key)),
|
|
50
|
-
);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Strip the tracking params from a URL (or URL string) and return the
|
|
55
|
-
* cleaned `href`. Used by Magento PDP/PLP loaders as the cache key
|
|
56
|
-
* basis so cache lookups don't depend on the visitor's referer chain.
|
|
57
|
-
*/
|
|
58
|
-
export const filterSearchParamsFromURL = (defaultURL: string | URL): string => {
|
|
59
|
-
const url = new URL(defaultURL);
|
|
60
|
-
const paramsArray = Array.from(url.searchParams.entries());
|
|
61
|
-
const filteredParams = filterSearchParams(paramsArray);
|
|
62
|
-
|
|
63
|
-
url.search = "";
|
|
64
|
-
for (const [key, value] of filteredParams) {
|
|
65
|
-
url.searchParams.append(key, value);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return url.href;
|
|
69
|
-
};
|