@cobrastyle/adapter-magento2 1.0.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.
@@ -0,0 +1,4 @@
1
+ /** Code→label map for the current request's store, cached per store code. */
2
+ export declare function getAttributeLabels(): Promise<ReadonlyMap<string, string>>;
3
+ /** Clear the per-store attribute-label cache (tests / after config changes). */
4
+ export declare function clearAttributeLabelsCache(): void;
@@ -0,0 +1,56 @@
1
+ import { graphqlFetch } from "./client";
2
+ import { getConfig } from "./config";
3
+ import * as queries from "./queries";
4
+ // `custom_attributesV2` returns only an attribute's `code`, never its storefront
5
+ // label. The code→label mapping is store-wide (not per product), so we fetch it
6
+ // once per store code and cache it, mirroring store-config.ts. Products then
7
+ // join the label in when mapping their technical attributes.
8
+ const cache = new Map();
9
+ const inflight = new Map();
10
+ function currentStoreCode() {
11
+ const config = getConfig();
12
+ let resolved;
13
+ try {
14
+ resolved = config.storeResolver?.();
15
+ }
16
+ catch {
17
+ resolved = undefined;
18
+ }
19
+ return resolved || config.storeCode || "default";
20
+ }
21
+ /** Code→label map for the current request's store, cached per store code. */
22
+ export async function getAttributeLabels() {
23
+ const code = currentStoreCode();
24
+ const cached = cache.get(code);
25
+ if (cached)
26
+ return cached;
27
+ const existing = inflight.get(code);
28
+ if (existing)
29
+ return existing;
30
+ const promise = (async () => {
31
+ let map = new Map();
32
+ try {
33
+ const data = await graphqlFetch(queries.GET_PRODUCT_ATTRIBUTE_LABELS, undefined, { storeCode: code });
34
+ const m = new Map();
35
+ for (const item of data.attributesList?.items ?? []) {
36
+ if (item?.code && item.label)
37
+ m.set(item.code, item.label);
38
+ }
39
+ map = m;
40
+ }
41
+ catch {
42
+ // Labels are cosmetic — a failed metadata fetch must not break the PDP.
43
+ // Fall back to an empty map; the mapper then shows raw codes.
44
+ map = new Map();
45
+ }
46
+ cache.set(code, map);
47
+ return map;
48
+ })().finally(() => inflight.delete(code));
49
+ inflight.set(code, promise);
50
+ return promise;
51
+ }
52
+ /** Clear the per-store attribute-label cache (tests / after config changes). */
53
+ export function clearAttributeLabelsCache() {
54
+ cache.clear();
55
+ inflight.clear();
56
+ }
@@ -0,0 +1,10 @@
1
+ import type { MagentoUserError } from './magento-types';
2
+ /**
3
+ * `error.name` used for cart user-errors (out of stock, not salable, quantity
4
+ * exceeds available, …). Upstream layers match on this to tell a *user* error
5
+ * apart from a stale/invalid cart: a user error means the cart is fine and must
6
+ * be preserved, not cleared and recreated.
7
+ */
8
+ export declare const CART_USER_ERROR = "CartUserError";
9
+ /** Wrap a Magento cart `user_error` in a tagged Error. */
10
+ export declare function makeCartUserError(userError: MagentoUserError): Error;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `error.name` used for cart user-errors (out of stock, not salable, quantity
3
+ * exceeds available, …). Upstream layers match on this to tell a *user* error
4
+ * apart from a stale/invalid cart: a user error means the cart is fine and must
5
+ * be preserved, not cleared and recreated.
6
+ */
7
+ export const CART_USER_ERROR = 'CartUserError';
8
+ /** Wrap a Magento cart `user_error` in a tagged Error. */
9
+ export function makeCartUserError(userError) {
10
+ const err = new Error(userError.message || 'This item could not be added to the cart.');
11
+ err.name = CART_USER_ERROR;
12
+ err.code = userError.code;
13
+ return err;
14
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Thrown when the GraphQL endpoint rejects a request with HTTP 401. Carries the
3
+ * status so callers can distinguish an expired/invalid customer token from other
4
+ * failures and react (e.g. clear the token cookie and fall back to guest).
5
+ */
6
+ export declare class GraphQLAuthError extends Error {
7
+ readonly status: 401;
8
+ constructor(message: string);
9
+ }
10
+ export interface GraphQLClientOptions {
11
+ customerToken?: string;
12
+ storeCode?: string;
13
+ /**
14
+ * Cache behavior for this request (Next.js cache options)
15
+ * - 'force-cache': Cache indefinitely (default for GET-like queries)
16
+ * - 'no-store': Skip cache entirely (for mutations or user-specific data)
17
+ * - number: Revalidate after N seconds
18
+ *
19
+ * NOTE: Mutations and authenticated requests are NEVER cached, regardless of this setting.
20
+ */
21
+ cache?: 'force-cache' | 'no-store' | number;
22
+ /** Tags for on-demand cache revalidation */
23
+ tags?: string[];
24
+ /** Request timeout in milliseconds. Defaults to 30000 (30s). */
25
+ timeout?: number;
26
+ }
27
+ /**
28
+ * Resolve the store code for a request. Precedence:
29
+ * explicit option > per-request resolver > config default > "default".
30
+ * Never throws (a resolver that throws is treated as "no value").
31
+ */
32
+ export declare function resolveStoreCode(optionStoreCode: string | undefined, resolver: (() => string | undefined) | undefined, configStoreCode: string | undefined): string;
33
+ /**
34
+ * Append the store code to the request URL so Next's fetch data cache keys
35
+ * distinctly per store (Magento ignores the extra query param). Prevents
36
+ * identical queries across stores from colliding in the cache.
37
+ */
38
+ export declare function cacheScopedUrl(baseUrl: string, storeCode: string): string;
39
+ export declare function graphqlFetch<T>(query: string, variables?: Record<string, unknown>, options?: GraphQLClientOptions): Promise<T>;
package/dist/client.js ADDED
@@ -0,0 +1,337 @@
1
+ import { getConfig } from './config';
2
+ import { logger } from './logger';
3
+ /**
4
+ * Thrown when the GraphQL endpoint rejects a request with HTTP 401. Carries the
5
+ * status so callers can distinguish an expired/invalid customer token from other
6
+ * failures and react (e.g. clear the token cookie and fall back to guest).
7
+ */
8
+ export class GraphQLAuthError extends Error {
9
+ status = 401;
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'GraphQLAuthError';
13
+ }
14
+ }
15
+ // Sensitive fields that should never be logged
16
+ const SENSITIVE_FIELDS = new Set([
17
+ 'password',
18
+ 'currentPassword',
19
+ 'newPassword',
20
+ 'confirmPassword',
21
+ 'token',
22
+ 'customerToken',
23
+ 'authToken',
24
+ 'accessToken',
25
+ 'refreshToken',
26
+ 'creditCard',
27
+ 'cardNumber',
28
+ 'cvv',
29
+ 'cvc',
30
+ 'securityCode',
31
+ 'ssn',
32
+ 'socialSecurityNumber',
33
+ 'secret',
34
+ 'apiKey',
35
+ 'apiSecret',
36
+ 'pin',
37
+ 'otp',
38
+ 'mfaCode',
39
+ 'twoFactorCode',
40
+ ]);
41
+ /**
42
+ * Recursively redact sensitive fields from an object for safe logging.
43
+ * Creates a deep copy - does not mutate the original.
44
+ */
45
+ function redactSensitiveData(data) {
46
+ if (data === null || data === undefined)
47
+ return data;
48
+ if (typeof data !== 'object')
49
+ return data;
50
+ if (Array.isArray(data)) {
51
+ return data.map(item => redactSensitiveData(item));
52
+ }
53
+ const redacted = {};
54
+ for (const [key, value] of Object.entries(data)) {
55
+ if (SENSITIVE_FIELDS.has(key) || SENSITIVE_FIELDS.has(key.toLowerCase())) {
56
+ redacted[key] = '[REDACTED]';
57
+ }
58
+ else if (typeof value === 'object' && value !== null) {
59
+ redacted[key] = redactSensitiveData(value);
60
+ }
61
+ else {
62
+ redacted[key] = value;
63
+ }
64
+ }
65
+ return redacted;
66
+ }
67
+ /**
68
+ * Validate that a URL is secure (HTTPS) in production environments.
69
+ * Returns an error message if validation fails, null if valid.
70
+ */
71
+ function validateSecureUrl(url, allowInsecure) {
72
+ try {
73
+ const parsed = new URL(url);
74
+ const isProduction = process.env.NODE_ENV === 'production';
75
+ const isDevelopment = process.env.NODE_ENV === 'development';
76
+ const isLocalhost = ['localhost', '127.0.0.1', '::1'].includes(parsed.hostname);
77
+ const isPrivateNetwork = /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/.test(parsed.hostname);
78
+ // In production, require HTTPS unless explicitly overridden or localhost/private network
79
+ if (isProduction && !isLocalhost && !isPrivateNetwork && !allowInsecure && parsed.protocol !== 'https:') {
80
+ return `Insecure connection blocked: API endpoint must use HTTPS in production`;
81
+ }
82
+ // Warn in development about insecure connections (but don't block)
83
+ if (isDevelopment && !isLocalhost && !isPrivateNetwork && parsed.protocol !== 'https:') {
84
+ console.warn('[Magento2] WARNING: Using insecure HTTP connection. Use HTTPS in production.');
85
+ }
86
+ return null;
87
+ }
88
+ catch {
89
+ return 'Invalid GraphQL endpoint URL';
90
+ }
91
+ }
92
+ /**
93
+ * Generate a unique request ID for tracing.
94
+ */
95
+ function generateRequestId() {
96
+ const timestamp = Date.now().toString(36);
97
+ const random = Math.random().toString(36).substring(2, 10);
98
+ return `mg2-${timestamp}-${random}`;
99
+ }
100
+ // Extract operation name from GraphQL query
101
+ const extractOperationName = (query) => {
102
+ const match = query.match(/(?:query|mutation)\s+(\w+)/);
103
+ return match?.[1] ?? 'Anonymous';
104
+ };
105
+ // Check if the query is a mutation
106
+ const isMutation = (query) => {
107
+ return /^\s*mutation\s/i.test(query.trim());
108
+ };
109
+ const DEFAULT_TIMEOUT_MS = 30_000;
110
+ /**
111
+ * Resolve the store code for a request. Precedence:
112
+ * explicit option > per-request resolver > config default > "default".
113
+ * Never throws (a resolver that throws is treated as "no value").
114
+ */
115
+ export function resolveStoreCode(optionStoreCode, resolver, configStoreCode) {
116
+ let resolved;
117
+ try {
118
+ resolved = resolver?.();
119
+ }
120
+ catch {
121
+ resolved = undefined;
122
+ }
123
+ return optionStoreCode || resolved || configStoreCode || "default";
124
+ }
125
+ /**
126
+ * Append the store code to the request URL so Next's fetch data cache keys
127
+ * distinctly per store (Magento ignores the extra query param). Prevents
128
+ * identical queries across stores from colliding in the cache.
129
+ */
130
+ export function cacheScopedUrl(baseUrl, storeCode) {
131
+ const sep = baseUrl.includes("?") ? "&" : "?";
132
+ return `${baseUrl}${sep}store=${encodeURIComponent(storeCode)}`;
133
+ }
134
+ export async function graphqlFetch(query, variables, options = {}) {
135
+ const config = getConfig();
136
+ const storeCode = resolveStoreCode(options.storeCode, config.storeResolver, config.storeCode);
137
+ const operationName = extractOperationName(query);
138
+ const startTime = performance.now();
139
+ const timeoutMs = options.timeout ?? config.timeout ?? DEFAULT_TIMEOUT_MS;
140
+ const isDev = process.env.NODE_ENV === 'development';
141
+ // Validate URL security in production
142
+ const urlError = validateSecureUrl(config.baseUrl, config.allowInsecure);
143
+ if (urlError) {
144
+ throw new Error(urlError);
145
+ }
146
+ // Log the outgoing request with sensitive data redacted
147
+ logger.request(query, redactSensitiveData(variables), {
148
+ storeCode,
149
+ hasAuth: !!options.customerToken,
150
+ });
151
+ const requestId = generateRequestId();
152
+ const headers = {
153
+ 'Content-Type': 'application/json',
154
+ 'Accept': 'application/json',
155
+ 'Store': storeCode,
156
+ // Security headers
157
+ 'X-Request-ID': requestId,
158
+ 'X-Requested-With': 'XMLHttpRequest',
159
+ // Prevent MIME type sniffing
160
+ 'X-Content-Type-Options': 'nosniff',
161
+ };
162
+ // Add any custom headers from config
163
+ if (config.headers) {
164
+ Object.assign(headers, config.headers);
165
+ }
166
+ if (options.customerToken) {
167
+ // Validate token format (basic sanity check)
168
+ if (typeof options.customerToken !== 'string' || options.customerToken.length === 0) {
169
+ throw new Error('Invalid customer token: token must be a non-empty string');
170
+ }
171
+ headers['Authorization'] = `Bearer ${options.customerToken}`;
172
+ }
173
+ // Build Next.js cache options
174
+ const nextOptions = {};
175
+ // NEVER cache:
176
+ // 1. Mutations (side effects)
177
+ // 2. Authenticated requests (user-specific data)
178
+ // 3. Explicitly disabled cache
179
+ const isQueryMutation = isMutation(query);
180
+ const isAuthenticated = !!options.customerToken;
181
+ const isExplicitNoStore = options.cache === 'no-store';
182
+ const shouldCache = !isQueryMutation && !isAuthenticated && !isExplicitNoStore;
183
+ if (shouldCache) {
184
+ if (typeof options.cache === 'number') {
185
+ nextOptions.revalidate = options.cache;
186
+ }
187
+ else if (options.cache !== 'no-store') {
188
+ // Default: revalidate every 60 seconds for non-authenticated requests
189
+ nextOptions.revalidate = 60;
190
+ }
191
+ nextOptions.tags = [`store:${storeCode}`, ...(options.tags ?? [])];
192
+ }
193
+ const body = JSON.stringify({ query, variables });
194
+ // Handle SSL verification for local development against self-signed certs.
195
+ // Hard-guarded to non-production so a stray SKIP_SSL can never weaken TLS in
196
+ // production. Node reads NODE_TLS_REJECT_UNAUTHORIZED per TLS connection, so
197
+ // setting it here takes effect for the fetch below.
198
+ if (config.skipSsl && process.env.NODE_ENV !== 'production') {
199
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0') {
200
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
201
+ console.warn('[Magento2] TLS certificate verification disabled (SKIP_SSL). Development only — never enable in production.');
202
+ }
203
+ }
204
+ // Retry only side-effect-free queries: replaying a mutation could double-add
205
+ // to cart, place duplicate orders, etc. Mutations get a single attempt.
206
+ const maxAttempts = isQueryMutation ? 1 : Math.max(1, (config.maxRetries ?? 2) + 1);
207
+ const baseDelayMs = config.retryDelayMs ?? 150;
208
+ let lastError;
209
+ // A 401 on an authenticated request means the customer token expired. We give
210
+ // the app one chance to swap in a fresh token (via config.tokenRefresher) and
211
+ // replay the request. Guarded to a single attempt so a persistently rejected
212
+ // token can't loop forever.
213
+ let authRefreshed = false;
214
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
215
+ const isLastAttempt = attempt === maxAttempts;
216
+ const controller = new AbortController();
217
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
218
+ const fetchOptions = {
219
+ method: 'POST',
220
+ headers,
221
+ body,
222
+ signal: controller.signal,
223
+ ...(shouldCache && Object.keys(nextOptions).length > 0 && { next: nextOptions }),
224
+ ...(!shouldCache && { cache: 'no-store' }),
225
+ };
226
+ try {
227
+ const response = await fetch(cacheScopedUrl(config.baseUrl, storeCode), fetchOptions);
228
+ const duration = Math.round(performance.now() - startTime);
229
+ // Helper for safe error messages (show URL only in dev)
230
+ const safeUrl = isDev ? config.baseUrl : 'GraphQL endpoint';
231
+ if (!response.ok) {
232
+ const text = await response.text();
233
+ // Transient server errors (5xx) are safe to retry for queries.
234
+ if (response.status >= 500 && !isLastAttempt) {
235
+ logger.error(operationName, new Error(`HTTP ${response.status} (attempt ${attempt}/${maxAttempts}), retrying`), duration);
236
+ await delay(backoffMs(baseDelayMs, attempt));
237
+ continue;
238
+ }
239
+ // An authenticated request rejected with 401 usually means the customer
240
+ // token expired. Ask the app to refresh it once, then replay with the
241
+ // fresh token. A 401 means the server never processed the request, so
242
+ // replaying is safe even for mutations (nothing was mutated).
243
+ if (response.status === 401 &&
244
+ !authRefreshed &&
245
+ options.customerToken &&
246
+ config.tokenRefresher) {
247
+ authRefreshed = true;
248
+ const freshToken = await config.tokenRefresher();
249
+ if (freshToken) {
250
+ headers['Authorization'] = `Bearer ${freshToken}`;
251
+ logger.error(operationName, new Error(`HTTP 401, refreshed token and retrying [${requestId}]`), duration);
252
+ attempt--; // don't consume a transient-retry attempt
253
+ continue;
254
+ }
255
+ }
256
+ logger.error(operationName, new Error(`HTTP ${response.status}: ${text}`), duration);
257
+ if (response.status === 401) {
258
+ throw new GraphQLAuthError(`GraphQL request failed: 401 ${response.statusText} [${requestId}]`);
259
+ }
260
+ throw new Error(`GraphQL request failed: ${response.status} ${response.statusText} [${requestId}]`);
261
+ }
262
+ const text = await response.text();
263
+ // Check if response is HTML (likely an error page)
264
+ if (text.trim().startsWith('<!') || text.trim().startsWith('<html')) {
265
+ logger.error(operationName, new Error(`Received HTML instead of JSON. URL: ${config.baseUrl}`), duration);
266
+ throw new Error(`${safeUrl} returned HTML instead of JSON [${requestId}]`);
267
+ }
268
+ // Check if response is empty
269
+ if (!text.trim()) {
270
+ logger.error(operationName, new Error(`Empty response from ${config.baseUrl}`), duration);
271
+ throw new Error(`Empty response from ${safeUrl} [${requestId}]`);
272
+ }
273
+ let json;
274
+ try {
275
+ json = JSON.parse(text);
276
+ }
277
+ catch (parseError) {
278
+ logger.error(operationName, new Error(`Invalid JSON: ${text.substring(0, 200)}`), duration);
279
+ throw new Error(`${safeUrl} returned invalid JSON [${requestId}]`);
280
+ }
281
+ if (json.errors && json.errors.length > 0) {
282
+ const messages = json.errors.map((e) => e.message).join(', ');
283
+ logger.error(operationName, new Error(messages), duration);
284
+ throw new Error(`GraphQL errors: ${messages} [${requestId}]`);
285
+ }
286
+ if (!json.data) {
287
+ logger.error(operationName, new Error('No data returned'), duration);
288
+ throw new Error(`No data returned from GraphQL [${requestId}]`);
289
+ }
290
+ // Log successful response
291
+ logger.response(operationName, json.data, duration);
292
+ return json.data;
293
+ }
294
+ catch (error) {
295
+ const duration = Math.round(performance.now() - startTime);
296
+ const isTimeout = error instanceof DOMException && error.name === 'AbortError';
297
+ // A raw fetch failure (DNS, connection reset/refused) surfaces as a
298
+ // TypeError before the request reaches the server, so it's safe to retry.
299
+ const isNetworkError = error instanceof TypeError;
300
+ // Retry transient transport failures for side-effect-free queries only.
301
+ if ((isTimeout || isNetworkError) && !isLastAttempt) {
302
+ lastError = error;
303
+ logger.error(operationName, new Error(`${isTimeout ? `Timeout after ${timeoutMs}ms` : 'Network error'} (attempt ${attempt}/${maxAttempts}), retrying`), duration);
304
+ await delay(backoffMs(baseDelayMs, attempt));
305
+ continue;
306
+ }
307
+ if (isTimeout) {
308
+ logger.error(operationName, new Error(`Request timed out after ${timeoutMs}ms`), duration);
309
+ throw new Error(`GraphQL request timed out after ${timeoutMs}ms for ${operationName} [${requestId}]`);
310
+ }
311
+ if (!(error instanceof Error && error.message.includes('GraphQL'))) {
312
+ // Only log if not already logged above
313
+ logger.error(operationName, error, duration);
314
+ }
315
+ throw error;
316
+ }
317
+ finally {
318
+ clearTimeout(timeoutId);
319
+ }
320
+ }
321
+ // Unreachable in practice: the loop either returns or throws. Rethrow the
322
+ // last transport error as a defensive fallback for the type checker.
323
+ throw lastError instanceof Error
324
+ ? lastError
325
+ : new Error(`GraphQL request failed after ${maxAttempts} attempts for ${operationName} [${requestId}]`);
326
+ }
327
+ /** Sleep helper for retry backoff. */
328
+ function delay(ms) {
329
+ return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
330
+ }
331
+ /** Exponential backoff with jitter, capped at 2s. */
332
+ function backoffMs(base, attempt) {
333
+ if (base <= 0)
334
+ return 0;
335
+ const exp = Math.min(2000, base * 2 ** (attempt - 1));
336
+ return exp + Math.floor(Math.random() * base);
337
+ }
@@ -0,0 +1,52 @@
1
+ import type { AdapterConfig } from '@cobrastyle/shared-types';
2
+ import type { GraphQLClientOptions } from './client';
3
+ export interface MagentoConfig extends AdapterConfig {
4
+ baseUrl: string;
5
+ storeCode?: string;
6
+ skipSsl?: boolean;
7
+ timeout?: number;
8
+ rootCategoryId?: number;
9
+ /**
10
+ * Number of times to retry a *side-effect-free* query after a transient
11
+ * failure (network error, timeout, or 5xx). Mutations are never retried.
12
+ * @default 2
13
+ */
14
+ maxRetries?: number;
15
+ /**
16
+ * Base backoff in milliseconds between retries (grows exponentially with a
17
+ * little jitter). Set to 0 to disable the delay (used in tests).
18
+ * @default 150
19
+ */
20
+ retryDelayMs?: number;
21
+ /**
22
+ * Allow insecure (HTTP) connections.
23
+ * WARNING: Only use in development! Never enable in production.
24
+ * @default false
25
+ */
26
+ allowInsecure?: boolean;
27
+ /** Callback to retrieve the current customer token (e.g. from cookies). */
28
+ customerTokenGetter?: () => Promise<string | undefined>;
29
+ /**
30
+ * Callback invoked when an authenticated request is rejected with HTTP 401
31
+ * (expired customer token). Should attempt to mint a fresh access token
32
+ * (e.g. via a refresh token) and return it, or return undefined if the
33
+ * session can't be recovered. The client replays the failed request once
34
+ * with the returned token.
35
+ */
36
+ tokenRefresher?: () => Promise<string | undefined>;
37
+ /**
38
+ * Callback returning the store code for the current request (e.g. read from a
39
+ * request header the app set from the URL). Used to scope the `Store` header
40
+ * per request. Returns undefined outside a request; resolution then falls
41
+ * back to `storeCode`.
42
+ */
43
+ storeResolver?: () => string | undefined;
44
+ }
45
+ export declare const getConfig: () => MagentoConfig;
46
+ export declare const setConfig: (newConfig: Partial<MagentoConfig>) => void;
47
+ export declare const createConfig: (overrides?: Partial<MagentoConfig>) => MagentoConfig;
48
+ /**
49
+ * Build GraphQL client options with the customer token if available.
50
+ * Merges with any extra options passed in.
51
+ */
52
+ export declare function withCustomerAuth(extra?: GraphQLClientOptions): Promise<GraphQLClientOptions>;
package/dist/config.js ADDED
@@ -0,0 +1,28 @@
1
+ let config = {
2
+ baseUrl: process.env.MAGENTO_URL || 'https://magento.example.com/graphql',
3
+ storeCode: process.env.MAGENTO_STORE || 'default',
4
+ skipSsl: process.env.SKIP_SSL === 'true',
5
+ allowInsecure: process.env.ALLOW_INSECURE === 'true',
6
+ timeout: 30000,
7
+ // If not set, will be fetched dynamically from storeConfig
8
+ rootCategoryId: process.env.MAGENTO_ROOT_CATEGORY_ID ? parseInt(process.env.MAGENTO_ROOT_CATEGORY_ID, 10) : undefined,
9
+ };
10
+ export const getConfig = () => config;
11
+ export const setConfig = (newConfig) => {
12
+ config = { ...config, ...newConfig };
13
+ };
14
+ export const createConfig = (overrides = {}) => ({
15
+ ...config,
16
+ ...overrides,
17
+ });
18
+ /**
19
+ * Build GraphQL client options with the customer token if available.
20
+ * Merges with any extra options passed in.
21
+ */
22
+ export async function withCustomerAuth(extra = {}) {
23
+ const token = await config.customerTokenGetter?.();
24
+ if (token) {
25
+ return { ...extra, customerToken: token };
26
+ }
27
+ return extra;
28
+ }
@@ -0,0 +1,7 @@
1
+ export { magento2Adapter, magento2Adapter as default } from './adapter';
2
+ export { getConfig, setConfig, createConfig, withCustomerAuth } from './config';
3
+ export type { MagentoConfig } from './config';
4
+ export { graphqlFetch, GraphQLAuthError } from './client';
5
+ export { logger, MagentoLogger } from './logger';
6
+ export * as queries from './queries';
7
+ export * as mappers from './mappers';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export { magento2Adapter, magento2Adapter as default } from './adapter';
2
+ export { getConfig, setConfig, createConfig, withCustomerAuth } from './config';
3
+ export { graphqlFetch, GraphQLAuthError } from './client';
4
+ export { logger, MagentoLogger } from './logger';
5
+ export * as queries from './queries';
6
+ export * as mappers from './mappers';
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Magento2 Adapter Logger
3
+ *
4
+ * A clear, colorful logger for tracking GraphQL requests and responses.
5
+ * Enable/disable via MAGENTO_DEBUG environment variable.
6
+ */
7
+ interface LogOptions {
8
+ showVariables?: boolean;
9
+ showFullQuery?: boolean;
10
+ showResponseData?: boolean;
11
+ showFullResponse?: boolean;
12
+ maxDataLength?: number;
13
+ }
14
+ declare class MagentoLogger {
15
+ private requestCount;
16
+ private options;
17
+ constructor(options?: LogOptions);
18
+ private log;
19
+ private getPrefix;
20
+ /**
21
+ * Log an outgoing GraphQL request
22
+ */
23
+ request(query: string, variables?: Record<string, unknown>, meta?: {
24
+ storeCode?: string;
25
+ hasAuth?: boolean;
26
+ }): void;
27
+ /**
28
+ * Log a successful response
29
+ */
30
+ response(operationName: string, data: unknown, duration: number): void;
31
+ /**
32
+ * Log an error response
33
+ */
34
+ error(operationName: string, error: unknown, duration?: number): void;
35
+ /**
36
+ * Generate a preview of response data
37
+ */
38
+ private getDataPreview;
39
+ /**
40
+ * Log general info
41
+ */
42
+ info(message: string, data?: unknown): void;
43
+ /**
44
+ * Log a warning
45
+ */
46
+ warn(message: string, data?: unknown): void;
47
+ /**
48
+ * Get request statistics
49
+ */
50
+ getStats(): {
51
+ totalRequests: number;
52
+ };
53
+ /**
54
+ * Reset statistics
55
+ */
56
+ resetStats(): void;
57
+ }
58
+ export declare const logger: MagentoLogger;
59
+ export { MagentoLogger };