@cobrastyle/adapter-magento2 1.1.1 → 1.3.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/dist/adapter.js CHANGED
@@ -232,6 +232,16 @@ export const magento2Adapter = {
232
232
  return mapCartWithUrls(data.cart);
233
233
  }
234
234
  catch (error) {
235
+ // An ownership refusal means the id is unusable for this session (e.g. a
236
+ // guest cart id left in the cookie after login). Reporting it as `null`
237
+ // would render an empty cart while the quote still exists and leave the bad
238
+ // id in place, so let it propagate; callers can drop the id and start over.
239
+ if (error instanceof Error &&
240
+ error.message.toLowerCase().includes('cannot perform operations on cart')) {
241
+ throw error;
242
+ }
243
+ // Anything else (missing cart, transport failure) stays soft: the cart is
244
+ // simply unavailable and the page must still render.
235
245
  return null;
236
246
  }
237
247
  },
package/dist/client.js CHANGED
@@ -254,10 +254,15 @@ export async function graphqlFetch(query, variables, options = {}) {
254
254
  }
255
255
  }
256
256
  logger.error(operationName, new Error(`HTTP ${response.status}: ${text}`), duration);
257
+ // Carry the backend's reason, not just the status: callers need it to tell
258
+ // an expired cart from an ownership problem, and without it a failure can
259
+ // only be diagnosed by reproducing the request by hand.
260
+ const reason = extractErrorReason(text);
261
+ const detail = reason ? `: ${reason}` : '';
257
262
  if (response.status === 401) {
258
- throw new GraphQLAuthError(`GraphQL request failed: 401 ${response.statusText} [${requestId}]`);
263
+ throw new GraphQLAuthError(`GraphQL request failed: 401 ${response.statusText}${detail} [${requestId}]`.replace(/ +/g, ' '));
259
264
  }
260
- throw new Error(`GraphQL request failed: ${response.status} ${response.statusText} [${requestId}]`);
265
+ throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}${detail} [${requestId}]`.replace(/ +/g, ' '));
261
266
  }
262
267
  const text = await response.text();
263
268
  // Check if response is HTML (likely an error page)
@@ -324,6 +329,26 @@ export async function graphqlFetch(query, variables, options = {}) {
324
329
  ? lastError
325
330
  : new Error(`GraphQL request failed after ${maxAttempts} attempts for ${operationName} [${requestId}]`);
326
331
  }
332
+ /**
333
+ * Pull a human-readable reason out of a non-2xx response body: the first GraphQL
334
+ * error message when the body is a GraphQL envelope, otherwise a trimmed excerpt
335
+ * of whatever the server sent (proxy errors, HTML pages, empty bodies).
336
+ */
337
+ function extractErrorReason(body) {
338
+ const text = body?.trim();
339
+ if (!text)
340
+ return undefined;
341
+ try {
342
+ const parsed = JSON.parse(text);
343
+ const message = parsed.errors?.find((e) => e?.message)?.message;
344
+ if (message)
345
+ return message;
346
+ }
347
+ catch {
348
+ // Not JSON — fall through to the excerpt.
349
+ }
350
+ return text.slice(0, 200);
351
+ }
327
352
  /** Sleep helper for retry backoff. */
328
353
  function delay(ms) {
329
354
  return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
package/dist/config.d.ts CHANGED
@@ -42,6 +42,9 @@ export interface MagentoConfig extends AdapterConfig {
42
42
  */
43
43
  storeResolver?: () => string | undefined;
44
44
  }
45
+ export declare const isConfigured: () => boolean;
46
+ /** Warn once per process that the client is running on defaults only. */
47
+ export declare function warnIfUnconfigured(context: string): void;
45
48
  export declare const getConfig: () => MagentoConfig;
46
49
  export declare const setConfig: (newConfig: Partial<MagentoConfig>) => void;
47
50
  export declare const createConfig: (overrides?: Partial<MagentoConfig>) => MagentoConfig;
package/dist/config.js CHANGED
@@ -1,5 +1,8 @@
1
1
  let config = {
2
- baseUrl: process.env.MAGENTO_URL || 'https://magento.example.com/graphql',
2
+ // Deliberately empty rather than a placeholder host: an unset MAGENTO_URL is a
3
+ // configuration error, and pointing it at a domain nobody owns turns that into
4
+ // confusing request failures instead.
5
+ baseUrl: process.env.MAGENTO_URL || '',
3
6
  storeCode: process.env.MAGENTO_STORE || 'default',
4
7
  skipSsl: process.env.SKIP_SSL === 'true',
5
8
  allowInsecure: process.env.ALLOW_INSECURE === 'true',
@@ -7,9 +10,32 @@ let config = {
7
10
  // If not set, will be fetched dynamically from storeConfig
8
11
  rootCategoryId: process.env.MAGENTO_ROOT_CATEGORY_ID ? parseInt(process.env.MAGENTO_ROOT_CATEGORY_ID, 10) : undefined,
9
12
  };
13
+ /**
14
+ * Whether the consuming app has supplied its configuration yet.
15
+ *
16
+ * The env-derived defaults above make an *unconfigured* client look usable, but
17
+ * the injected callbacks — `customerTokenGetter`, `tokenRefresher`,
18
+ * `storeResolver` — have no defaults. Requests then go out unauthenticated and
19
+ * without per-request store resolution, which surfaces far downstream as an
20
+ * authorization error on customer-owned data. Consumers of the exported client
21
+ * (`graphqlFetch`, `withCustomerAuth`) need to be told.
22
+ */
23
+ let configured = false;
24
+ let warnedUnconfigured = false;
25
+ export const isConfigured = () => configured;
26
+ /** Warn once per process that the client is running on defaults only. */
27
+ export function warnIfUnconfigured(context) {
28
+ if (configured || warnedUnconfigured)
29
+ return;
30
+ warnedUnconfigured = true;
31
+ console.warn(`[magento2] ${context} before setConfig() ran: requests will be sent unauthenticated ` +
32
+ 'and without per-request store resolution. Call setConfig() (or load the adapter ' +
33
+ 'through configureStorefront/getAdapter) before using the exported GraphQL client.');
34
+ }
10
35
  export const getConfig = () => config;
11
36
  export const setConfig = (newConfig) => {
12
37
  config = { ...config, ...newConfig };
38
+ configured = true;
13
39
  };
14
40
  export const createConfig = (overrides = {}) => ({
15
41
  ...config,
@@ -20,6 +46,7 @@ export const createConfig = (overrides = {}) => ({
20
46
  * Merges with any extra options passed in.
21
47
  */
22
48
  export async function withCustomerAuth(extra = {}) {
49
+ warnIfUnconfigured('withCustomerAuth() was called');
23
50
  const token = await config.customerTokenGetter?.();
24
51
  if (token) {
25
52
  return { ...extra, customerToken: token };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { magento2Adapter, magento2Adapter as default } from './adapter';
2
- export { getConfig, setConfig, createConfig, withCustomerAuth } from './config';
2
+ export { getConfig, setConfig, createConfig, withCustomerAuth, isConfigured } from './config';
3
3
  export type { MagentoConfig } from './config';
4
4
  export { graphqlFetch, GraphQLAuthError } from './client';
5
5
  export { logger, MagentoLogger } from './logger';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { magento2Adapter, magento2Adapter as default } from './adapter';
2
- export { getConfig, setConfig, createConfig, withCustomerAuth } from './config';
2
+ export { getConfig, setConfig, createConfig, withCustomerAuth, isConfigured } from './config';
3
3
  export { graphqlFetch, GraphQLAuthError } from './client';
4
4
  export { logger, MagentoLogger } from './logger';
5
5
  export * as queries from './queries';
@@ -301,6 +301,11 @@ export interface MagentoCategory {
301
301
  breadcrumbs?: MagentoCategoryBreadcrumb[] | null;
302
302
  meta_title?: string;
303
303
  meta_description?: string;
304
+ /**
305
+ * PRODUCTS | PAGE | PRODUCTS_AND_PAGE. Magento's enum is a plain String in the
306
+ * GraphQL schema, so it is typed loosely here and narrowed by the mapper.
307
+ */
308
+ display_mode?: string | null;
304
309
  }
305
310
  export interface MagentoCmsPage {
306
311
  identifier: string;
package/dist/mappers.js CHANGED
@@ -462,8 +462,15 @@ export const mapCategory = (cat, suffix = '', ancestorSegments) => {
462
462
  })),
463
463
  metaTitle: cat.meta_title,
464
464
  metaDescription: cat.meta_description,
465
+ displayMode: mapDisplayMode(cat.display_mode),
465
466
  };
466
467
  };
468
+ /**
469
+ * Narrow Magento's `display_mode` to our union. Unknown or absent values map to
470
+ * undefined rather than a guessed default, so the storefront's own fallback
471
+ * (render products, the historic behaviour) stays in one place.
472
+ */
473
+ const mapDisplayMode = (mode) => mode === 'PRODUCTS' || mode === 'PAGE' || mode === 'PRODUCTS_AND_PAGE' ? mode : undefined;
467
474
  // Magento exposes `include_in_menu` as a per-category flag (Boolean, or 0/1 in
468
475
  // older schemas). Categories may exist and be browsable while intentionally
469
476
  // hidden from navigation, so we must not render them in the menu. A missing
package/dist/queries.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export declare const GET_PRODUCT_BY_URL_KEY = "\n query GetProductByUrlKey($urlKey: String!) {\n products(filter: { url_key: { eq: $urlKey } }) {\n items {\n __typename\n id\n sku\n name\n url_key\n description { html }\n short_description { html }\n meta_title\n meta_description\n stock_status\n price_range {\n minimum_price {\n regular_price { value currency }\n final_price { value currency }\n discount { amount_off percent_off }\n }\n maximum_price {\n final_price { value currency }\n }\n }\n price_tiers {\n quantity\n final_price { value currency }\n discount { amount_off percent_off }\n }\n media_gallery {\n url\n label\n position\n }\n categories {\n id\n name\n url_key\n breadcrumbs {\n category_url_key\n category_level\n }\n }\n custom_attributesV2(filters: { is_visible_on_front: true }) {\n items {\n code\n __typename\n ... on AttributeValue {\n value\n }\n ... on AttributeSelectedOptions {\n selected_options {\n label\n value\n }\n }\n }\n errors {\n message\n }\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_code\n label\n values {\n uid\n label\n swatch_data {\n __typename\n value\n }\n }\n }\n variants {\n product {\n id\n sku\n name\n stock_status\n price_range {\n minimum_price {\n regular_price { value currency }\n final_price { value currency }\n }\n }\n }\n attributes {\n code\n label\n value_index\n }\n }\n }\n ... on GroupedProduct {\n items {\n qty\n position\n product {\n ... on ProductInterface {\n __typename\n id\n uid\n sku\n name\n url_key\n stock_status\n price_range {\n minimum_price {\n regular_price { value currency }\n final_price { value currency }\n discount { amount_off percent_off }\n }\n }\n thumbnail { url label }\n }\n }\n }\n }\n ... on BundleProduct {\n dynamic_price\n price_view\n bundleItems: items {\n uid\n option_id\n title\n type\n required\n position\n options {\n uid\n id\n label\n quantity\n can_change_quantity\n is_default\n price\n price_type\n product {\n sku\n price_range { minimum_price { final_price { value currency } } }\n }\n }\n }\n }\n }\n }\n }\n";
2
2
  export declare const GET_PRODUCT_BY_ID = "\n query GetProductBySku($sku: String!) {\n products(filter: { sku: { eq: $sku } }) {\n items {\n __typename\n id\n sku\n name\n url_key\n description { html }\n short_description { html }\n meta_title\n meta_description\n stock_status\n price_range {\n minimum_price {\n regular_price { value currency }\n final_price { value currency }\n discount { amount_off percent_off }\n }\n maximum_price {\n final_price { value currency }\n }\n }\n price_tiers {\n quantity\n final_price { value currency }\n discount { amount_off percent_off }\n }\n media_gallery {\n url\n label\n position\n }\n categories {\n id\n name\n url_key\n breadcrumbs {\n category_url_key\n category_level\n }\n }\n custom_attributesV2(filters: { is_visible_on_front: true }) {\n items {\n code\n __typename\n ... on AttributeValue {\n value\n }\n ... on AttributeSelectedOptions {\n selected_options {\n label\n value\n }\n }\n }\n errors {\n message\n }\n }\n ... on ConfigurableProduct {\n configurable_options {\n id\n attribute_code\n label\n values {\n uid\n label\n swatch_data {\n __typename\n value\n }\n }\n }\n variants {\n product {\n id\n sku\n name\n stock_status\n price_range {\n minimum_price {\n regular_price { value currency }\n final_price { value currency }\n }\n }\n }\n attributes {\n code\n label\n value_index\n }\n }\n }\n ... on GroupedProduct {\n items {\n qty\n position\n product {\n ... on ProductInterface {\n __typename\n id\n uid\n sku\n name\n url_key\n stock_status\n price_range {\n minimum_price {\n regular_price { value currency }\n final_price { value currency }\n discount { amount_off percent_off }\n }\n }\n thumbnail { url label }\n }\n }\n }\n }\n ... on BundleProduct {\n dynamic_price\n price_view\n bundleItems: items {\n uid\n option_id\n title\n type\n required\n position\n options {\n uid\n id\n label\n quantity\n can_change_quantity\n is_default\n price\n price_type\n product {\n sku\n price_range { minimum_price { final_price { value currency } } }\n }\n }\n }\n }\n }\n }\n }\n";
3
3
  export declare const GET_PRODUCT_ATTRIBUTE_LABELS = "\n query GetProductAttributeLabels {\n attributesList(entityType: CATALOG_PRODUCT, filters: { is_visible_on_front: true }) {\n items {\n code\n label\n }\n errors {\n message\n }\n }\n }\n";
4
- export declare const GET_CATEGORY_BY_URL_KEY = "\n query GetCategoryByUrlKey($urlKey: String!) {\n categoryList(filters: { url_path: { eq: $urlKey } }) {\n id\n uid\n name\n url_key\n url_path\n description\n image\n meta_title\n meta_description\n product_count\n breadcrumbs {\n category_id\n category_uid\n category_name\n category_url_key\n category_url_path\n category_level\n }\n children {\n id\n uid\n name\n url_key\n url_path\n product_count\n }\n }\n }\n";
5
- export declare const GET_CATEGORY_BY_URL_KEY_SINGLE = "\n query GetCategoryByUrlKeySingle($urlKey: String!) {\n categoryList(filters: { url_key: { eq: $urlKey } }) {\n id\n uid\n name\n url_key\n url_path\n description\n image\n meta_title\n meta_description\n product_count\n breadcrumbs {\n category_id\n category_uid\n category_name\n category_url_key\n category_url_path\n category_level\n }\n children {\n id\n uid\n name\n url_key\n url_path\n product_count\n }\n }\n }\n";
6
- export declare const GET_CATEGORY_BY_UID = "\n query GetCategoryByUid($uid: String!) {\n categoryList(filters: { category_uid: { eq: $uid } }) {\n id\n uid\n name\n url_key\n url_path\n description\n image\n meta_title\n meta_description\n product_count\n breadcrumbs {\n category_id\n category_uid\n category_name\n category_url_key\n category_url_path\n category_level\n }\n children {\n id\n uid\n name\n url_key\n url_path\n product_count\n }\n }\n }\n";
4
+ export declare const GET_CATEGORY_BY_URL_KEY = "\n query GetCategoryByUrlKey($urlKey: String!) {\n categoryList(filters: { url_path: { eq: $urlKey } }) {\n id\n uid\n name\n url_key\n url_path\n description\n image\n meta_title\n meta_description\n product_count\n display_mode\n breadcrumbs {\n category_id\n category_uid\n category_name\n category_url_key\n category_url_path\n category_level\n }\n children {\n id\n uid\n name\n url_key\n url_path\n product_count\n }\n }\n }\n";
5
+ export declare const GET_CATEGORY_BY_URL_KEY_SINGLE = "\n query GetCategoryByUrlKeySingle($urlKey: String!) {\n categoryList(filters: { url_key: { eq: $urlKey } }) {\n id\n uid\n name\n url_key\n url_path\n description\n image\n meta_title\n meta_description\n product_count\n display_mode\n breadcrumbs {\n category_id\n category_uid\n category_name\n category_url_key\n category_url_path\n category_level\n }\n children {\n id\n uid\n name\n url_key\n url_path\n product_count\n }\n }\n }\n";
6
+ export declare const GET_CATEGORY_BY_UID = "\n query GetCategoryByUid($uid: String!) {\n categoryList(filters: { category_uid: { eq: $uid } }) {\n id\n uid\n name\n url_key\n url_path\n description\n image\n meta_title\n meta_description\n product_count\n display_mode\n breadcrumbs {\n category_id\n category_uid\n category_name\n category_url_key\n category_url_path\n category_level\n }\n children {\n id\n uid\n name\n url_key\n url_path\n product_count\n }\n }\n }\n";
7
7
  export declare const GET_CATEGORY_SUGGESTIONS = "\n query GetCategorySuggestions($name: String!) {\n categoryList(filters: { name: { match: $name } }) {\n uid\n name\n url_key\n url_path\n }\n }\n";
8
8
  export declare const GET_CATEGORY_PRODUCTS = "\n query GetCategoryProducts(\n $pageSize: Int!\n $currentPage: Int!\n $filter: ProductAttributeFilterInput!\n $sort: ProductAttributeSortInput\n ) {\n products(\n filter: $filter\n pageSize: $pageSize\n currentPage: $currentPage\n sort: $sort\n ) {\n total_count\n page_info {\n current_page\n page_size\n total_pages\n }\n items {\n id\n sku\n name\n url_key\n stock_status\n price_range {\n minimum_price {\n regular_price { value currency }\n final_price { value currency }\n discount { amount_off percent_off }\n }\n }\n price_tiers {\n quantity\n final_price { value currency }\n discount { amount_off percent_off }\n }\n thumbnail {\n url\n label\n }\n }\n aggregations(filter: { category: { includeDirectChildrenOnly: false } }) {\n attribute_code\n label\n options {\n value\n label\n count\n }\n }\n sort_fields {\n default\n options {\n value\n label\n }\n }\n }\n }\n";
9
9
  export declare const GET_STORE_CONFIG = "\n query GetStoreConfig {\n storeConfig {\n root_category_uid\n root_category_id\n store_code\n store_name\n base_currency_code\n default_display_currency_code\n locale\n timezone\n category_url_suffix\n product_url_suffix\n use_store_in_url\n cms_home_page\n cms_no_route\n default_title\n default_description\n default_keywords\n title_prefix\n title_suffix\n title_separator\n }\n }\n";
package/dist/queries.js CHANGED
@@ -330,6 +330,7 @@ export const GET_CATEGORY_BY_URL_KEY = `
330
330
  meta_title
331
331
  meta_description
332
332
  product_count
333
+ display_mode
333
334
  breadcrumbs {
334
335
  category_id
335
336
  category_uid
@@ -363,6 +364,7 @@ export const GET_CATEGORY_BY_URL_KEY_SINGLE = `
363
364
  meta_title
364
365
  meta_description
365
366
  product_count
367
+ display_mode
366
368
  breadcrumbs {
367
369
  category_id
368
370
  category_uid
@@ -397,6 +399,7 @@ export const GET_CATEGORY_BY_UID = `
397
399
  meta_title
398
400
  meta_description
399
401
  product_count
402
+ display_mode
400
403
  breadcrumbs {
401
404
  category_id
402
405
  category_uid
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cobrastyle/adapter-magento2",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -13,12 +13,12 @@
13
13
  "dist"
14
14
  ],
15
15
  "dependencies": {
16
- "@cobrastyle/shared-types": "1.1.0"
16
+ "@cobrastyle/shared-types": "1.2.0"
17
17
  },
18
18
  "devDependencies": {
19
19
  "@types/node": "^20.0.0",
20
20
  "typescript": "^5.3.0",
21
- "@cobrastyle/adapter-conformance": "1.0.2"
21
+ "@cobrastyle/adapter-conformance": "1.0.3"
22
22
  },
23
23
  "license": "MIT",
24
24
  "repository": {