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