@dotcms/client 1.0.6 → 1.1.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.
package/index.esm.js CHANGED
@@ -1,6 +1,115 @@
1
1
  import { consola } from 'consola';
2
+ import { BaseHttpClient, DotHttpError, DotErrorContent, DotErrorNavigation, DotErrorPage } from '@dotcms/types';
2
3
  import { graphqlToPageEntity } from './internal.esm.js';
3
4
 
5
+ /**
6
+ * HTTP client implementation using the Fetch API.
7
+ *
8
+ * Extends BaseHttpClient to provide a standard interface for making HTTP requests.
9
+ * This implementation uses the native Fetch API and handles:
10
+ * - JSON and non-JSON response parsing
11
+ * - HTTP error response parsing and conversion to DotHttpError
12
+ * - Network error handling and wrapping
13
+ * - Content-Type detection for proper response handling
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const client = new FetchHttpClient();
18
+ *
19
+ * // JSON request
20
+ * const data = await client.request<MyType>('/api/data', {
21
+ * method: 'GET',
22
+ * headers: { 'Authorization': 'Bearer token' }
23
+ * });
24
+ *
25
+ * // Non-JSON request (e.g., file download)
26
+ * const response = await client.request<Response>('/api/file.pdf', {
27
+ * method: 'GET'
28
+ * });
29
+ * ```
30
+ */
31
+ class FetchHttpClient extends BaseHttpClient {
32
+ /**
33
+ * Sends an HTTP request using the Fetch API.
34
+ *
35
+ * Implements the abstract request method from BaseHttpClient using the native Fetch API.
36
+ * Automatically handles response parsing based on Content-Type headers and converts
37
+ * HTTP errors to standardized DotHttpError instances.
38
+ *
39
+ * @template T - The expected response type. For JSON responses, T should be the parsed object type.
40
+ * For non-JSON responses, T should be Response or the expected response type.
41
+ * @param url - The URL to send the request to.
42
+ * @param options - Optional fetch options including method, headers, body, etc.
43
+ * @returns Promise that resolves with the parsed response data or the Response object for non-JSON.
44
+ * @throws {DotHttpError} - Throws DotHttpError for HTTP errors (4xx/5xx status codes).
45
+ * @throws {DotHttpError} - Throws DotHttpError for network errors (connection issues, timeouts).
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // JSON API request
50
+ * const user = await client.request<User>('/api/users/123', {
51
+ * method: 'GET',
52
+ * headers: { 'Accept': 'application/json' }
53
+ * });
54
+ *
55
+ * // POST request with JSON body
56
+ * const result = await client.request<CreateResult>('/api/users', {
57
+ * method: 'POST',
58
+ * headers: { 'Content-Type': 'application/json' },
59
+ * body: JSON.stringify({ name: 'John', email: 'john@example.com' })
60
+ * });
61
+ *
62
+ * // File download (non-JSON response)
63
+ * const response = await client.request<Response>('/api/files/document.pdf', {
64
+ * method: 'GET'
65
+ * });
66
+ * ```
67
+ */
68
+ async request(url, options) {
69
+ try {
70
+ // Use native fetch API - no additional configuration needed
71
+ const response = await fetch(url, options);
72
+ if (!response.ok) {
73
+ // Parse response body for error context
74
+ let errorBody;
75
+ try {
76
+ const contentType = response.headers.get('content-type');
77
+ if (contentType?.includes('application/json')) {
78
+ errorBody = await response.json();
79
+ }
80
+ else {
81
+ errorBody = await response.text();
82
+ }
83
+ }
84
+ catch {
85
+ errorBody = response.statusText;
86
+ }
87
+ // Convert headers to plain object
88
+ const headers = {};
89
+ response.headers.forEach((value, key) => {
90
+ headers[key] = value;
91
+ });
92
+ throw this.createHttpError(response.status, response.statusText, headers, errorBody);
93
+ }
94
+ // Handle different response types
95
+ const contentType = response.headers.get('content-type');
96
+ if (contentType?.includes('application/json')) {
97
+ return response.json();
98
+ }
99
+ // For non-JSON responses, return the response object
100
+ // Sub-clients can handle specific response types as needed
101
+ return response;
102
+ }
103
+ catch (error) {
104
+ // Handle network errors (fetch throws TypeError for network issues)
105
+ if (error instanceof TypeError) {
106
+ throw this.createNetworkError(error);
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+ }
112
+
4
113
  /******************************************************************************
5
114
  Copyright (c) Microsoft Corporation.
6
115
 
@@ -43,7 +152,13 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
43
152
  * Fields that should not be formatted when sanitizing the query.
44
153
  * These fields are essential for maintaining the integrity of the content type.
45
154
  */
46
- const CONTENT_TYPE_MAIN_FIELDS = ['live', 'variant', 'contentType', 'languageId'];
155
+ const CONTENT_TYPE_MAIN_FIELDS = [
156
+ 'live',
157
+ 'variant',
158
+ 'contentType',
159
+ 'languageId',
160
+ 'conhost'
161
+ ];
47
162
  /**
48
163
  * URL endpoint for the content API search functionality.
49
164
  */
@@ -75,6 +190,58 @@ function sanitizeQueryForContentType(query, contentType) {
75
190
  : original; // Return the field if it is a content type field
76
191
  });
77
192
  }
193
+ /**
194
+ * @description
195
+ * Determines whether a site ID constraint should be added to a query based on existing constraints.
196
+ *
197
+ * The site ID constraint is added only when:
198
+ * - Query doesn't already contain a positive site constraint (+conhost)
199
+ * - Query doesn't explicitly exclude the specified site ID (-conhost:siteId)
200
+ * - Site ID is provided and configured
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const query = '+contentType:Blog +languageId:1';
205
+ * const siteId = '123';
206
+ * const shouldAdd = shouldAddSiteIdConstraint(query, siteId); // true
207
+ * ```
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * const query = '+contentType:Blog -conhost:123';
212
+ * const siteId = '123';
213
+ * const shouldAdd = shouldAddSiteIdConstraint(query, siteId); // false (explicitly excluded)
214
+ * ```
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * const query = '+contentType:Blog +conhost:456';
219
+ * const siteId = '123';
220
+ * const shouldAdd = shouldAddSiteIdConstraint(query, siteId); // false (already has constraint)
221
+ * ```
222
+ *
223
+ * @export
224
+ * @param {string} query - The Lucene query string to analyze
225
+ * @param {string | number | null | undefined} siteId - The site ID to check for
226
+ * @returns {boolean} True if site ID constraint should be added, false otherwise
227
+ */
228
+ function shouldAddSiteIdConstraint(query, siteId) {
229
+ // No site ID configured
230
+ if (!siteId) {
231
+ return false;
232
+ }
233
+ // Query already contains a positive site constraint
234
+ const hasExistingSiteConstraint = /\+conhost/gi.test(query);
235
+ if (hasExistingSiteConstraint) {
236
+ return false;
237
+ }
238
+ // Query explicitly excludes this specific site ID
239
+ const hasThisSiteIdExclusion = new RegExp(`-conhost:${siteId}`, 'gi').test(query);
240
+ if (hasThisSiteIdExclusion) {
241
+ return false;
242
+ }
243
+ return true;
244
+ }
78
245
 
79
246
  var _Field_query;
80
247
  /**
@@ -577,7 +744,7 @@ class QueryBuilder {
577
744
  }
578
745
  _QueryBuilder_query = new WeakMap();
579
746
 
580
- var _CollectionBuilder_page, _CollectionBuilder_limit, _CollectionBuilder_depth, _CollectionBuilder_render, _CollectionBuilder_sortBy, _CollectionBuilder_contentType, _CollectionBuilder_defaultQuery, _CollectionBuilder_query, _CollectionBuilder_rawQuery, _CollectionBuilder_languageId, _CollectionBuilder_draft, _CollectionBuilder_serverUrl, _CollectionBuilder_requestOptions;
747
+ var _CollectionBuilder_page, _CollectionBuilder_limit, _CollectionBuilder_depth, _CollectionBuilder_render, _CollectionBuilder_sortBy, _CollectionBuilder_contentType, _CollectionBuilder_defaultQuery, _CollectionBuilder_query, _CollectionBuilder_rawQuery, _CollectionBuilder_languageId, _CollectionBuilder_draft, _CollectionBuilder_requestOptions, _CollectionBuilder_httpClient, _CollectionBuilder_config;
581
748
  /**
582
749
  * Creates a Builder to filter and fetch content from the content API for a specific content type.
583
750
  *
@@ -589,11 +756,12 @@ class CollectionBuilder {
589
756
  /**
590
757
  * Creates an instance of CollectionBuilder.
591
758
  * @param {ClientOptions} requestOptions Options for the client request.
592
- * @param {string} serverUrl The server URL.
759
+ * @param {DotCMSClientConfig} config The client configuration.
593
760
  * @param {string} contentType The content type to fetch.
761
+ * @param {DotHttpClient} httpClient HTTP client for making requests.
594
762
  * @memberof CollectionBuilder
595
763
  */
596
- constructor(requestOptions, serverUrl, contentType) {
764
+ constructor(requestOptions, config, contentType, httpClient) {
597
765
  _CollectionBuilder_page.set(this, 1);
598
766
  _CollectionBuilder_limit.set(this, 10);
599
767
  _CollectionBuilder_depth.set(this, 0);
@@ -605,11 +773,14 @@ class CollectionBuilder {
605
773
  _CollectionBuilder_rawQuery.set(this, void 0);
606
774
  _CollectionBuilder_languageId.set(this, 1);
607
775
  _CollectionBuilder_draft.set(this, false);
608
- _CollectionBuilder_serverUrl.set(this, void 0);
609
776
  _CollectionBuilder_requestOptions.set(this, void 0);
777
+ _CollectionBuilder_httpClient.set(this, void 0);
778
+ _CollectionBuilder_config.set(this, void 0);
610
779
  __classPrivateFieldSet(this, _CollectionBuilder_requestOptions, requestOptions, "f");
611
- __classPrivateFieldSet(this, _CollectionBuilder_serverUrl, serverUrl, "f");
780
+ __classPrivateFieldSet(this, _CollectionBuilder_config, config, "f");
612
781
  __classPrivateFieldSet(this, _CollectionBuilder_contentType, contentType, "f");
782
+ __classPrivateFieldSet(this, _CollectionBuilder_httpClient, httpClient, "f");
783
+ __classPrivateFieldSet(this, _CollectionBuilder_config, config, "f");
613
784
  // Build the default query with the contentType field
614
785
  __classPrivateFieldSet(this, _CollectionBuilder_defaultQuery, new QueryBuilder().field('contentType').equals(__classPrivateFieldGet(this, _CollectionBuilder_contentType, "f")), "f");
615
786
  }
@@ -641,7 +812,17 @@ class CollectionBuilder {
641
812
  * @memberof CollectionBuilder
642
813
  */
643
814
  get url() {
644
- return `${__classPrivateFieldGet(this, _CollectionBuilder_serverUrl, "f")}${CONTENT_API_URL}`;
815
+ return `${__classPrivateFieldGet(this, _CollectionBuilder_config, "f").dotcmsUrl}${CONTENT_API_URL}`;
816
+ }
817
+ /**
818
+ * Returns the site ID from the configuration.
819
+ *
820
+ * @readonly
821
+ * @private
822
+ * @memberof CollectionBuilder
823
+ */
824
+ get siteId() {
825
+ return __classPrivateFieldGet(this, _CollectionBuilder_config, "f").siteId;
645
826
  }
646
827
  /**
647
828
  * Returns the current query built.
@@ -826,26 +1007,36 @@ class CollectionBuilder {
826
1007
  *
827
1008
  * @param {OnFullfilled} [onfulfilled] A callback that is called when the fetch is successful.
828
1009
  * @param {OnRejected} [onrejected] A callback that is called when the fetch fails.
829
- * @return {Promise<GetCollectionResponse<T> | GetCollectionError>} A promise that resolves to the content or rejects with an error.
1010
+ * @return {Promise<GetCollectionResponse<T> | DotErrorContent>} A promise that resolves to the content or rejects with an error.
830
1011
  * @memberof CollectionBuilder
831
1012
  */
832
1013
  then(onfulfilled, onrejected) {
833
- return this.fetch().then(async (response) => {
834
- const data = await response.json();
835
- if (response.ok) {
836
- const formattedResponse = this.formatResponse(data);
837
- const finalResponse = typeof onfulfilled === 'function'
838
- ? onfulfilled(formattedResponse)
839
- : formattedResponse;
840
- return finalResponse;
1014
+ return this.fetch().then((data) => {
1015
+ const formattedResponse = this.formatResponse(data);
1016
+ if (typeof onfulfilled === 'function') {
1017
+ const result = onfulfilled(formattedResponse);
1018
+ // Ensure we always return a value, fallback to formattedResponse if callback returns undefined
1019
+ return result ?? formattedResponse;
1020
+ }
1021
+ return formattedResponse;
1022
+ }, (error) => {
1023
+ // Wrap error in DotCMSContentError
1024
+ let contentError;
1025
+ if (error instanceof DotHttpError) {
1026
+ contentError = new DotErrorContent(`Content API failed for '${__classPrivateFieldGet(this, _CollectionBuilder_contentType, "f")}' (fetch): ${error.message}`, __classPrivateFieldGet(this, _CollectionBuilder_contentType, "f"), 'fetch', error, this.getFinalQuery());
841
1027
  }
842
1028
  else {
843
- return {
844
- status: response.status,
845
- ...data
846
- };
1029
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1030
+ contentError = new DotErrorContent(`Content API failed for '${__classPrivateFieldGet(this, _CollectionBuilder_contentType, "f")}' (fetch): ${errorMessage}`, __classPrivateFieldGet(this, _CollectionBuilder_contentType, "f"), 'fetch', undefined, this.getFinalQuery());
847
1031
  }
848
- }, onrejected);
1032
+ if (typeof onrejected === 'function') {
1033
+ const result = onrejected(contentError);
1034
+ // Ensure we always return a value, fallback to original error if callback returns undefined
1035
+ return result ?? contentError;
1036
+ }
1037
+ // Throw the wrapped error to trigger .catch()
1038
+ throw contentError;
1039
+ });
849
1040
  }
850
1041
  /**
851
1042
  * Formats the response to the desired format.
@@ -875,19 +1066,15 @@ class CollectionBuilder {
875
1066
  * Calls the content API to fetch the content.
876
1067
  *
877
1068
  * @private
878
- * @return {Promise<Response>} The fetch response.
1069
+ * @return {Promise<GetCollectionRawResponse<T>>} The fetch response data.
1070
+ * @throws {DotHttpError} When the HTTP request fails.
879
1071
  * @memberof CollectionBuilder
880
1072
  */
881
1073
  fetch() {
882
- const finalQuery = this.currentQuery
883
- .field('languageId')
884
- .equals(__classPrivateFieldGet(this, _CollectionBuilder_languageId, "f").toString())
885
- .field('live')
886
- .equals((!__classPrivateFieldGet(this, _CollectionBuilder_draft, "f")).toString())
887
- .build();
1074
+ const finalQuery = this.getFinalQuery();
888
1075
  const sanitizedQuery = sanitizeQueryForContentType(finalQuery, __classPrivateFieldGet(this, _CollectionBuilder_contentType, "f"));
889
1076
  const query = __classPrivateFieldGet(this, _CollectionBuilder_rawQuery, "f") ? `${sanitizedQuery} ${__classPrivateFieldGet(this, _CollectionBuilder_rawQuery, "f")}` : sanitizedQuery;
890
- return fetch(this.url, {
1077
+ return __classPrivateFieldGet(this, _CollectionBuilder_httpClient, "f").request(this.url, {
891
1078
  ...__classPrivateFieldGet(this, _CollectionBuilder_requestOptions, "f"),
892
1079
  method: 'POST',
893
1080
  headers: {
@@ -906,10 +1093,62 @@ class CollectionBuilder {
906
1093
  })
907
1094
  });
908
1095
  }
1096
+ /**
1097
+ * Builds the final Lucene query string by combining the base query with required system constraints.
1098
+ *
1099
+ * This method constructs the complete query by:
1100
+ * 1. Adding language ID filter to ensure content matches the specified language
1101
+ * 2. Adding live/draft status filter based on the draft flag
1102
+ * 3. Optionally adding site ID constraint if conditions are met
1103
+ *
1104
+ * Site ID constraint is added only when:
1105
+ * - Query doesn't already contain a positive site constraint (+conhost)
1106
+ * - Query doesn't explicitly exclude the current site ID (-conhost:currentSiteId)
1107
+ * - Site ID is configured in the system
1108
+ *
1109
+ * @private
1110
+ * @returns {string} The complete Lucene query string ready for the Content API
1111
+ * @memberof CollectionBuilder
1112
+ *
1113
+ * @example
1114
+ * // For live content in language 1 with site ID 123:
1115
+ * // Returns: "+contentType:Blog +languageId:1 +live:true +conhost:123"
1116
+ *
1117
+ * @example
1118
+ * // For draft content without site constraint:
1119
+ * // Returns: "+contentType:Blog +languageId:1 +live:false"
1120
+ *
1121
+ * @example
1122
+ * // For content with explicit exclusion of current site (site ID 123):
1123
+ * // Query: "+contentType:Blog -conhost:123"
1124
+ * // Returns: "+contentType:Blog -conhost:123 +languageId:1 +live:true" (no site ID added)
1125
+ *
1126
+ * @example
1127
+ * // For content with exclusion of different site (site ID 456, current is 123):
1128
+ * // Query: "+contentType:Blog -conhost:456"
1129
+ * // Returns: "+contentType:Blog -conhost:456 +languageId:1 +live:true +conhost:123" (site ID still added)
1130
+ */
1131
+ getFinalQuery() {
1132
+ // Build base query with language and live/draft constraints
1133
+ const baseQuery = this.currentQuery
1134
+ .field('languageId')
1135
+ .equals(__classPrivateFieldGet(this, _CollectionBuilder_languageId, "f").toString())
1136
+ .field('live')
1137
+ .equals((!__classPrivateFieldGet(this, _CollectionBuilder_draft, "f")).toString())
1138
+ .build();
1139
+ // Check if site ID constraint should be added using utility function
1140
+ const shouldAddSiteId = shouldAddSiteIdConstraint(baseQuery, this.siteId);
1141
+ // Add site ID constraint if needed
1142
+ if (shouldAddSiteId) {
1143
+ const queryWithSiteId = `${baseQuery} +conhost:${this.siteId}`;
1144
+ return sanitizeQuery(queryWithSiteId);
1145
+ }
1146
+ return baseQuery;
1147
+ }
909
1148
  }
910
- _CollectionBuilder_page = new WeakMap(), _CollectionBuilder_limit = new WeakMap(), _CollectionBuilder_depth = new WeakMap(), _CollectionBuilder_render = new WeakMap(), _CollectionBuilder_sortBy = new WeakMap(), _CollectionBuilder_contentType = new WeakMap(), _CollectionBuilder_defaultQuery = new WeakMap(), _CollectionBuilder_query = new WeakMap(), _CollectionBuilder_rawQuery = new WeakMap(), _CollectionBuilder_languageId = new WeakMap(), _CollectionBuilder_draft = new WeakMap(), _CollectionBuilder_serverUrl = new WeakMap(), _CollectionBuilder_requestOptions = new WeakMap();
1149
+ _CollectionBuilder_page = new WeakMap(), _CollectionBuilder_limit = new WeakMap(), _CollectionBuilder_depth = new WeakMap(), _CollectionBuilder_render = new WeakMap(), _CollectionBuilder_sortBy = new WeakMap(), _CollectionBuilder_contentType = new WeakMap(), _CollectionBuilder_defaultQuery = new WeakMap(), _CollectionBuilder_query = new WeakMap(), _CollectionBuilder_rawQuery = new WeakMap(), _CollectionBuilder_languageId = new WeakMap(), _CollectionBuilder_draft = new WeakMap(), _CollectionBuilder_requestOptions = new WeakMap(), _CollectionBuilder_httpClient = new WeakMap(), _CollectionBuilder_config = new WeakMap();
911
1150
 
912
- var _Content_requestOptions, _Content_serverUrl;
1151
+ var _Content_requestOptions, _Content_httpClient, _Content_config;
913
1152
  /**
914
1153
  * Creates a builder to filter and fetch a collection of content items.
915
1154
  * @param contentType - The content type to retrieve.
@@ -962,14 +1201,17 @@ var _Content_requestOptions, _Content_serverUrl;
962
1201
  class Content {
963
1202
  /**
964
1203
  * Creates an instance of Content.
965
- * @param {RequestOptions} requestOptions - The options for the client request.
1204
+ * @param {DotRequestOptions} requestOptions - The options for the client request.
966
1205
  * @param {string} serverUrl - The server URL.
1206
+ * @param {DotHttpClient} httpClient - HTTP client for making requests.
967
1207
  */
968
- constructor(requestOptions, serverUrl) {
1208
+ constructor(config, requestOptions, httpClient) {
969
1209
  _Content_requestOptions.set(this, void 0);
970
- _Content_serverUrl.set(this, void 0);
1210
+ _Content_httpClient.set(this, void 0);
1211
+ _Content_config.set(this, void 0);
971
1212
  __classPrivateFieldSet(this, _Content_requestOptions, requestOptions, "f");
972
- __classPrivateFieldSet(this, _Content_serverUrl, serverUrl, "f");
1213
+ __classPrivateFieldSet(this, _Content_config, config, "f");
1214
+ __classPrivateFieldSet(this, _Content_httpClient, httpClient, "f");
973
1215
  }
974
1216
  /**
975
1217
  * Takes a content type and returns a builder to filter and fetch the collection.
@@ -1037,35 +1279,44 @@ class Content {
1037
1279
  *
1038
1280
  */
1039
1281
  getCollection(contentType) {
1040
- return new CollectionBuilder(__classPrivateFieldGet(this, _Content_requestOptions, "f"), __classPrivateFieldGet(this, _Content_serverUrl, "f"), contentType);
1282
+ return new CollectionBuilder(__classPrivateFieldGet(this, _Content_requestOptions, "f"), __classPrivateFieldGet(this, _Content_config, "f"), contentType, __classPrivateFieldGet(this, _Content_httpClient, "f"));
1041
1283
  }
1042
1284
  }
1043
- _Content_requestOptions = new WeakMap(), _Content_serverUrl = new WeakMap();
1285
+ _Content_requestOptions = new WeakMap(), _Content_httpClient = new WeakMap(), _Content_config = new WeakMap();
1044
1286
 
1045
1287
  class NavigationClient {
1046
- constructor(config, requestOptions) {
1288
+ constructor(config, requestOptions, httpClient) {
1047
1289
  this.requestOptions = requestOptions;
1048
1290
  this.BASE_URL = `${config?.dotcmsUrl}/api/v1/nav`;
1291
+ this.httpClient = httpClient;
1049
1292
  }
1050
1293
  /**
1051
1294
  * Retrieves information about the dotCMS file and folder tree.
1052
- * @param {NavigationApiOptions} options - The options for the Navigation API call. Defaults to `{ depth: 0, path: '/', languageId: 1 }`.
1295
+ * @param {string} path - The path to retrieve navigation for.
1296
+ * @param {DotCMSNavigationRequestParams} params - The options for the Navigation API call.
1053
1297
  * @returns {Promise<DotCMSNavigationItem[]>} - A Promise that resolves to the response from the DotCMS API.
1054
- * @throws {Error} - Throws an error if the options are not valid.
1298
+ * @throws {DotErrorNavigation} - Throws a navigation-specific error if the request fails.
1055
1299
  */
1056
1300
  async get(path, params) {
1057
1301
  if (!path) {
1058
- throw new Error("The 'path' parameter is required for the Navigation API");
1302
+ throw new DotErrorNavigation("The 'path' parameter is required for the Navigation API", path);
1059
1303
  }
1060
1304
  const navParams = params ? this.mapToBackendParams(params) : {};
1061
1305
  const urlParams = new URLSearchParams(navParams).toString();
1062
1306
  const parsedPath = path.replace(/^\/+/, '/').replace(/\/+$/, '/');
1063
1307
  const url = `${this.BASE_URL}${parsedPath}${urlParams ? `?${urlParams}` : ''}`;
1064
- const response = await fetch(url, this.requestOptions);
1065
- if (!response.ok) {
1066
- throw new Error(`Failed to fetch navigation data: ${response.statusText} - ${response.status}`);
1308
+ try {
1309
+ const response = await this.httpClient.request(url, this.requestOptions);
1310
+ return response.entity;
1311
+ }
1312
+ catch (error) {
1313
+ // Handle DotHttpError instances from httpClient.request
1314
+ if (error instanceof DotHttpError) {
1315
+ throw new DotErrorNavigation(`Navigation API failed for path '${parsedPath}': ${error.message}`, parsedPath, error);
1316
+ }
1317
+ // Handle other errors (validation, network, etc.)
1318
+ throw new DotErrorNavigation(`Navigation API failed for path '${parsedPath}': ${error instanceof Error ? error.message : 'Unknown error'}`, parsedPath);
1067
1319
  }
1068
- return response.json().then((data) => data.entity);
1069
1320
  }
1070
1321
  mapToBackendParams(params) {
1071
1322
  const backendParams = {};
@@ -1079,26 +1330,6 @@ class NavigationClient {
1079
1330
  }
1080
1331
  }
1081
1332
 
1082
- /**
1083
- * A record of HTTP status codes and their corresponding error messages.
1084
- *
1085
- * @type {Record<number, string>}
1086
- * @property {string} 401 - Unauthorized. Check the token and try again.
1087
- * @property {string} 403 - Forbidden. Check the permissions and try again.
1088
- * @property {string} 404 - Not Found. Check the URL and try again.
1089
- * @property {string} 500 - Internal Server Error. Try again later.
1090
- * @property {string} 502 - Bad Gateway. Try again later.
1091
- * @property {string} 503 - Service Unavailable. Try again later.
1092
- */
1093
- const ErrorMessages = {
1094
- 401: 'Unauthorized. Check the token and try again.',
1095
- 403: 'Forbidden. Check the permissions and try again.',
1096
- 404: 'Not Found. Check the URL and try again.',
1097
- 500: 'Internal Server Error. Try again later.',
1098
- 502: 'Bad Gateway. Try again later.',
1099
- 503: 'Service Unavailable. Try again later.'
1100
- };
1101
-
1102
1333
  const DEFAULT_PAGE_CONTENTLETS_CONTENT = `
1103
1334
  publishDate
1104
1335
  inode
@@ -1314,11 +1545,14 @@ function buildQuery(queryData) {
1314
1545
  /**
1315
1546
  * Filters response data to include only specified keys.
1316
1547
  *
1317
- * @param {Record<string, string>} responseData - Original response data object
1548
+ * @param {Record<string, unknown> | undefined} responseData - Original response data object
1318
1549
  * @param {string[]} keys - Array of keys to extract from the response data
1319
- * @returns {Record<string, string>} New object containing only the specified keys
1550
+ * @returns {Record<string, unknown> | undefined} New object containing only the specified keys
1320
1551
  */
1321
- function mapResponseData(responseData, keys) {
1552
+ function mapContentResponse(responseData, keys) {
1553
+ if (!responseData) {
1554
+ return undefined;
1555
+ }
1322
1556
  return keys.reduce((accumulator, key) => {
1323
1557
  if (responseData[key] !== undefined) {
1324
1558
  accumulator[key] = responseData[key];
@@ -1332,25 +1566,19 @@ function mapResponseData(responseData, keys) {
1332
1566
  * @param {Object} options - Options for the fetch request
1333
1567
  * @param {string} options.body - GraphQL query string
1334
1568
  * @param {Record<string, string>} options.headers - HTTP headers for the request
1335
- * @returns {Promise<any>} Parsed JSON response from the GraphQL API
1336
- * @throws {Error} If the HTTP response is not successful
1569
+ * @param {DotHttpClient} options.httpClient - HTTP client for making requests
1570
+ * @returns {Promise<DotGraphQLApiResponse>} Parsed JSON response from the GraphQL API
1571
+ * @throws {DotHttpError} If the HTTP request fails (non-2xx status or network error)
1337
1572
  */
1338
- async function fetchGraphQL({ baseURL, body, headers }) {
1573
+ async function fetchGraphQL({ baseURL, body, headers, httpClient }) {
1339
1574
  const url = new URL(baseURL);
1340
1575
  url.pathname = '/api/v1/graphql';
1341
- const response = await fetch(url.toString(), {
1576
+ // httpClient.request throws DotHttpError on failure, so we just return the response directly
1577
+ return await httpClient.request(url.toString(), {
1342
1578
  method: 'POST',
1343
1579
  body,
1344
1580
  headers
1345
1581
  });
1346
- if (!response.ok) {
1347
- const error = {
1348
- status: response.status,
1349
- message: ErrorMessages[response.status] || response.statusText
1350
- };
1351
- throw error;
1352
- }
1353
- return await response.json();
1354
1582
  }
1355
1583
 
1356
1584
  /**
@@ -1362,7 +1590,8 @@ class PageClient {
1362
1590
  * Creates a new PageClient instance.
1363
1591
  *
1364
1592
  * @param {DotCMSClientConfig} config - Configuration options for the DotCMS client
1365
- * @param {RequestOptions} requestOptions - Options for fetch requests including authorization headers
1593
+ * @param {DotRequestOptions} requestOptions - Options for fetch requests including authorization headers
1594
+ * @param {DotHttpClient} httpClient - HTTP client for making requests
1366
1595
  * @example
1367
1596
  * ```typescript
1368
1597
  * const pageClient = new PageClient(
@@ -1375,14 +1604,16 @@ class PageClient {
1375
1604
  * headers: {
1376
1605
  * Authorization: 'Bearer your-auth-token'
1377
1606
  * }
1378
- * }
1607
+ * },
1608
+ * httpClient
1379
1609
  * );
1380
1610
  * ```
1381
1611
  */
1382
- constructor(config, requestOptions) {
1612
+ constructor(config, requestOptions, httpClient) {
1383
1613
  this.requestOptions = requestOptions;
1384
1614
  this.siteId = config.siteId || '';
1385
1615
  this.dotcmsUrl = config.dotcmsUrl;
1616
+ this.httpClient = httpClient;
1386
1617
  }
1387
1618
  /**
1388
1619
  * Retrieves a page from DotCMS using GraphQL.
@@ -1391,6 +1622,7 @@ class PageClient {
1391
1622
  * @param {DotCMSPageRequestParams} [options] - Options for the request
1392
1623
  * @template T - The type of the page and content, defaults to DotCMSBasicPage and Record<string, unknown> | unknown
1393
1624
  * @returns {Promise<DotCMSComposedPageResponse<T>>} A Promise that resolves to the page data
1625
+ * @throws {DotErrorPage} - Throws a page-specific error if the request fails or page is not found
1394
1626
  *
1395
1627
  * @example Using GraphQL
1396
1628
  * ```typescript
@@ -1458,21 +1690,26 @@ class PageClient {
1458
1690
  const requestHeaders = this.requestOptions.headers;
1459
1691
  const requestBody = JSON.stringify({ query: completeQuery, variables: requestVariables });
1460
1692
  try {
1461
- const { data, errors } = await fetchGraphQL({
1693
+ const response = await fetchGraphQL({
1462
1694
  baseURL: this.dotcmsUrl,
1463
1695
  body: requestBody,
1464
- headers: requestHeaders
1696
+ headers: requestHeaders,
1697
+ httpClient: this.httpClient
1465
1698
  });
1466
- if (errors) {
1467
- errors.forEach((error) => {
1699
+ // The GQL endpoint can return errors and data, we need to handle both
1700
+ if (response.errors) {
1701
+ response.errors.forEach((error) => {
1468
1702
  consola.error('[DotCMS GraphQL Error]: ', error.message);
1469
1703
  });
1470
1704
  }
1471
- const pageResponse = graphqlToPageEntity(data);
1705
+ const pageResponse = graphqlToPageEntity(response.data.page);
1472
1706
  if (!pageResponse) {
1473
- throw new Error('No page data found');
1707
+ throw new DotErrorPage(`Page ${url} not found. Check the page URL and permissions.`, undefined, {
1708
+ query: completeQuery,
1709
+ variables: requestVariables
1710
+ });
1474
1711
  }
1475
- const contentResponse = mapResponseData(data, Object.keys(content));
1712
+ const contentResponse = mapContentResponse(response.data, Object.keys(content));
1476
1713
  return {
1477
1714
  pageAsset: pageResponse,
1478
1715
  content: contentResponse,
@@ -1483,15 +1720,18 @@ class PageClient {
1483
1720
  };
1484
1721
  }
1485
1722
  catch (error) {
1486
- const errorMessage = {
1487
- error,
1488
- message: 'Failed to retrieve page data',
1489
- graphql: {
1723
+ // Handle DotHttpError instances from httpClient.request
1724
+ if (error instanceof DotHttpError) {
1725
+ throw new DotErrorPage(`Page request failed for URL '${url}': ${error.message}`, error, {
1490
1726
  query: completeQuery,
1491
1727
  variables: requestVariables
1492
- }
1493
- };
1494
- throw errorMessage;
1728
+ });
1729
+ }
1730
+ // Handle other errors (GraphQL errors, validation errors, etc.)
1731
+ throw new DotErrorPage(`Page request failed for URL '${url}': ${error instanceof Error ? error.message : 'Unknown error'}`, undefined, {
1732
+ query: completeQuery,
1733
+ variables: requestVariables
1734
+ });
1495
1735
  }
1496
1736
  }
1497
1737
  }
@@ -1532,11 +1772,12 @@ class DotCMSClient {
1532
1772
  */
1533
1773
  constructor(config = defaultConfig) {
1534
1774
  this.config = config;
1775
+ this.httpClient = config.httpClient || new FetchHttpClient();
1535
1776
  this.requestOptions = this.createAuthenticatedRequestOptions(this.config);
1536
- // Initialize clients
1537
- this.page = new PageClient(this.config, this.requestOptions);
1538
- this.nav = new NavigationClient(this.config, this.requestOptions);
1539
- this.content = new Content(this.requestOptions, this.config.dotcmsUrl);
1777
+ // Initialize clients with httpClient
1778
+ this.page = new PageClient(this.config, this.requestOptions, this.httpClient);
1779
+ this.nav = new NavigationClient(this.config, this.requestOptions, this.httpClient);
1780
+ this.content = new Content(this.config, this.requestOptions, this.httpClient);
1540
1781
  }
1541
1782
  /**
1542
1783
  * Creates request options with authentication headers.