@dotcms/client 1.5.1-next.1965 → 1.5.1-next.1972

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/README.md CHANGED
@@ -699,13 +699,14 @@ createDotCMSClient(config: DotCMSClientConfig): DotCMSClient
699
699
 
700
700
  #### Parameters
701
701
 
702
- | Option | Type | Required | Description |
703
- | ---------------- | ----------------- | -------- | ------------------------------------------------------------- |
704
- | `dotcmsUrl` | string | ✅ | Your dotCMS instance URL |
705
- | `authToken` | string | ✅ | Authentication token |
706
- | `siteId` | string | ❌ | Site identifier (falls back to default site if not specified) |
707
- | `requestOptions` | DotRequestOptions | ❌ | Additional request options |
708
- | `httpClient` | DotHttpClient | ❌ | Custom HTTP client implementation |
702
+ | Option | Type | Required | Description |
703
+ | ---------------- | -------------------------- | -------- | ------------------------------------------------------------- |
704
+ | `dotcmsUrl` | string | ✅ | Your dotCMS instance URL |
705
+ | `authToken` | string | ✅ | Authentication token |
706
+ | `siteId` | string | ❌ | Site identifier (falls back to default site if not specified) |
707
+ | `requestOptions` | DotRequestOptions | ❌ | Additional request options |
708
+ | `httpClient` | DotHttpClient | ❌ | Custom HTTP client implementation |
709
+ | `logLevel` | `'default'` \| `'verbose'` | ❌ | Controls log verbosity. `'verbose'` adds status, code, and variables to error logs. Defaults to `'default'` |
709
710
 
710
711
  #### Example
711
712
  ```typescript
@@ -713,10 +714,14 @@ const client = createDotCMSClient({
713
714
  dotcmsUrl: 'https://your-dotcms-instance.com',
714
715
  authToken: 'your-auth-token',
715
716
  siteId: 'your-site-id',
716
- httpClient: customHttpClient // Optional: provide custom HTTP client
717
+ httpClient: customHttpClient, // Optional: provide custom HTTP client
718
+ logLevel: 'verbose' // Optional: enable detailed error logs
717
719
  });
718
720
  ```
719
721
 
722
+ > [!TIP]
723
+ > Enable `logLevel: 'verbose'` during development to see HTTP status codes, error codes, and request variables in error logs. In verbose mode, error logs also include a hint to access the full GraphQL query via `error.graphql.query`. Keep it at `'default'` (or omit it) in production to avoid noisy logs.
724
+
720
725
  ### HTTP Client Configuration
721
726
 
722
727
  The SDK now supports custom HTTP client implementations for advanced use cases. By default, it uses the built-in `FetchHttpClient` based on the native Fetch API.
package/index.cjs.js CHANGED
@@ -2107,8 +2107,8 @@ const DEFAULT_PAGE_CONTENTLETS_CONTENT = `
2107
2107
  * @param {string} additionalQueries - Additional GraphQL queries to include in the main query
2108
2108
  * @returns {string} Complete GraphQL query string for page content
2109
2109
  */
2110
- const buildPageQuery = ({ page, fragments, additionalQueries }) => {
2111
- if (!page) {
2110
+ const buildPageQuery = ({ page, fragments, additionalQueries, verbose = false }) => {
2111
+ if (!page && verbose) {
2112
2112
  consola.consola.warn("[DotCMS Client]: No page query was found, so we're loading all content using _map. This might slow things down. For better performance, we recommend adding a specific query in the page attribute.");
2113
2113
  }
2114
2114
  return `
@@ -2373,6 +2373,11 @@ async function fetchGraphQL({ baseURL, body, headers, httpClient }) {
2373
2373
  });
2374
2374
  }
2375
2375
 
2376
+ function logVerboseError(url, message, details) {
2377
+ const statusLine = details.status !== undefined ? `\n status: ${details.status} | code: ${details.code}` : '';
2378
+ const variables = JSON.stringify(details.variables, null, 2).replace(/\n/g, '\n ');
2379
+ consola.consola.error(`[DotCMS GraphQL Error] ${url}: ${message}${statusLine}\n\n variables:\n ${variables}\n\n (full query available at error.graphql.query)`);
2380
+ }
2376
2381
  /**
2377
2382
  * Client for interacting with the DotCMS Page API.
2378
2383
  * Provides methods to retrieve and manipulate pages.
@@ -2458,15 +2463,18 @@ class PageClient extends BaseApiClient {
2458
2463
  async get(url, options) {
2459
2464
  const { languageId = '1', mode = 'LIVE', siteId = this.siteId, fireRules = false, personaId, publishDate, variantName, graphql = {} } = options || {};
2460
2465
  const { page, content = {}, variables, fragments } = graphql;
2466
+ const verbose = this.config.logLevel === 'verbose';
2461
2467
  const contentQuery = buildQuery(content);
2462
2468
  const completeQuery = buildPageQuery({
2463
2469
  page,
2464
2470
  fragments,
2465
- additionalQueries: contentQuery
2471
+ additionalQueries: contentQuery,
2472
+ verbose
2466
2473
  });
2474
+ const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
2467
2475
  const requestVariables = {
2468
2476
  // The url is expected to have a leading slash to comply on VanityURL Matching, some frameworks like Angular will not add the leading slash
2469
- url: url.startsWith('/') ? url : `/${url}`,
2477
+ url: normalizedUrl,
2470
2478
  mode,
2471
2479
  languageId,
2472
2480
  personaId,
@@ -2485,33 +2493,77 @@ class PageClient extends BaseApiClient {
2485
2493
  headers: requestHeaders,
2486
2494
  httpClient: this.httpClient
2487
2495
  });
2488
- // The GQL endpoint can return errors and data, we need to handle both
2489
- if (response.errors) {
2490
- response.errors.forEach((error) => {
2491
- consola.consola.error('[DotCMS GraphQL Error]: ', error.message);
2496
+ // 1. Log unstructured GraphQL errors (structured ones are logged with enriched messages below)
2497
+ if (response.errors?.length) {
2498
+ response.errors
2499
+ .filter((error) => !error.extensions?.code)
2500
+ .forEach((error) => {
2501
+ if (verbose) {
2502
+ logVerboseError(normalizedUrl, error.message, {
2503
+ variables: requestVariables
2504
+ });
2505
+ }
2506
+ else {
2507
+ consola.consola.error(`[DotCMS GraphQL Error] ${normalizedUrl}: `, error.message);
2508
+ }
2492
2509
  });
2493
- const pageError = response.errors.find((error) => error.message.includes('DotPage'));
2494
- if (pageError) {
2495
- // Throw HTTP error - will be caught and wrapped in DotErrorPage below
2496
- throw new types.DotHttpError({
2497
- status: 400,
2498
- statusText: 'Bad Request',
2499
- message: `GraphQL query failed for URL '${url}': ${pageError.message}`,
2500
- data: response.errors
2510
+ }
2511
+ // 2. BAD QUERY — data is null/undefined means the entire query failed
2512
+ // (syntax error, unknown type, validation error)
2513
+ // Must check BEFORE accessing response.data.page
2514
+ if (!response.data) {
2515
+ const firstError = response.errors?.[0];
2516
+ throw new types.DotErrorPage(firstError?.message ?? 'GraphQL query failed', 400, 'BAD_REQUEST', new types.DotHttpError({
2517
+ status: 400,
2518
+ statusText: 'Bad Request',
2519
+ message: firstError?.message ?? 'GraphQL query failed',
2520
+ data: response.errors
2521
+ }), { query: completeQuery, variables: requestVariables });
2522
+ }
2523
+ // 3. STRUCTURED ERRORS — check extensions.code for NOT_FOUND, PERMISSION_DENIED, etc.
2524
+ // Only fatal when the page itself failed (data.page is null/undefined).
2525
+ // If data.page exists, partial errors (e.g. secondary content) surface via errors[].
2526
+ if (response.errors?.length && !response.data.page) {
2527
+ const structuredError = response.errors.find((error) => error.extensions?.code);
2528
+ if (structuredError) {
2529
+ const code = structuredError.extensions.code;
2530
+ const status = structuredError.extensions.status ??
2531
+ (code === 'NOT_FOUND' ? 404 : code === 'PERMISSION_DENIED' ? 403 : 400);
2532
+ const message = code === 'NOT_FOUND'
2533
+ ? `Page '${normalizedUrl}' was not found`
2534
+ : code === 'PERMISSION_DENIED'
2535
+ ? `Permission denied: you do not have access to page '${normalizedUrl}'. Verify the page permissions in dotCMS and that the auth token has sufficient access.`
2536
+ : `Page '${normalizedUrl}' could not be loaded (${code})`;
2537
+ if (verbose) {
2538
+ logVerboseError(normalizedUrl, message, {
2539
+ status,
2540
+ code,
2541
+ variables: requestVariables
2542
+ });
2543
+ }
2544
+ else {
2545
+ consola.consola.error(`[DotCMS GraphQL Error] ${normalizedUrl}: `, message);
2546
+ }
2547
+ throw new types.DotErrorPage(message, status, code, undefined, {
2548
+ query: completeQuery,
2549
+ variables: requestVariables
2501
2550
  });
2502
2551
  }
2503
2552
  }
2504
- const pageResponse = internal.graphqlToPageEntity(response.data.page);
2553
+ // 4. Transform and check page — null page with no structured error = 404
2554
+ const pageResponse = response.data.page
2555
+ ? internal.graphqlToPageEntity(response.data.page)
2556
+ : null;
2505
2557
  if (!pageResponse) {
2506
- // Throw HTTP error - will be caught and wrapped in DotErrorPage below
2507
- throw new types.DotHttpError({
2558
+ throw new types.DotErrorPage(`Page '${normalizedUrl}' was not found`, 404, 'NOT_FOUND', new types.DotHttpError({
2508
2559
  status: 404,
2509
2560
  statusText: 'Not Found',
2510
- message: `Page ${url} not found. Check the page URL and permissions.`,
2561
+ message: `Page '${normalizedUrl}' was not found`,
2511
2562
  data: response.errors
2512
- });
2563
+ }), { query: completeQuery, variables: requestVariables });
2513
2564
  }
2514
2565
  const styleEditorSchemas = await fetchStyleEditorSchemas(pageResponse.page.identifier, this.config, this.requestOptions, this.httpClient);
2566
+ // 5. Build response — include any non-fatal errors for consumers to inspect
2515
2567
  const contentResponse = mapContentResponse(response.data, Object.keys(content));
2516
2568
  return {
2517
2569
  pageAsset: pageResponse,
@@ -2520,22 +2572,18 @@ class PageClient extends BaseApiClient {
2520
2572
  query: completeQuery,
2521
2573
  variables: requestVariables
2522
2574
  },
2575
+ errors: response.errors?.length ? response.errors : undefined,
2523
2576
  ...(styleEditorSchemas.length > 0 && { styleEditorSchemas })
2524
2577
  };
2525
2578
  }
2526
2579
  catch (error) {
2527
- // Handle DotHttpError instances
2580
+ if (error instanceof types.DotErrorPage) {
2581
+ throw error;
2582
+ }
2528
2583
  if (error instanceof types.DotHttpError) {
2529
- throw new types.DotErrorPage(`Page request failed for URL '${url}': ${error.message}`, error, {
2530
- query: completeQuery,
2531
- variables: requestVariables
2532
- });
2584
+ throw new types.DotErrorPage(`Page request failed for URL '${normalizedUrl}': ${error.message}`, error.status, 'UNKNOWN', error, { query: completeQuery, variables: requestVariables });
2533
2585
  }
2534
- // Handle other errors (GraphQL errors, validation errors, etc.)
2535
- throw new types.DotErrorPage(`Page request failed for URL '${url}': ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, {
2536
- query: completeQuery,
2537
- variables: requestVariables
2538
- });
2586
+ throw new types.DotErrorPage(`Page request failed for URL '${normalizedUrl}': ${error instanceof Error ? error.message : 'Unknown error'}`, 500, 'UNKNOWN', undefined, { query: completeQuery, variables: requestVariables });
2539
2587
  }
2540
2588
  }
2541
2589
  }
package/index.esm.js CHANGED
@@ -2105,8 +2105,8 @@ const DEFAULT_PAGE_CONTENTLETS_CONTENT = `
2105
2105
  * @param {string} additionalQueries - Additional GraphQL queries to include in the main query
2106
2106
  * @returns {string} Complete GraphQL query string for page content
2107
2107
  */
2108
- const buildPageQuery = ({ page, fragments, additionalQueries }) => {
2109
- if (!page) {
2108
+ const buildPageQuery = ({ page, fragments, additionalQueries, verbose = false }) => {
2109
+ if (!page && verbose) {
2110
2110
  consola.warn("[DotCMS Client]: No page query was found, so we're loading all content using _map. This might slow things down. For better performance, we recommend adding a specific query in the page attribute.");
2111
2111
  }
2112
2112
  return `
@@ -2371,6 +2371,11 @@ async function fetchGraphQL({ baseURL, body, headers, httpClient }) {
2371
2371
  });
2372
2372
  }
2373
2373
 
2374
+ function logVerboseError(url, message, details) {
2375
+ const statusLine = details.status !== undefined ? `\n status: ${details.status} | code: ${details.code}` : '';
2376
+ const variables = JSON.stringify(details.variables, null, 2).replace(/\n/g, '\n ');
2377
+ consola.error(`[DotCMS GraphQL Error] ${url}: ${message}${statusLine}\n\n variables:\n ${variables}\n\n (full query available at error.graphql.query)`);
2378
+ }
2374
2379
  /**
2375
2380
  * Client for interacting with the DotCMS Page API.
2376
2381
  * Provides methods to retrieve and manipulate pages.
@@ -2456,15 +2461,18 @@ class PageClient extends BaseApiClient {
2456
2461
  async get(url, options) {
2457
2462
  const { languageId = '1', mode = 'LIVE', siteId = this.siteId, fireRules = false, personaId, publishDate, variantName, graphql = {} } = options || {};
2458
2463
  const { page, content = {}, variables, fragments } = graphql;
2464
+ const verbose = this.config.logLevel === 'verbose';
2459
2465
  const contentQuery = buildQuery(content);
2460
2466
  const completeQuery = buildPageQuery({
2461
2467
  page,
2462
2468
  fragments,
2463
- additionalQueries: contentQuery
2469
+ additionalQueries: contentQuery,
2470
+ verbose
2464
2471
  });
2472
+ const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
2465
2473
  const requestVariables = {
2466
2474
  // The url is expected to have a leading slash to comply on VanityURL Matching, some frameworks like Angular will not add the leading slash
2467
- url: url.startsWith('/') ? url : `/${url}`,
2475
+ url: normalizedUrl,
2468
2476
  mode,
2469
2477
  languageId,
2470
2478
  personaId,
@@ -2483,33 +2491,77 @@ class PageClient extends BaseApiClient {
2483
2491
  headers: requestHeaders,
2484
2492
  httpClient: this.httpClient
2485
2493
  });
2486
- // The GQL endpoint can return errors and data, we need to handle both
2487
- if (response.errors) {
2488
- response.errors.forEach((error) => {
2489
- consola.error('[DotCMS GraphQL Error]: ', error.message);
2494
+ // 1. Log unstructured GraphQL errors (structured ones are logged with enriched messages below)
2495
+ if (response.errors?.length) {
2496
+ response.errors
2497
+ .filter((error) => !error.extensions?.code)
2498
+ .forEach((error) => {
2499
+ if (verbose) {
2500
+ logVerboseError(normalizedUrl, error.message, {
2501
+ variables: requestVariables
2502
+ });
2503
+ }
2504
+ else {
2505
+ consola.error(`[DotCMS GraphQL Error] ${normalizedUrl}: `, error.message);
2506
+ }
2490
2507
  });
2491
- const pageError = response.errors.find((error) => error.message.includes('DotPage'));
2492
- if (pageError) {
2493
- // Throw HTTP error - will be caught and wrapped in DotErrorPage below
2494
- throw new DotHttpError({
2495
- status: 400,
2496
- statusText: 'Bad Request',
2497
- message: `GraphQL query failed for URL '${url}': ${pageError.message}`,
2498
- data: response.errors
2508
+ }
2509
+ // 2. BAD QUERY — data is null/undefined means the entire query failed
2510
+ // (syntax error, unknown type, validation error)
2511
+ // Must check BEFORE accessing response.data.page
2512
+ if (!response.data) {
2513
+ const firstError = response.errors?.[0];
2514
+ throw new DotErrorPage(firstError?.message ?? 'GraphQL query failed', 400, 'BAD_REQUEST', new DotHttpError({
2515
+ status: 400,
2516
+ statusText: 'Bad Request',
2517
+ message: firstError?.message ?? 'GraphQL query failed',
2518
+ data: response.errors
2519
+ }), { query: completeQuery, variables: requestVariables });
2520
+ }
2521
+ // 3. STRUCTURED ERRORS — check extensions.code for NOT_FOUND, PERMISSION_DENIED, etc.
2522
+ // Only fatal when the page itself failed (data.page is null/undefined).
2523
+ // If data.page exists, partial errors (e.g. secondary content) surface via errors[].
2524
+ if (response.errors?.length && !response.data.page) {
2525
+ const structuredError = response.errors.find((error) => error.extensions?.code);
2526
+ if (structuredError) {
2527
+ const code = structuredError.extensions.code;
2528
+ const status = structuredError.extensions.status ??
2529
+ (code === 'NOT_FOUND' ? 404 : code === 'PERMISSION_DENIED' ? 403 : 400);
2530
+ const message = code === 'NOT_FOUND'
2531
+ ? `Page '${normalizedUrl}' was not found`
2532
+ : code === 'PERMISSION_DENIED'
2533
+ ? `Permission denied: you do not have access to page '${normalizedUrl}'. Verify the page permissions in dotCMS and that the auth token has sufficient access.`
2534
+ : `Page '${normalizedUrl}' could not be loaded (${code})`;
2535
+ if (verbose) {
2536
+ logVerboseError(normalizedUrl, message, {
2537
+ status,
2538
+ code,
2539
+ variables: requestVariables
2540
+ });
2541
+ }
2542
+ else {
2543
+ consola.error(`[DotCMS GraphQL Error] ${normalizedUrl}: `, message);
2544
+ }
2545
+ throw new DotErrorPage(message, status, code, undefined, {
2546
+ query: completeQuery,
2547
+ variables: requestVariables
2499
2548
  });
2500
2549
  }
2501
2550
  }
2502
- const pageResponse = graphqlToPageEntity(response.data.page);
2551
+ // 4. Transform and check page — null page with no structured error = 404
2552
+ const pageResponse = response.data.page
2553
+ ? graphqlToPageEntity(response.data.page)
2554
+ : null;
2503
2555
  if (!pageResponse) {
2504
- // Throw HTTP error - will be caught and wrapped in DotErrorPage below
2505
- throw new DotHttpError({
2556
+ throw new DotErrorPage(`Page '${normalizedUrl}' was not found`, 404, 'NOT_FOUND', new DotHttpError({
2506
2557
  status: 404,
2507
2558
  statusText: 'Not Found',
2508
- message: `Page ${url} not found. Check the page URL and permissions.`,
2559
+ message: `Page '${normalizedUrl}' was not found`,
2509
2560
  data: response.errors
2510
- });
2561
+ }), { query: completeQuery, variables: requestVariables });
2511
2562
  }
2512
2563
  const styleEditorSchemas = await fetchStyleEditorSchemas(pageResponse.page.identifier, this.config, this.requestOptions, this.httpClient);
2564
+ // 5. Build response — include any non-fatal errors for consumers to inspect
2513
2565
  const contentResponse = mapContentResponse(response.data, Object.keys(content));
2514
2566
  return {
2515
2567
  pageAsset: pageResponse,
@@ -2518,22 +2570,18 @@ class PageClient extends BaseApiClient {
2518
2570
  query: completeQuery,
2519
2571
  variables: requestVariables
2520
2572
  },
2573
+ errors: response.errors?.length ? response.errors : undefined,
2521
2574
  ...(styleEditorSchemas.length > 0 && { styleEditorSchemas })
2522
2575
  };
2523
2576
  }
2524
2577
  catch (error) {
2525
- // Handle DotHttpError instances
2578
+ if (error instanceof DotErrorPage) {
2579
+ throw error;
2580
+ }
2526
2581
  if (error instanceof DotHttpError) {
2527
- throw new DotErrorPage(`Page request failed for URL '${url}': ${error.message}`, error, {
2528
- query: completeQuery,
2529
- variables: requestVariables
2530
- });
2582
+ throw new DotErrorPage(`Page request failed for URL '${normalizedUrl}': ${error.message}`, error.status, 'UNKNOWN', error, { query: completeQuery, variables: requestVariables });
2531
2583
  }
2532
- // Handle other errors (GraphQL errors, validation errors, etc.)
2533
- throw new DotErrorPage(`Page request failed for URL '${url}': ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, {
2534
- query: completeQuery,
2535
- variables: requestVariables
2536
- });
2584
+ throw new DotErrorPage(`Page request failed for URL '${normalizedUrl}': ${error instanceof Error ? error.message : 'Unknown error'}`, 500, 'UNKNOWN', undefined, { query: completeQuery, variables: requestVariables });
2537
2585
  }
2538
2586
  }
2539
2587
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotcms/client",
3
- "version": "1.5.1-next.1965",
3
+ "version": "1.5.1-next.1972",
4
4
  "description": "Official JavaScript library for interacting with DotCMS REST APIs.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,10 +7,11 @@ import { StyleEditorFormSchema } from '@dotcms/types/internal';
7
7
  * @param {string} additionalQueries - Additional GraphQL queries to include in the main query
8
8
  * @returns {string} Complete GraphQL query string for page content
9
9
  */
10
- export declare const buildPageQuery: ({ page, fragments, additionalQueries }: {
10
+ export declare const buildPageQuery: ({ page, fragments, additionalQueries, verbose }: {
11
11
  page?: string;
12
12
  fragments?: string[];
13
13
  additionalQueries?: string;
14
+ verbose?: boolean;
14
15
  }) => string;
15
16
  /**
16
17
  * Converts a record of query strings into a single GraphQL query string.