@dotcms/client 1.0.0 → 1.0.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/.eslintrc.json +18 -0
- package/CLAUDE.md +253 -0
- package/MIGRATION.md +329 -0
- package/README.md +250 -434
- package/jest.config.ts +15 -0
- package/package.json +8 -25
- package/project.json +73 -0
- package/src/lib/client/client.spec.ts +147 -0
- package/src/lib/client/client.ts +125 -0
- package/src/lib/client/content/builders/collection/collection.spec.ts +514 -0
- package/src/lib/client/content/builders/collection/{collection.d.ts → collection.ts} +210 -19
- package/src/lib/client/content/builders/query/lucene-syntax/{Equals.d.ts → Equals.ts} +45 -11
- package/src/lib/client/content/builders/query/lucene-syntax/{Field.d.ts → Field.ts} +13 -5
- package/src/lib/client/content/builders/query/lucene-syntax/{NotOperand.d.ts → NotOperand.ts} +13 -5
- package/src/lib/client/content/builders/query/lucene-syntax/{Operand.d.ts → Operand.ts} +21 -7
- package/src/lib/client/content/builders/query/query.spec.ts +159 -0
- package/src/lib/client/content/builders/query/{query.d.ts → query.ts} +16 -5
- package/src/lib/client/content/builders/query/utils/{index.d.ts → index.ts} +49 -12
- package/src/lib/client/content/{content-api.d.ts → content-api.ts} +14 -4
- package/src/lib/client/content/shared/{const.d.ts → const.ts} +5 -3
- package/src/lib/client/content/shared/{types.d.ts → types.ts} +18 -2
- package/src/lib/client/content/shared/{utils.d.ts → utils.ts} +9 -1
- package/src/lib/client/models/{index.d.ts → index.ts} +8 -1
- package/src/lib/client/navigation/navigation-api.spec.ts +167 -0
- package/src/lib/client/navigation/navigation-api.ts +62 -0
- package/src/lib/client/page/page-api.spec.ts +359 -0
- package/src/lib/client/page/page-api.ts +197 -0
- package/src/lib/client/page/utils.ts +291 -0
- package/src/lib/utils/graphql/transforms.spec.ts +250 -0
- package/src/lib/utils/graphql/transforms.ts +128 -0
- package/tsconfig.json +22 -0
- package/tsconfig.lib.json +13 -0
- package/tsconfig.spec.json +9 -0
- package/index.cjs.d.ts +0 -1
- package/index.cjs.default.js +0 -1
- package/index.cjs.js +0 -1591
- package/index.cjs.mjs +0 -2
- package/index.esm.d.ts +0 -1
- package/index.esm.js +0 -1589
- package/internal.cjs.d.ts +0 -1
- package/internal.cjs.default.js +0 -1
- package/internal.cjs.js +0 -85
- package/internal.cjs.mjs +0 -2
- package/internal.esm.d.ts +0 -1
- package/internal.esm.js +0 -83
- package/src/lib/client/client.d.ts +0 -56
- package/src/lib/client/navigation/navigation-api.d.ts +0 -14
- package/src/lib/client/page/page-api.d.ts +0 -95
- package/src/lib/client/page/utils.d.ts +0 -41
- package/src/lib/utils/graphql/transforms.d.ts +0 -13
- /package/src/{index.d.ts → index.ts} +0 -0
- /package/src/{internal.d.ts → internal.ts} +0 -0
- /package/src/lib/client/content/builders/query/lucene-syntax/{index.d.ts → index.ts} +0 -0
- /package/src/lib/utils/{index.d.ts → index.ts} +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DotCMSClientConfig,
|
|
3
|
+
DotCMSNavigationRequestParams,
|
|
4
|
+
RequestOptions,
|
|
5
|
+
DotCMSNavigationItem
|
|
6
|
+
} from '@dotcms/types';
|
|
7
|
+
|
|
8
|
+
export class NavigationClient {
|
|
9
|
+
private requestOptions: RequestOptions;
|
|
10
|
+
|
|
11
|
+
private BASE_URL: string;
|
|
12
|
+
|
|
13
|
+
constructor(config: DotCMSClientConfig, requestOptions: RequestOptions) {
|
|
14
|
+
this.requestOptions = requestOptions;
|
|
15
|
+
this.BASE_URL = `${config?.dotcmsUrl}/api/v1/nav`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Retrieves information about the dotCMS file and folder tree.
|
|
20
|
+
* @param {NavigationApiOptions} options - The options for the Navigation API call. Defaults to `{ depth: 0, path: '/', languageId: 1 }`.
|
|
21
|
+
* @returns {Promise<DotCMSNavigationItem[]>} - A Promise that resolves to the response from the DotCMS API.
|
|
22
|
+
* @throws {Error} - Throws an error if the options are not valid.
|
|
23
|
+
*/
|
|
24
|
+
async get(
|
|
25
|
+
path: string,
|
|
26
|
+
params?: DotCMSNavigationRequestParams
|
|
27
|
+
): Promise<DotCMSNavigationItem[]> {
|
|
28
|
+
if (!path) {
|
|
29
|
+
throw new Error("The 'path' parameter is required for the Navigation API");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const navParams = params ? this.mapToBackendParams(params) : {};
|
|
33
|
+
const urlParams = new URLSearchParams(navParams).toString();
|
|
34
|
+
|
|
35
|
+
const parsedPath = path.replace(/^\/+/, '/').replace(/\/+$/, '/');
|
|
36
|
+
const url = `${this.BASE_URL}${parsedPath}${urlParams ? `?${urlParams}` : ''}`;
|
|
37
|
+
|
|
38
|
+
const response = await fetch(url, this.requestOptions);
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Failed to fetch navigation data: ${response.statusText} - ${response.status}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return response.json().then((data) => data.entity);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private mapToBackendParams(params: DotCMSNavigationRequestParams): Record<string, string> {
|
|
50
|
+
const backendParams: Record<string, string> = {};
|
|
51
|
+
|
|
52
|
+
if (params.depth) {
|
|
53
|
+
backendParams['depth'] = String(params.depth);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (params.languageId) {
|
|
57
|
+
backendParams['language_id'] = String(params.languageId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return backendParams;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
jest.mock('consola');
|
|
3
|
+
|
|
4
|
+
import * as consola from 'consola';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DotCMSClientConfig,
|
|
8
|
+
DotCMSPageRequestParams,
|
|
9
|
+
RequestOptions,
|
|
10
|
+
DotCMSGraphQLPageResponse,
|
|
11
|
+
DotCMSPageResponse
|
|
12
|
+
} from '@dotcms/types';
|
|
13
|
+
|
|
14
|
+
import { PageClient } from './page-api';
|
|
15
|
+
import * as utils from './utils';
|
|
16
|
+
|
|
17
|
+
import { graphqlToPageEntity } from '../../utils';
|
|
18
|
+
|
|
19
|
+
describe('PageClient', () => {
|
|
20
|
+
const mockFetchGraphQL = jest.fn();
|
|
21
|
+
|
|
22
|
+
const validConfig: DotCMSClientConfig = {
|
|
23
|
+
dotcmsUrl: 'https://demo.dotcms.com',
|
|
24
|
+
authToken: 'test-token',
|
|
25
|
+
siteId: 'test-site'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const requestOptions: RequestOptions = {
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: 'Bearer test-token'
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const mockGraphQLResponse = {
|
|
35
|
+
data: {
|
|
36
|
+
page: {
|
|
37
|
+
title: 'GraphQL Page',
|
|
38
|
+
url: '/graphql-page'
|
|
39
|
+
},
|
|
40
|
+
testContent: {
|
|
41
|
+
items: [{ title: 'Content Item' }]
|
|
42
|
+
},
|
|
43
|
+
testNav: {
|
|
44
|
+
items: [{ label: 'Nav Item', url: '/nav' }]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
errors: null
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
mockFetchGraphQL.mockReset();
|
|
52
|
+
global.console.error = jest.fn(); // Mock console.error to prevent actual errors from being logged in the console when running tests
|
|
53
|
+
|
|
54
|
+
jest.spyOn(utils, 'fetchGraphQL').mockImplementation(mockFetchGraphQL);
|
|
55
|
+
mockFetchGraphQL.mockResolvedValue(mockGraphQLResponse);
|
|
56
|
+
|
|
57
|
+
jest.spyOn(utils, 'buildPageQuery').mockReturnValue('mock-page-query');
|
|
58
|
+
jest.spyOn(utils, 'buildQuery').mockReturnValue('mock-query');
|
|
59
|
+
jest.spyOn(utils, 'mapResponseData').mockImplementation((data, keys) => {
|
|
60
|
+
const result: Record<string, any> = {};
|
|
61
|
+
|
|
62
|
+
keys.forEach((key) => {
|
|
63
|
+
result[key] = data[`test${key.charAt(0).toUpperCase() + key.slice(1)}`];
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return result;
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
jest.restoreAllMocks();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('GraphQL API', () => {
|
|
75
|
+
it('should fetch page using GraphQL when query option is provided', async () => {
|
|
76
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
77
|
+
|
|
78
|
+
const graphQLOptions: DotCMSPageRequestParams = {
|
|
79
|
+
graphql: {
|
|
80
|
+
page: `containers {
|
|
81
|
+
containerContentlets {
|
|
82
|
+
contentlets {
|
|
83
|
+
... on Banner {
|
|
84
|
+
title
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}`,
|
|
89
|
+
content: { content: 'query Content { items { title } }' }
|
|
90
|
+
},
|
|
91
|
+
languageId: '1',
|
|
92
|
+
mode: 'LIVE'
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const result = await pageClient.get('/graphql-page', graphQLOptions);
|
|
96
|
+
|
|
97
|
+
expect(utils.buildPageQuery).toHaveBeenCalled();
|
|
98
|
+
expect(utils.buildQuery).toHaveBeenCalledTimes(1);
|
|
99
|
+
expect(utils.fetchGraphQL).toHaveBeenCalledWith({
|
|
100
|
+
body: expect.any(String),
|
|
101
|
+
headers: requestOptions.headers,
|
|
102
|
+
baseURL: 'https://demo.dotcms.com'
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
expect(result).toEqual({
|
|
106
|
+
pageAsset: graphqlToPageEntity(
|
|
107
|
+
mockGraphQLResponse.data as unknown as DotCMSGraphQLPageResponse
|
|
108
|
+
),
|
|
109
|
+
content: { content: mockGraphQLResponse.data.testContent },
|
|
110
|
+
graphql: {
|
|
111
|
+
query: expect.any(String),
|
|
112
|
+
variables: expect.any(Object)
|
|
113
|
+
},
|
|
114
|
+
vanityUrl: undefined,
|
|
115
|
+
runningExperimentId: undefined
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should print graphql errors ', async () => {
|
|
120
|
+
const consolaSpy = jest.spyOn(consola, 'error');
|
|
121
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
122
|
+
const graphQLOptions = {
|
|
123
|
+
graphql: {
|
|
124
|
+
page: `containers {
|
|
125
|
+
containerContentlets {
|
|
126
|
+
contentlets {
|
|
127
|
+
... on Banner {
|
|
128
|
+
title
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}`,
|
|
133
|
+
content: { content: 'query Content { items { title } }' }
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
mockFetchGraphQL.mockResolvedValue({
|
|
138
|
+
data: {
|
|
139
|
+
page: {
|
|
140
|
+
title: 'GraphQL Page'
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
errors: [{ message: 'Some internal server error' }]
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await pageClient.get('/graphql-page', graphQLOptions);
|
|
147
|
+
|
|
148
|
+
expect(consolaSpy).toHaveBeenCalledWith(
|
|
149
|
+
'[DotCMS GraphQL Error]: ',
|
|
150
|
+
'Some internal server error'
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return an error if the page is not found', async () => {
|
|
155
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
156
|
+
const graphQLOptions = {
|
|
157
|
+
graphql: {
|
|
158
|
+
page: `containers {
|
|
159
|
+
containerContentlets {
|
|
160
|
+
contentlets {
|
|
161
|
+
... on Banner {
|
|
162
|
+
title
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}`,
|
|
167
|
+
content: { content: 'query Content { items { title } }' }
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
mockFetchGraphQL.mockResolvedValue({
|
|
172
|
+
data: {
|
|
173
|
+
page: null
|
|
174
|
+
},
|
|
175
|
+
errors: [{ message: 'No page data found' }]
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await pageClient.get('/graphql-page', graphQLOptions);
|
|
180
|
+
} catch (response: unknown) {
|
|
181
|
+
const responseData = response as DotCMSPageResponse;
|
|
182
|
+
|
|
183
|
+
expect(responseData.error?.message).toBe('No page data found');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should add leading slash to url if it does not have it', async () => {
|
|
188
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
189
|
+
const graphQLOptions = {
|
|
190
|
+
graphql: {
|
|
191
|
+
page: `containers {
|
|
192
|
+
containerContentlets {
|
|
193
|
+
contentlets {
|
|
194
|
+
... on Banner {
|
|
195
|
+
title
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}`,
|
|
200
|
+
content: { content: 'query Content { items { title } }' }
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// No leading slash
|
|
205
|
+
const result = await pageClient.get('graphql-page', graphQLOptions as any);
|
|
206
|
+
|
|
207
|
+
expect(result).toEqual({
|
|
208
|
+
pageAsset: graphqlToPageEntity(
|
|
209
|
+
mockGraphQLResponse.data as unknown as DotCMSGraphQLPageResponse
|
|
210
|
+
),
|
|
211
|
+
content: { content: mockGraphQLResponse.data.testContent },
|
|
212
|
+
graphql: {
|
|
213
|
+
query: expect.any(String),
|
|
214
|
+
variables: expect.objectContaining({
|
|
215
|
+
url: '/graphql-page'
|
|
216
|
+
})
|
|
217
|
+
},
|
|
218
|
+
vanityUrl: undefined,
|
|
219
|
+
runningExperimentId: undefined
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should pass correct variables to GraphQL query', async () => {
|
|
224
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
225
|
+
const graphQLOptions = {
|
|
226
|
+
graphql: {
|
|
227
|
+
page: `containers {
|
|
228
|
+
containerContentlets {
|
|
229
|
+
contentlets {
|
|
230
|
+
... on Banner {
|
|
231
|
+
title
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}`,
|
|
236
|
+
content: { content: 'query Content { items { title } }' }
|
|
237
|
+
},
|
|
238
|
+
languageId: '2',
|
|
239
|
+
mode: 'PREVIEW_MODE'
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
await pageClient.get('/custom-page', graphQLOptions as any);
|
|
243
|
+
|
|
244
|
+
const requestBody = JSON.parse(mockFetchGraphQL.mock.calls[0][0].body);
|
|
245
|
+
expect(requestBody.variables).toEqual({
|
|
246
|
+
url: '/custom-page',
|
|
247
|
+
mode: 'PREVIEW_MODE',
|
|
248
|
+
languageId: '2',
|
|
249
|
+
fireRules: false,
|
|
250
|
+
siteId: 'test-site'
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should handle GraphQL errors', async () => {
|
|
255
|
+
mockFetchGraphQL.mockRejectedValue(new Error('GraphQL error'));
|
|
256
|
+
|
|
257
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
258
|
+
const graphQLOptions = {
|
|
259
|
+
graphql: {
|
|
260
|
+
page: `containers {
|
|
261
|
+
containerContentlets {
|
|
262
|
+
contentlets {
|
|
263
|
+
... on Banner {
|
|
264
|
+
title
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}`,
|
|
269
|
+
content: { content: 'query Content { items { title } }' }
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
await pageClient.get('/page', graphQLOptions);
|
|
274
|
+
} catch (error: any) {
|
|
275
|
+
expect(error.message).toBe('Failed to retrieve page data');
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should throw errors from GraphQL', async () => {
|
|
280
|
+
mockFetchGraphQL.mockResolvedValue({
|
|
281
|
+
errors: [{ message: 'GraphQL error' }]
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
285
|
+
const graphQLOptions = {
|
|
286
|
+
graphql: {
|
|
287
|
+
page: `containers {
|
|
288
|
+
containerContentlets {
|
|
289
|
+
contentlets {
|
|
290
|
+
... on Banner {
|
|
291
|
+
title
|
|
292
|
+
}
|
|
293
|
+
}`,
|
|
294
|
+
content: { content: 'query Content { items { title } }' }
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
try {
|
|
298
|
+
await pageClient.get('/page', graphQLOptions);
|
|
299
|
+
} catch (error: any) {
|
|
300
|
+
expect(error.message).toBe('Failed to retrieve page data');
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('should use default values for languageId and mode if not provided', async () => {
|
|
305
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
306
|
+
const graphQLOptions = {
|
|
307
|
+
graphql: {
|
|
308
|
+
page: `containers {
|
|
309
|
+
containerContentlets {
|
|
310
|
+
contentlets {
|
|
311
|
+
... on Banner {
|
|
312
|
+
title
|
|
313
|
+
}
|
|
314
|
+
}`,
|
|
315
|
+
content: { content: 'query Content { items { title } }' }
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
await pageClient.get('/default-page', graphQLOptions);
|
|
319
|
+
|
|
320
|
+
const requestBody = JSON.parse(mockFetchGraphQL.mock.calls[0][0].body);
|
|
321
|
+
expect(requestBody.variables).toEqual({
|
|
322
|
+
url: '/default-page',
|
|
323
|
+
mode: 'LIVE',
|
|
324
|
+
languageId: '1',
|
|
325
|
+
fireRules: false,
|
|
326
|
+
siteId: 'test-site'
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should fetch using graphQL even if there is no graphql option', async () => {
|
|
331
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
332
|
+
|
|
333
|
+
await pageClient.get('/why-obi-wan-had-the-high-ground');
|
|
334
|
+
|
|
335
|
+
expect(mockFetchGraphQL).toHaveBeenCalled();
|
|
336
|
+
expect(utils.buildPageQuery).toHaveBeenCalled();
|
|
337
|
+
expect(utils.buildQuery).toHaveBeenCalledTimes(1);
|
|
338
|
+
expect(utils.fetchGraphQL).toHaveBeenCalledWith({
|
|
339
|
+
body: expect.stringContaining('"url":"/why-obi-wan-had-the-high-ground"'),
|
|
340
|
+
headers: requestOptions.headers,
|
|
341
|
+
baseURL: 'https://demo.dotcms.com'
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
describe('Client initialization', () => {
|
|
347
|
+
it('should use siteId from config when not provided in params', async () => {
|
|
348
|
+
const pageClient = new PageClient(validConfig, requestOptions);
|
|
349
|
+
|
|
350
|
+
await pageClient.get('/test-page');
|
|
351
|
+
|
|
352
|
+
expect(mockFetchGraphQL).toHaveBeenCalledWith({
|
|
353
|
+
baseURL: 'https://demo.dotcms.com',
|
|
354
|
+
body: expect.stringContaining('"siteId":"test-site"'),
|
|
355
|
+
headers: requestOptions.headers
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import consola from 'consola';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DotCMSClientConfig,
|
|
5
|
+
DotCMSComposedPageResponse,
|
|
6
|
+
DotCMSExtendedPageResponse,
|
|
7
|
+
DotCMSPageResponse,
|
|
8
|
+
DotCMSPageRequestParams,
|
|
9
|
+
RequestOptions
|
|
10
|
+
} from '@dotcms/types';
|
|
11
|
+
|
|
12
|
+
import { buildPageQuery, buildQuery, fetchGraphQL, mapResponseData } from './utils';
|
|
13
|
+
|
|
14
|
+
import { graphqlToPageEntity } from '../../utils';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Client for interacting with the DotCMS Page API.
|
|
18
|
+
* Provides methods to retrieve and manipulate pages.
|
|
19
|
+
*/
|
|
20
|
+
export class PageClient {
|
|
21
|
+
/**
|
|
22
|
+
* Request options including authorization headers.
|
|
23
|
+
* @private
|
|
24
|
+
*/
|
|
25
|
+
private requestOptions: RequestOptions;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Site ID for page requests.
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
private siteId: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* DotCMS URL for page requests.
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
private dotcmsUrl: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new PageClient instance.
|
|
41
|
+
*
|
|
42
|
+
* @param {DotCMSClientConfig} config - Configuration options for the DotCMS client
|
|
43
|
+
* @param {RequestOptions} requestOptions - Options for fetch requests including authorization headers
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* const pageClient = new PageClient(
|
|
47
|
+
* {
|
|
48
|
+
* dotcmsUrl: 'https://demo.dotcms.com',
|
|
49
|
+
* authToken: 'your-auth-token',
|
|
50
|
+
* siteId: 'demo.dotcms.com'
|
|
51
|
+
* },
|
|
52
|
+
* {
|
|
53
|
+
* headers: {
|
|
54
|
+
* Authorization: 'Bearer your-auth-token'
|
|
55
|
+
* }
|
|
56
|
+
* }
|
|
57
|
+
* );
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
constructor(config: DotCMSClientConfig, requestOptions: RequestOptions) {
|
|
61
|
+
this.requestOptions = requestOptions;
|
|
62
|
+
this.siteId = config.siteId || '';
|
|
63
|
+
this.dotcmsUrl = config.dotcmsUrl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Retrieves a page from DotCMS using GraphQL.
|
|
68
|
+
*
|
|
69
|
+
* @param {string} url - The URL of the page to retrieve
|
|
70
|
+
* @param {DotCMSPageRequestParams} [options] - Options for the request
|
|
71
|
+
* @template T - The type of the page and content, defaults to DotCMSBasicPage and Record<string, unknown> | unknown
|
|
72
|
+
* @returns {Promise<DotCMSComposedPageResponse<T>>} A Promise that resolves to the page data
|
|
73
|
+
*
|
|
74
|
+
* @example Using GraphQL
|
|
75
|
+
* ```typescript
|
|
76
|
+
* const page = await pageClient.get<{ page: MyPageWithBanners; content: { blogPosts: { blogTitle: string } } }>(
|
|
77
|
+
* '/index',
|
|
78
|
+
* {
|
|
79
|
+
* languageId: '1',
|
|
80
|
+
* mode: 'LIVE',
|
|
81
|
+
* graphql: {
|
|
82
|
+
* page: `
|
|
83
|
+
* containers {
|
|
84
|
+
* containerContentlets {
|
|
85
|
+
* contentlets {
|
|
86
|
+
* ... on Banner {
|
|
87
|
+
* ...bannerFragment
|
|
88
|
+
* }
|
|
89
|
+
* }
|
|
90
|
+
* }
|
|
91
|
+
* `,
|
|
92
|
+
* content: {
|
|
93
|
+
* blogPosts: `
|
|
94
|
+
* BlogCollection(limit: 3) {
|
|
95
|
+
* ...blogFragment
|
|
96
|
+
* }
|
|
97
|
+
* `,
|
|
98
|
+
* },
|
|
99
|
+
* fragments: [
|
|
100
|
+
* `
|
|
101
|
+
* fragment bannerFragment on Banner {
|
|
102
|
+
* caption
|
|
103
|
+
* }
|
|
104
|
+
* `,
|
|
105
|
+
* `
|
|
106
|
+
* fragment blogFragment on Blog {
|
|
107
|
+
* title
|
|
108
|
+
* urlTitle
|
|
109
|
+
* }
|
|
110
|
+
* `
|
|
111
|
+
* ]
|
|
112
|
+
* }
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
async get<T extends DotCMSExtendedPageResponse = DotCMSPageResponse>(
|
|
117
|
+
url: string,
|
|
118
|
+
options?: DotCMSPageRequestParams
|
|
119
|
+
): Promise<DotCMSComposedPageResponse<T>> {
|
|
120
|
+
const {
|
|
121
|
+
languageId = '1',
|
|
122
|
+
mode = 'LIVE',
|
|
123
|
+
siteId = this.siteId,
|
|
124
|
+
fireRules = false,
|
|
125
|
+
personaId,
|
|
126
|
+
publishDate,
|
|
127
|
+
variantName,
|
|
128
|
+
graphql = {}
|
|
129
|
+
} = options || {};
|
|
130
|
+
const { page, content = {}, variables, fragments } = graphql;
|
|
131
|
+
|
|
132
|
+
const contentQuery = buildQuery(content);
|
|
133
|
+
const completeQuery = buildPageQuery({
|
|
134
|
+
page,
|
|
135
|
+
fragments,
|
|
136
|
+
additionalQueries: contentQuery
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const requestVariables: Record<string, unknown> = {
|
|
140
|
+
// The url is expected to have a leading slash to comply on VanityURL Matching, some frameworks like Angular will not add the leading slash
|
|
141
|
+
url: url.startsWith('/') ? url : `/${url}`,
|
|
142
|
+
mode,
|
|
143
|
+
languageId,
|
|
144
|
+
personaId,
|
|
145
|
+
fireRules,
|
|
146
|
+
publishDate,
|
|
147
|
+
siteId,
|
|
148
|
+
variantName,
|
|
149
|
+
...variables
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const requestHeaders = this.requestOptions.headers as Record<string, string>;
|
|
153
|
+
const requestBody = JSON.stringify({ query: completeQuery, variables: requestVariables });
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const { data, errors } = await fetchGraphQL({
|
|
157
|
+
baseURL: this.dotcmsUrl,
|
|
158
|
+
body: requestBody,
|
|
159
|
+
headers: requestHeaders
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (errors) {
|
|
163
|
+
errors.forEach((error: { message: string }) => {
|
|
164
|
+
consola.error('[DotCMS GraphQL Error]: ', error.message);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const pageResponse = graphqlToPageEntity(data);
|
|
169
|
+
|
|
170
|
+
if (!pageResponse) {
|
|
171
|
+
throw new Error('No page data found');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const contentResponse = mapResponseData(data, Object.keys(content));
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
pageAsset: pageResponse,
|
|
178
|
+
content: contentResponse,
|
|
179
|
+
graphql: {
|
|
180
|
+
query: completeQuery,
|
|
181
|
+
variables: requestVariables
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const errorMessage = {
|
|
186
|
+
error,
|
|
187
|
+
message: 'Failed to retrieve page data',
|
|
188
|
+
graphql: {
|
|
189
|
+
query: completeQuery,
|
|
190
|
+
variables: requestVariables
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
throw errorMessage;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|