@dotcms/client 1.5.1-next.1964 → 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 +13 -8
- package/index.cjs.js +125 -32
- package/index.esm.js +125 -32
- package/package.json +1 -1
- package/src/lib/client/page/page-api.d.ts +1 -1
- package/src/lib/client/page/utils.d.ts +11 -2
package/README.md
CHANGED
|
@@ -699,13 +699,14 @@ createDotCMSClient(config: DotCMSClientConfig): DotCMSClient
|
|
|
699
699
|
|
|
700
700
|
#### Parameters
|
|
701
701
|
|
|
702
|
-
| Option | Type
|
|
703
|
-
| ---------------- |
|
|
704
|
-
| `dotcmsUrl` | string
|
|
705
|
-
| `authToken` | string
|
|
706
|
-
| `siteId` | string
|
|
707
|
-
| `requestOptions` | DotRequestOptions
|
|
708
|
-
| `httpClient` | DotHttpClient
|
|
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 `
|
|
@@ -2309,6 +2309,49 @@ function mapContentResponse(responseData, keys) {
|
|
|
2309
2309
|
return accumulator;
|
|
2310
2310
|
}, {});
|
|
2311
2311
|
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Loads style editor schemas from GET /api/v1/page/{pageId}/contenttype-schema.
|
|
2314
|
+
* Requires READ on the page; failures are silently ignored so callers still work without auth.
|
|
2315
|
+
*
|
|
2316
|
+
* @internal
|
|
2317
|
+
*/
|
|
2318
|
+
async function fetchStyleEditorSchemas(pageId, config, requestOptions, httpClient) {
|
|
2319
|
+
if (typeof window === 'undefined') {
|
|
2320
|
+
return [];
|
|
2321
|
+
}
|
|
2322
|
+
if (!pageId) {
|
|
2323
|
+
consola.consola.warn('[DotCMS PageClient]: fetchStyleEditorSchemas called without a pageId — ' +
|
|
2324
|
+
'make sure "identifier" is included in your GraphQL page fragment.');
|
|
2325
|
+
return [];
|
|
2326
|
+
}
|
|
2327
|
+
try {
|
|
2328
|
+
const url = new URL(config.dotcmsUrl);
|
|
2329
|
+
url.pathname = `/api/v1/page/${encodeURIComponent(pageId)}/contenttype-schema`;
|
|
2330
|
+
const data = await httpClient.request(url.toString(), {
|
|
2331
|
+
...requestOptions,
|
|
2332
|
+
method: 'GET',
|
|
2333
|
+
headers: {
|
|
2334
|
+
Accept: 'application/json',
|
|
2335
|
+
...requestOptions.headers
|
|
2336
|
+
}
|
|
2337
|
+
});
|
|
2338
|
+
const { entity } = data ?? {};
|
|
2339
|
+
if (!Array.isArray(entity)) {
|
|
2340
|
+
return [];
|
|
2341
|
+
}
|
|
2342
|
+
return entity;
|
|
2343
|
+
}
|
|
2344
|
+
catch (error) {
|
|
2345
|
+
if (error instanceof types.DotHttpError && (error.status === 401 || error.status === 403)) {
|
|
2346
|
+
consola.consola.warn(`[DotCMS PageClient]: Style editor schemas request failed with ${error.status} — ` +
|
|
2347
|
+
'make sure your DotCMS client is configured with a valid authToken that has READ access to the page.');
|
|
2348
|
+
}
|
|
2349
|
+
else {
|
|
2350
|
+
consola.consola.debug('[DotCMS PageClient]: Skipping style editor schemas:', error);
|
|
2351
|
+
}
|
|
2352
|
+
return [];
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2312
2355
|
/**
|
|
2313
2356
|
* Executes a GraphQL query against the DotCMS API.
|
|
2314
2357
|
*
|
|
@@ -2330,6 +2373,11 @@ async function fetchGraphQL({ baseURL, body, headers, httpClient }) {
|
|
|
2330
2373
|
});
|
|
2331
2374
|
}
|
|
2332
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
|
+
}
|
|
2333
2381
|
/**
|
|
2334
2382
|
* Client for interacting with the DotCMS Page API.
|
|
2335
2383
|
* Provides methods to retrieve and manipulate pages.
|
|
@@ -2415,15 +2463,18 @@ class PageClient extends BaseApiClient {
|
|
|
2415
2463
|
async get(url, options) {
|
|
2416
2464
|
const { languageId = '1', mode = 'LIVE', siteId = this.siteId, fireRules = false, personaId, publishDate, variantName, graphql = {} } = options || {};
|
|
2417
2465
|
const { page, content = {}, variables, fragments } = graphql;
|
|
2466
|
+
const verbose = this.config.logLevel === 'verbose';
|
|
2418
2467
|
const contentQuery = buildQuery(content);
|
|
2419
2468
|
const completeQuery = buildPageQuery({
|
|
2420
2469
|
page,
|
|
2421
2470
|
fragments,
|
|
2422
|
-
additionalQueries: contentQuery
|
|
2471
|
+
additionalQueries: contentQuery,
|
|
2472
|
+
verbose
|
|
2423
2473
|
});
|
|
2474
|
+
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
|
|
2424
2475
|
const requestVariables = {
|
|
2425
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
|
|
2426
|
-
url:
|
|
2477
|
+
url: normalizedUrl,
|
|
2427
2478
|
mode,
|
|
2428
2479
|
languageId,
|
|
2429
2480
|
personaId,
|
|
@@ -2442,32 +2493,77 @@ class PageClient extends BaseApiClient {
|
|
|
2442
2493
|
headers: requestHeaders,
|
|
2443
2494
|
httpClient: this.httpClient
|
|
2444
2495
|
});
|
|
2445
|
-
//
|
|
2446
|
-
if (response.errors) {
|
|
2447
|
-
response.errors
|
|
2448
|
-
|
|
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
|
+
}
|
|
2449
2509
|
});
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
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
|
|
2458
2550
|
});
|
|
2459
2551
|
}
|
|
2460
2552
|
}
|
|
2461
|
-
|
|
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;
|
|
2462
2557
|
if (!pageResponse) {
|
|
2463
|
-
|
|
2464
|
-
throw new types.DotHttpError({
|
|
2558
|
+
throw new types.DotErrorPage(`Page '${normalizedUrl}' was not found`, 404, 'NOT_FOUND', new types.DotHttpError({
|
|
2465
2559
|
status: 404,
|
|
2466
2560
|
statusText: 'Not Found',
|
|
2467
|
-
message: `Page ${
|
|
2561
|
+
message: `Page '${normalizedUrl}' was not found`,
|
|
2468
2562
|
data: response.errors
|
|
2469
|
-
});
|
|
2563
|
+
}), { query: completeQuery, variables: requestVariables });
|
|
2470
2564
|
}
|
|
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
|
|
2471
2567
|
const contentResponse = mapContentResponse(response.data, Object.keys(content));
|
|
2472
2568
|
return {
|
|
2473
2569
|
pageAsset: pageResponse,
|
|
@@ -2475,22 +2571,19 @@ class PageClient extends BaseApiClient {
|
|
|
2475
2571
|
graphql: {
|
|
2476
2572
|
query: completeQuery,
|
|
2477
2573
|
variables: requestVariables
|
|
2478
|
-
}
|
|
2574
|
+
},
|
|
2575
|
+
errors: response.errors?.length ? response.errors : undefined,
|
|
2576
|
+
...(styleEditorSchemas.length > 0 && { styleEditorSchemas })
|
|
2479
2577
|
};
|
|
2480
2578
|
}
|
|
2481
2579
|
catch (error) {
|
|
2482
|
-
|
|
2580
|
+
if (error instanceof types.DotErrorPage) {
|
|
2581
|
+
throw error;
|
|
2582
|
+
}
|
|
2483
2583
|
if (error instanceof types.DotHttpError) {
|
|
2484
|
-
throw new types.DotErrorPage(`Page request failed for URL '${
|
|
2485
|
-
query: completeQuery,
|
|
2486
|
-
variables: requestVariables
|
|
2487
|
-
});
|
|
2584
|
+
throw new types.DotErrorPage(`Page request failed for URL '${normalizedUrl}': ${error.message}`, error.status, 'UNKNOWN', error, { query: completeQuery, variables: requestVariables });
|
|
2488
2585
|
}
|
|
2489
|
-
|
|
2490
|
-
throw new types.DotErrorPage(`Page request failed for URL '${url}': ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, {
|
|
2491
|
-
query: completeQuery,
|
|
2492
|
-
variables: requestVariables
|
|
2493
|
-
});
|
|
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 });
|
|
2494
2587
|
}
|
|
2495
2588
|
}
|
|
2496
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 `
|
|
@@ -2307,6 +2307,49 @@ function mapContentResponse(responseData, keys) {
|
|
|
2307
2307
|
return accumulator;
|
|
2308
2308
|
}, {});
|
|
2309
2309
|
}
|
|
2310
|
+
/**
|
|
2311
|
+
* Loads style editor schemas from GET /api/v1/page/{pageId}/contenttype-schema.
|
|
2312
|
+
* Requires READ on the page; failures are silently ignored so callers still work without auth.
|
|
2313
|
+
*
|
|
2314
|
+
* @internal
|
|
2315
|
+
*/
|
|
2316
|
+
async function fetchStyleEditorSchemas(pageId, config, requestOptions, httpClient) {
|
|
2317
|
+
if (typeof window === 'undefined') {
|
|
2318
|
+
return [];
|
|
2319
|
+
}
|
|
2320
|
+
if (!pageId) {
|
|
2321
|
+
consola.warn('[DotCMS PageClient]: fetchStyleEditorSchemas called without a pageId — ' +
|
|
2322
|
+
'make sure "identifier" is included in your GraphQL page fragment.');
|
|
2323
|
+
return [];
|
|
2324
|
+
}
|
|
2325
|
+
try {
|
|
2326
|
+
const url = new URL(config.dotcmsUrl);
|
|
2327
|
+
url.pathname = `/api/v1/page/${encodeURIComponent(pageId)}/contenttype-schema`;
|
|
2328
|
+
const data = await httpClient.request(url.toString(), {
|
|
2329
|
+
...requestOptions,
|
|
2330
|
+
method: 'GET',
|
|
2331
|
+
headers: {
|
|
2332
|
+
Accept: 'application/json',
|
|
2333
|
+
...requestOptions.headers
|
|
2334
|
+
}
|
|
2335
|
+
});
|
|
2336
|
+
const { entity } = data ?? {};
|
|
2337
|
+
if (!Array.isArray(entity)) {
|
|
2338
|
+
return [];
|
|
2339
|
+
}
|
|
2340
|
+
return entity;
|
|
2341
|
+
}
|
|
2342
|
+
catch (error) {
|
|
2343
|
+
if (error instanceof DotHttpError && (error.status === 401 || error.status === 403)) {
|
|
2344
|
+
consola.warn(`[DotCMS PageClient]: Style editor schemas request failed with ${error.status} — ` +
|
|
2345
|
+
'make sure your DotCMS client is configured with a valid authToken that has READ access to the page.');
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
consola.debug('[DotCMS PageClient]: Skipping style editor schemas:', error);
|
|
2349
|
+
}
|
|
2350
|
+
return [];
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2310
2353
|
/**
|
|
2311
2354
|
* Executes a GraphQL query against the DotCMS API.
|
|
2312
2355
|
*
|
|
@@ -2328,6 +2371,11 @@ async function fetchGraphQL({ baseURL, body, headers, httpClient }) {
|
|
|
2328
2371
|
});
|
|
2329
2372
|
}
|
|
2330
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
|
+
}
|
|
2331
2379
|
/**
|
|
2332
2380
|
* Client for interacting with the DotCMS Page API.
|
|
2333
2381
|
* Provides methods to retrieve and manipulate pages.
|
|
@@ -2413,15 +2461,18 @@ class PageClient extends BaseApiClient {
|
|
|
2413
2461
|
async get(url, options) {
|
|
2414
2462
|
const { languageId = '1', mode = 'LIVE', siteId = this.siteId, fireRules = false, personaId, publishDate, variantName, graphql = {} } = options || {};
|
|
2415
2463
|
const { page, content = {}, variables, fragments } = graphql;
|
|
2464
|
+
const verbose = this.config.logLevel === 'verbose';
|
|
2416
2465
|
const contentQuery = buildQuery(content);
|
|
2417
2466
|
const completeQuery = buildPageQuery({
|
|
2418
2467
|
page,
|
|
2419
2468
|
fragments,
|
|
2420
|
-
additionalQueries: contentQuery
|
|
2469
|
+
additionalQueries: contentQuery,
|
|
2470
|
+
verbose
|
|
2421
2471
|
});
|
|
2472
|
+
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
|
|
2422
2473
|
const requestVariables = {
|
|
2423
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
|
|
2424
|
-
url:
|
|
2475
|
+
url: normalizedUrl,
|
|
2425
2476
|
mode,
|
|
2426
2477
|
languageId,
|
|
2427
2478
|
personaId,
|
|
@@ -2440,32 +2491,77 @@ class PageClient extends BaseApiClient {
|
|
|
2440
2491
|
headers: requestHeaders,
|
|
2441
2492
|
httpClient: this.httpClient
|
|
2442
2493
|
});
|
|
2443
|
-
//
|
|
2444
|
-
if (response.errors) {
|
|
2445
|
-
response.errors
|
|
2446
|
-
|
|
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
|
+
}
|
|
2447
2507
|
});
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
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
|
|
2456
2548
|
});
|
|
2457
2549
|
}
|
|
2458
2550
|
}
|
|
2459
|
-
|
|
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;
|
|
2460
2555
|
if (!pageResponse) {
|
|
2461
|
-
|
|
2462
|
-
throw new DotHttpError({
|
|
2556
|
+
throw new DotErrorPage(`Page '${normalizedUrl}' was not found`, 404, 'NOT_FOUND', new DotHttpError({
|
|
2463
2557
|
status: 404,
|
|
2464
2558
|
statusText: 'Not Found',
|
|
2465
|
-
message: `Page ${
|
|
2559
|
+
message: `Page '${normalizedUrl}' was not found`,
|
|
2466
2560
|
data: response.errors
|
|
2467
|
-
});
|
|
2561
|
+
}), { query: completeQuery, variables: requestVariables });
|
|
2468
2562
|
}
|
|
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
|
|
2469
2565
|
const contentResponse = mapContentResponse(response.data, Object.keys(content));
|
|
2470
2566
|
return {
|
|
2471
2567
|
pageAsset: pageResponse,
|
|
@@ -2473,22 +2569,19 @@ class PageClient extends BaseApiClient {
|
|
|
2473
2569
|
graphql: {
|
|
2474
2570
|
query: completeQuery,
|
|
2475
2571
|
variables: requestVariables
|
|
2476
|
-
}
|
|
2572
|
+
},
|
|
2573
|
+
errors: response.errors?.length ? response.errors : undefined,
|
|
2574
|
+
...(styleEditorSchemas.length > 0 && { styleEditorSchemas })
|
|
2477
2575
|
};
|
|
2478
2576
|
}
|
|
2479
2577
|
catch (error) {
|
|
2480
|
-
|
|
2578
|
+
if (error instanceof DotErrorPage) {
|
|
2579
|
+
throw error;
|
|
2580
|
+
}
|
|
2481
2581
|
if (error instanceof DotHttpError) {
|
|
2482
|
-
throw new DotErrorPage(`Page request failed for URL '${
|
|
2483
|
-
query: completeQuery,
|
|
2484
|
-
variables: requestVariables
|
|
2485
|
-
});
|
|
2582
|
+
throw new DotErrorPage(`Page request failed for URL '${normalizedUrl}': ${error.message}`, error.status, 'UNKNOWN', error, { query: completeQuery, variables: requestVariables });
|
|
2486
2583
|
}
|
|
2487
|
-
|
|
2488
|
-
throw new DotErrorPage(`Page request failed for URL '${url}': ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, {
|
|
2489
|
-
query: completeQuery,
|
|
2490
|
-
variables: requestVariables
|
|
2491
|
-
});
|
|
2584
|
+
throw new DotErrorPage(`Page request failed for URL '${normalizedUrl}': ${error instanceof Error ? error.message : 'Unknown error'}`, 500, 'UNKNOWN', undefined, { query: completeQuery, variables: requestVariables });
|
|
2492
2585
|
}
|
|
2493
2586
|
}
|
|
2494
2587
|
}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DotCMSClientConfig,
|
|
1
|
+
import { DotCMSClientConfig, DotCMSComposedPageResponse, DotCMSExtendedPageResponse, DotCMSPageRequestParams, DotCMSPageResponse, DotHttpClient, DotRequestOptions } from '@dotcms/types';
|
|
2
2
|
import { BaseApiClient } from '../base/api/base-api';
|
|
3
3
|
/**
|
|
4
4
|
* Client for interacting with the DotCMS Page API.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DotGraphQLApiResponse, DotHttpClient } from '@dotcms/types';
|
|
1
|
+
import { DotCMSClientConfig, DotGraphQLApiResponse, DotHttpClient, DotRequestOptions } from '@dotcms/types';
|
|
2
|
+
import { StyleEditorFormSchema } from '@dotcms/types/internal';
|
|
2
3
|
/**
|
|
3
4
|
* Builds a GraphQL query for retrieving page content from DotCMS.
|
|
4
5
|
*
|
|
@@ -6,10 +7,11 @@ import { DotGraphQLApiResponse, DotHttpClient } from '@dotcms/types';
|
|
|
6
7
|
* @param {string} additionalQueries - Additional GraphQL queries to include in the main query
|
|
7
8
|
* @returns {string} Complete GraphQL query string for page content
|
|
8
9
|
*/
|
|
9
|
-
export declare const buildPageQuery: ({ page, fragments, additionalQueries }: {
|
|
10
|
+
export declare const buildPageQuery: ({ page, fragments, additionalQueries, verbose }: {
|
|
10
11
|
page?: string;
|
|
11
12
|
fragments?: string[];
|
|
12
13
|
additionalQueries?: string;
|
|
14
|
+
verbose?: boolean;
|
|
13
15
|
}) => string;
|
|
14
16
|
/**
|
|
15
17
|
* Converts a record of query strings into a single GraphQL query string.
|
|
@@ -26,6 +28,13 @@ export declare function buildQuery(queryData: Record<string, string>): string;
|
|
|
26
28
|
* @returns {Record<string, unknown> | undefined} New object containing only the specified keys
|
|
27
29
|
*/
|
|
28
30
|
export declare function mapContentResponse(responseData: Record<string, unknown> | undefined, keys: string[]): Record<string, unknown> | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Loads style editor schemas from GET /api/v1/page/{pageId}/contenttype-schema.
|
|
33
|
+
* Requires READ on the page; failures are silently ignored so callers still work without auth.
|
|
34
|
+
*
|
|
35
|
+
* @internal
|
|
36
|
+
*/
|
|
37
|
+
export declare function fetchStyleEditorSchemas(pageId: string | undefined, config: DotCMSClientConfig, requestOptions: DotRequestOptions, httpClient: DotHttpClient): Promise<StyleEditorFormSchema[]>;
|
|
29
38
|
/**
|
|
30
39
|
* Executes a GraphQL query against the DotCMS API.
|
|
31
40
|
*
|