@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.
Files changed (54) hide show
  1. package/.eslintrc.json +18 -0
  2. package/CLAUDE.md +253 -0
  3. package/MIGRATION.md +329 -0
  4. package/README.md +250 -434
  5. package/jest.config.ts +15 -0
  6. package/package.json +8 -25
  7. package/project.json +73 -0
  8. package/src/lib/client/client.spec.ts +147 -0
  9. package/src/lib/client/client.ts +125 -0
  10. package/src/lib/client/content/builders/collection/collection.spec.ts +514 -0
  11. package/src/lib/client/content/builders/collection/{collection.d.ts → collection.ts} +210 -19
  12. package/src/lib/client/content/builders/query/lucene-syntax/{Equals.d.ts → Equals.ts} +45 -11
  13. package/src/lib/client/content/builders/query/lucene-syntax/{Field.d.ts → Field.ts} +13 -5
  14. package/src/lib/client/content/builders/query/lucene-syntax/{NotOperand.d.ts → NotOperand.ts} +13 -5
  15. package/src/lib/client/content/builders/query/lucene-syntax/{Operand.d.ts → Operand.ts} +21 -7
  16. package/src/lib/client/content/builders/query/query.spec.ts +159 -0
  17. package/src/lib/client/content/builders/query/{query.d.ts → query.ts} +16 -5
  18. package/src/lib/client/content/builders/query/utils/{index.d.ts → index.ts} +49 -12
  19. package/src/lib/client/content/{content-api.d.ts → content-api.ts} +14 -4
  20. package/src/lib/client/content/shared/{const.d.ts → const.ts} +5 -3
  21. package/src/lib/client/content/shared/{types.d.ts → types.ts} +18 -2
  22. package/src/lib/client/content/shared/{utils.d.ts → utils.ts} +9 -1
  23. package/src/lib/client/models/{index.d.ts → index.ts} +8 -1
  24. package/src/lib/client/navigation/navigation-api.spec.ts +167 -0
  25. package/src/lib/client/navigation/navigation-api.ts +62 -0
  26. package/src/lib/client/page/page-api.spec.ts +359 -0
  27. package/src/lib/client/page/page-api.ts +197 -0
  28. package/src/lib/client/page/utils.ts +291 -0
  29. package/src/lib/utils/graphql/transforms.spec.ts +250 -0
  30. package/src/lib/utils/graphql/transforms.ts +128 -0
  31. package/tsconfig.json +22 -0
  32. package/tsconfig.lib.json +13 -0
  33. package/tsconfig.spec.json +9 -0
  34. package/index.cjs.d.ts +0 -1
  35. package/index.cjs.default.js +0 -1
  36. package/index.cjs.js +0 -1591
  37. package/index.cjs.mjs +0 -2
  38. package/index.esm.d.ts +0 -1
  39. package/index.esm.js +0 -1589
  40. package/internal.cjs.d.ts +0 -1
  41. package/internal.cjs.default.js +0 -1
  42. package/internal.cjs.js +0 -85
  43. package/internal.cjs.mjs +0 -2
  44. package/internal.esm.d.ts +0 -1
  45. package/internal.esm.js +0 -83
  46. package/src/lib/client/client.d.ts +0 -56
  47. package/src/lib/client/navigation/navigation-api.d.ts +0 -14
  48. package/src/lib/client/page/page-api.d.ts +0 -95
  49. package/src/lib/client/page/utils.d.ts +0 -41
  50. package/src/lib/utils/graphql/transforms.d.ts +0 -13
  51. /package/src/{index.d.ts → index.ts} +0 -0
  52. /package/src/{internal.d.ts → internal.ts} +0 -0
  53. /package/src/lib/client/content/builders/query/lucene-syntax/{index.d.ts → index.ts} +0 -0
  54. /package/src/lib/utils/{index.d.ts → index.ts} +0 -0
@@ -0,0 +1,159 @@
1
+ import { QueryBuilder } from './query';
2
+
3
+ describe('QueryBuilder', () => {
4
+ let queryBuilder: QueryBuilder;
5
+
6
+ beforeEach(() => {
7
+ queryBuilder = new QueryBuilder();
8
+ });
9
+
10
+ it('should return a query with a simple term', () => {
11
+ const queryForBlogs = queryBuilder.field('contentType').equals('Blog').build();
12
+
13
+ expect(queryForBlogs).toBe('+contentType:Blog');
14
+ });
15
+
16
+ it('should return a query with multiple fields with a simple term ', () => {
17
+ const queryForBlogsInSuperCoolSite = queryBuilder
18
+ .field('contentType')
19
+ .equals('Blog')
20
+ .field('conhost')
21
+ .equals('my-super-cool-site')
22
+ .build();
23
+
24
+ expect(queryForBlogsInSuperCoolSite).toBe('+contentType:Blog +conhost:my-super-cool-site');
25
+ });
26
+
27
+ it('should return a query with an "OR" operand', () => {
28
+ const queryForBlogsOrArticles = queryBuilder
29
+ .field('contentType')
30
+ .equals('Blog')
31
+ .or()
32
+ .equals('Article')
33
+ .build();
34
+
35
+ expect(queryForBlogsOrArticles).toBe('+contentType:Blog OR Article');
36
+ });
37
+
38
+ it('should return a query with an "AND" operand', () => {
39
+ const queryForBlogsAndArticles = queryBuilder
40
+ .field('contentType')
41
+ .equals('Blog')
42
+ .and()
43
+ .equals('Article')
44
+ .build();
45
+
46
+ expect(queryForBlogsAndArticles).toBe('+contentType:Blog AND Article');
47
+ });
48
+
49
+ it('should return a query with a "NOT" operand', () => {
50
+ const queryForSkiingTripsNotInSwissAlps = queryBuilder
51
+ .field('summary')
52
+ .equals('Skiing trip')
53
+ .not()
54
+ .equals('Swiss Alps')
55
+ .build();
56
+
57
+ expect(queryForSkiingTripsNotInSwissAlps).toBe(`+summary:'Skiing trip' NOT 'Swiss Alps'`);
58
+ });
59
+
60
+ it('should return a query with an exclusion field', () => {
61
+ const queryForFootballBlogsWithoutMessi = queryBuilder
62
+ .field('contentType')
63
+ .equals('Blog')
64
+ .field('title')
65
+ .equals('Football')
66
+ .excludeField('summary')
67
+ .equals('Lionel Messi')
68
+ .build();
69
+
70
+ expect(queryForFootballBlogsWithoutMessi).toBe(
71
+ `+contentType:Blog +title:Football -summary:'Lionel Messi'`
72
+ );
73
+ });
74
+
75
+ it('should build a raw query from the query builder', () => {
76
+ const queryForBlogs = queryBuilder
77
+ .raw('+summary:Snowboard')
78
+ .not()
79
+ .equals('Swiss Alps')
80
+ .field('contentType')
81
+ .equals('Blog')
82
+ .build();
83
+
84
+ expect(queryForBlogs).toBe(`+summary:Snowboard NOT 'Swiss Alps' +contentType:Blog`);
85
+ });
86
+
87
+ it('should return a query with a raw query appended', () => {
88
+ const queryForBlogs = queryBuilder
89
+ .field('contentType')
90
+ .equals('Blog')
91
+ .raw('+summary:Snowboard')
92
+ .not()
93
+ .equals('Swiss Alps')
94
+ .build();
95
+
96
+ expect(queryForBlogs).toBe(`+contentType:Blog +summary:Snowboard NOT 'Swiss Alps'`);
97
+ });
98
+
99
+ it('should return a query with a raw query created with a queryBuilder appended and a term', () => {
100
+ const anotherQueryBuilder = new QueryBuilder();
101
+
102
+ const snowboardInCanada = anotherQueryBuilder
103
+ .field('summary')
104
+ .equals('Snowboard')
105
+ .field('country')
106
+ .equals('Canada')
107
+ .build();
108
+
109
+ const queryForBlogs = queryBuilder
110
+ .field('contentType')
111
+ .equals('Blog')
112
+ .raw(snowboardInCanada)
113
+ .build();
114
+
115
+ expect(queryForBlogs).toBe('+contentType:Blog +summary:Snowboard +country:Canada');
116
+ });
117
+
118
+ it('should return a query with all possible combinations', () => {
119
+ const blogOrActivity = queryBuilder
120
+ .field('contentType')
121
+ .equals('Blog')
122
+ .or()
123
+ .equals('Activity');
124
+
125
+ const customIdSiteOrCoolSite = blogOrActivity
126
+ .field('conhost')
127
+ .equals('48190c8c-42c4-46af-8d1a-0cd5db894797')
128
+ .or()
129
+ .equals('cool-site');
130
+
131
+ const englishAndSpanish = customIdSiteOrCoolSite
132
+ .field('languageId')
133
+ .equals('1')
134
+ .and()
135
+ .equals('2');
136
+
137
+ const notDeleted = englishAndSpanish.field('deleted').equals('false');
138
+
139
+ const currentlyWorking = notDeleted.field('working').equals('true');
140
+
141
+ const defaultVariant = currentlyWorking.field('variant').equals('default');
142
+
143
+ const snowboardOutsideSwissAlps = defaultVariant
144
+ .field('title')
145
+ .equals('Snowboard')
146
+ .excludeField('summary')
147
+ .equals('Swiss Alps');
148
+
149
+ const writtenByJohnDoe = snowboardOutsideSwissAlps.field('authors').equals('John Doe');
150
+
151
+ const withoutJaneDoeHelp = writtenByJohnDoe.not().equals('Jane Doe');
152
+
153
+ const query = withoutJaneDoeHelp.build();
154
+
155
+ expect(query).toBe(
156
+ `+contentType:Blog OR Activity +conhost:48190c8c-42c4-46af-8d1a-0cd5db894797 OR cool-site +languageId:1 AND 2 +deleted:false +working:true +variant:default +title:Snowboard -summary:'Swiss Alps' +authors:'John Doe' NOT 'Jane Doe'`
157
+ );
158
+ });
159
+ });
@@ -1,4 +1,6 @@
1
1
  import { Equals, Field } from './lucene-syntax/index';
2
+ import { buildExcludeField, buildField, buildRawEquals } from './utils';
3
+
2
4
  /**
3
5
  * 'QueryBuilder' Is a Typescript class that provides the ability to build a query string using the Lucene syntax in a more readable way.
4
6
  * @example
@@ -27,8 +29,9 @@ import { Equals, Field } from './lucene-syntax/index';
27
29
  * @export
28
30
  * @class QueryBuilder
29
31
  */
30
- export declare class QueryBuilder {
31
- #private;
32
+ export class QueryBuilder {
33
+ #query = '';
34
+
32
35
  /**
33
36
  * This method appends to the query a field that should be included in the search.
34
37
  *
@@ -42,7 +45,10 @@ export declare class QueryBuilder {
42
45
  * @return {*} {Field} - An instance of a Lucene Field. A field is a key used to search for a specific value in a document.
43
46
  * @memberof QueryBuilder
44
47
  */
45
- field(field: string): Field;
48
+ field(field: string): Field {
49
+ return buildField(this.#query, field);
50
+ }
51
+
46
52
  /**
47
53
  * This method appends to the query a field that should be excluded from the search.
48
54
  *
@@ -56,7 +62,10 @@ export declare class QueryBuilder {
56
62
  * @return {*} {Field} - An instance of a Lucene Exclude Field. An exclude field is a key used to exclude for a specific value in a document.
57
63
  * @memberof QueryBuilder
58
64
  */
59
- excludeField(field: string): Field;
65
+ excludeField(field: string): Field {
66
+ return buildExcludeField(this.#query, field);
67
+ }
68
+
60
69
  /**
61
70
  * This method allows to pass a raw query string to the query builder.
62
71
  * This raw query should end in Equals.
@@ -72,5 +81,7 @@ export declare class QueryBuilder {
72
81
  * @return {*} {Equals} - An instance of Equals. A term is a value used to search for a specific value in a document.
73
82
  * @memberof QueryBuilder
74
83
  */
75
- raw(query: string): Equals;
84
+ raw(query: string): Equals {
85
+ return buildRawEquals(this.#query, query);
86
+ }
76
87
  }
@@ -2,17 +2,19 @@ import { Equals } from '../lucene-syntax/Equals';
2
2
  import { Field } from '../lucene-syntax/Field';
3
3
  import { NotOperand } from '../lucene-syntax/NotOperand';
4
4
  import { Operand } from '../lucene-syntax/Operand';
5
+
5
6
  /**
6
7
  * Enum for common Operands
7
8
  *
8
9
  * @export
9
10
  * @enum {number}
10
11
  */
11
- export declare enum OPERAND {
12
- OR = "OR",
13
- AND = "AND",
14
- NOT = "NOT"
12
+ export enum OPERAND {
13
+ OR = 'OR',
14
+ AND = 'AND',
15
+ NOT = 'NOT'
15
16
  }
17
+
16
18
  /**
17
19
  * This function removes extra spaces from a string.
18
20
  *
@@ -25,7 +27,10 @@ export declare enum OPERAND {
25
27
  * @param {string} str
26
28
  * @return {*} {string}
27
29
  */
28
- export declare function sanitizeQuery(str: string): string;
30
+ export function sanitizeQuery(str: string): string {
31
+ return str.replace(/\s{2,}/g, ' ').trim();
32
+ }
33
+
29
34
  /**
30
35
  * This function sanitizes a term by adding quotes if it contains spaces.
31
36
  * In lucene, a term with spaces should be enclosed in quotes.
@@ -40,7 +45,10 @@ export declare function sanitizeQuery(str: string): string;
40
45
  * @param {string} term
41
46
  * @return {*} {string}
42
47
  */
43
- export declare function sanitizePhrases(term: string): string;
48
+ export function sanitizePhrases(term: string): string {
49
+ return term.includes(' ') ? `'${term}'` : term;
50
+ }
51
+
44
52
  /**
45
53
  * This function builds a term to be used in a lucene query.
46
54
  * We need to sanitize the term before adding it to the query.
@@ -55,7 +63,12 @@ export declare function sanitizePhrases(term: string): string;
55
63
  * @param {string} term
56
64
  * @return {*} {Equals}
57
65
  */
58
- export declare function buildEquals(query: string, term: string): Equals;
66
+ export function buildEquals(query: string, term: string): Equals {
67
+ const newQuery = query + sanitizePhrases(term);
68
+
69
+ return new Equals(newQuery);
70
+ }
71
+
59
72
  /**
60
73
  * This function builds a term to be used in a lucene query.
61
74
  * We need to sanitize the raw query before adding it to the query.
@@ -71,7 +84,12 @@ export declare function buildEquals(query: string, term: string): Equals;
71
84
  * @param {string} raw
72
85
  * @return {*} {Equals}
73
86
  */
74
- export declare function buildRawEquals(query: string, raw: string): Equals;
87
+ export function buildRawEquals(query: string, raw: string): Equals {
88
+ const newQuery = query + ` ${raw}`;
89
+
90
+ return new Equals(sanitizeQuery(newQuery));
91
+ }
92
+
75
93
  /**
76
94
  * This function builds a field to be used in a lucene query.
77
95
  * We need to format the field before adding it to the query.
@@ -86,7 +104,12 @@ export declare function buildRawEquals(query: string, raw: string): Equals;
86
104
  * @param {string} field
87
105
  * @return {*} {Field}
88
106
  */
89
- export declare function buildField(query: string, field: string): Field;
107
+ export function buildField(query: string, field: string): Field {
108
+ const newQuery = query + ` +${field}:`;
109
+
110
+ return new Field(newQuery);
111
+ }
112
+
90
113
  /**
91
114
  * This function builds an exclude field to be used in a lucene query.
92
115
  * We need to format the field before adding it to the query.
@@ -102,7 +125,12 @@ export declare function buildField(query: string, field: string): Field;
102
125
  * @param {string} field
103
126
  * @return {*} {Field}
104
127
  */
105
- export declare function buildExcludeField(query: string, field: string): Field;
128
+ export function buildExcludeField(query: string, field: string): Field {
129
+ const newQuery = query + ` -${field}:`;
130
+
131
+ return new Field(newQuery);
132
+ }
133
+
106
134
  /**
107
135
  * This function builds an operand to be used in a lucene query.
108
136
  * We need to format the operand before adding it to the query.
@@ -124,7 +152,12 @@ export declare function buildExcludeField(query: string, field: string): Field;
124
152
  * @param {OPERAND} operand
125
153
  * @return {*} {Operand}
126
154
  */
127
- export declare function buildOperand(query: string, operand: OPERAND): Operand;
155
+ export function buildOperand(query: string, operand: OPERAND): Operand {
156
+ const newQuery = query + ` ${operand} `;
157
+
158
+ return new Operand(newQuery);
159
+ }
160
+
128
161
  /**
129
162
  * This function builds a NOT operand to be used in a lucene query.
130
163
  * We need to format the operand before adding it to the query.
@@ -139,4 +172,8 @@ export declare function buildOperand(query: string, operand: OPERAND): Operand;
139
172
  * @param {string} query
140
173
  * @return {*} {NotOperand}
141
174
  */
142
- export declare function buildNotOperand(query: string): NotOperand;
175
+ export function buildNotOperand(query: string): NotOperand {
176
+ const newQuery = query + ` ${OPERAND.NOT} `;
177
+
178
+ return new NotOperand(newQuery);
179
+ }
@@ -1,5 +1,7 @@
1
1
  import { RequestOptions } from '@dotcms/types';
2
+
2
3
  import { CollectionBuilder } from './builders/collection/collection';
4
+
3
5
  /**
4
6
  * Creates a builder to filter and fetch a collection of content items.
5
7
  * @param contentType - The content type to retrieve.
@@ -49,14 +51,20 @@ import { CollectionBuilder } from './builders/collection/collection';
49
51
  * });
50
52
  * ```
51
53
  */
52
- export declare class Content {
53
- #private;
54
+ export class Content {
55
+ #requestOptions: RequestOptions;
56
+ #serverUrl: string;
57
+
54
58
  /**
55
59
  * Creates an instance of Content.
56
60
  * @param {RequestOptions} requestOptions - The options for the client request.
57
61
  * @param {string} serverUrl - The server URL.
58
62
  */
59
- constructor(requestOptions: RequestOptions, serverUrl: string);
63
+ constructor(requestOptions: RequestOptions, serverUrl: string) {
64
+ this.#requestOptions = requestOptions;
65
+ this.#serverUrl = serverUrl;
66
+ }
67
+
60
68
  /**
61
69
  * Takes a content type and returns a builder to filter and fetch the collection.
62
70
  * @param {string} contentType - The content type to get the collection.
@@ -122,5 +130,7 @@ export declare class Content {
122
130
  * ```
123
131
  *
124
132
  */
125
- getCollection<T = unknown>(contentType: string): CollectionBuilder<T>;
133
+ getCollection<T = unknown>(contentType: string): CollectionBuilder<T> {
134
+ return new CollectionBuilder<T>(this.#requestOptions, this.#serverUrl, contentType);
135
+ }
126
136
  }
@@ -1,13 +1,15 @@
1
1
  /**
2
2
  * Default variant identifier used in the application.
3
3
  */
4
- export declare const DEFAULT_VARIANT_ID = "DEFAULT";
4
+ export const DEFAULT_VARIANT_ID = 'DEFAULT';
5
+
5
6
  /**
6
7
  * Fields that should not be formatted when sanitizing the query.
7
8
  * These fields are essential for maintaining the integrity of the content type.
8
9
  */
9
- export declare const CONTENT_TYPE_MAIN_FIELDS: string[];
10
+ export const CONTENT_TYPE_MAIN_FIELDS: string[] = ['live', 'variant', 'contentType', 'languageId'];
11
+
10
12
  /**
11
13
  * URL endpoint for the content API search functionality.
12
14
  */
13
- export declare const CONTENT_API_URL = "/api/content/_search";
15
+ export const CONTENT_API_URL = '/api/content/_search';
@@ -1,6 +1,8 @@
1
1
  import { Contentlet } from '@dotcms/types';
2
+
2
3
  import { Equals } from '../builders/query/lucene-syntax';
3
4
  import { QueryBuilder } from '../builders/query/query';
5
+
4
6
  /**
5
7
  * Model to sort by fields.
6
8
  */
@@ -14,6 +16,7 @@ export type SortBy = {
14
16
  */
15
17
  order: 'asc' | 'desc';
16
18
  };
19
+
17
20
  /**
18
21
  * Callback to build a query.
19
22
  *
@@ -22,6 +25,7 @@ export type SortBy = {
22
25
  * @returns {Equals} The built query.
23
26
  */
24
27
  export type BuildQuery = (qb: QueryBuilder) => Equals;
28
+
25
29
  /**
26
30
  * Callback for a fulfilled promise.
27
31
  *
@@ -30,7 +34,13 @@ export type BuildQuery = (qb: QueryBuilder) => Equals;
30
34
  * @param {GetCollectionResponse<T>} value - The response value.
31
35
  * @returns {GetCollectionResponse<T> | PromiseLike<GetCollectionResponse<T>> | void} The processed response or a promise.
32
36
  */
33
- export type OnFullfilled<T> = ((value: GetCollectionResponse<T>) => GetCollectionResponse<T> | PromiseLike<GetCollectionResponse<T>> | void) | undefined | null;
37
+ export type OnFullfilled<T> =
38
+ | ((
39
+ value: GetCollectionResponse<T>
40
+ ) => GetCollectionResponse<T> | PromiseLike<GetCollectionResponse<T>> | void)
41
+ | undefined
42
+ | null;
43
+
34
44
  /**
35
45
  * Callback for a rejected promise.
36
46
  *
@@ -38,7 +48,11 @@ export type OnFullfilled<T> = ((value: GetCollectionResponse<T>) => GetCollectio
38
48
  * @param {GetCollectionError} error - The error object.
39
49
  * @returns {GetCollectionError | PromiseLike<GetCollectionError> | void} The processed error or a promise.
40
50
  */
41
- export type OnRejected = ((error: GetCollectionError) => GetCollectionError | PromiseLike<GetCollectionError> | void) | undefined | null;
51
+ export type OnRejected =
52
+ | ((error: GetCollectionError) => GetCollectionError | PromiseLike<GetCollectionError> | void)
53
+ | undefined
54
+ | null;
55
+
42
56
  /**
43
57
  * Response of the get collection method.
44
58
  *
@@ -66,6 +80,7 @@ export interface GetCollectionResponse<T> {
66
80
  */
67
81
  sortedBy?: SortBy[];
68
82
  }
83
+
69
84
  /**
70
85
  * Raw response of the get collection method.
71
86
  *
@@ -85,6 +100,7 @@ export interface GetCollectionRawResponse<T> {
85
100
  resultsSize: number;
86
101
  };
87
102
  }
103
+
88
104
  /**
89
105
  * Error object for the get collection method.
90
106
  */
@@ -1,3 +1,5 @@
1
+ import { CONTENT_TYPE_MAIN_FIELDS } from './const';
2
+
1
3
  /**
2
4
  * @description
3
5
  * Sanitizes the query for the given content type.
@@ -17,4 +19,10 @@
17
19
  * @param {string} contentType - The content type to be used for formatting the fields.
18
20
  * @returns {string} The sanitized query string.
19
21
  */
20
- export declare function sanitizeQueryForContentType(query: string, contentType: string): string;
22
+ export function sanitizeQueryForContentType(query: string, contentType: string): string {
23
+ return query.replace(/\+([^+:]*?):/g, (original, field) => {
24
+ return !CONTENT_TYPE_MAIN_FIELDS.includes(field) // Fields that are not content type fields
25
+ ? `+${contentType}.${field}:` // Should have this format: +contentTypeVar.field:
26
+ : original; // Return the field if it is a content type field
27
+ });
28
+ }
@@ -9,4 +9,11 @@
9
9
  * @property {string} 502 - Bad Gateway. Try again later.
10
10
  * @property {string} 503 - Service Unavailable. Try again later.
11
11
  */
12
- export declare const ErrorMessages: Record<number, string>;
12
+ export const ErrorMessages: Record<number, string> = {
13
+ 401: 'Unauthorized. Check the token and try again.',
14
+ 403: 'Forbidden. Check the permissions and try again.',
15
+ 404: 'Not Found. Check the URL and try again.',
16
+ 500: 'Internal Server Error. Try again later.',
17
+ 502: 'Bad Gateway. Try again later.',
18
+ 503: 'Service Unavailable. Try again later.'
19
+ };
@@ -0,0 +1,167 @@
1
+ import { DotCMSClientConfig, RequestOptions } from '@dotcms/types';
2
+
3
+ import { NavigationClient } from './navigation-api';
4
+
5
+ describe('NavigationClient', () => {
6
+ const mockFetch = jest.fn();
7
+ const originalFetch = global.fetch;
8
+
9
+ const validConfig: DotCMSClientConfig = {
10
+ dotcmsUrl: 'https://demo.dotcms.com',
11
+ authToken: 'test-token',
12
+ siteId: 'test-site'
13
+ };
14
+
15
+ const requestOptions: RequestOptions = {
16
+ headers: {
17
+ Authorization: 'Bearer test-token'
18
+ }
19
+ };
20
+
21
+ const mockNavigationData = {
22
+ entity: {
23
+ title: 'Main Navigation',
24
+ items: [
25
+ { label: 'Home', url: '/' },
26
+ { label: 'About', url: '/about' },
27
+ { label: 'Contact', url: '/contact' }
28
+ ]
29
+ }
30
+ };
31
+
32
+ beforeEach(() => {
33
+ mockFetch.mockReset();
34
+ global.fetch = mockFetch;
35
+ mockFetch.mockResolvedValue({
36
+ ok: true,
37
+ json: async () => mockNavigationData
38
+ });
39
+ });
40
+
41
+ afterAll(() => {
42
+ global.fetch = originalFetch;
43
+ });
44
+
45
+ it('should fetch navigation successfully', async () => {
46
+ const navClient = new NavigationClient(validConfig, requestOptions);
47
+ const result = await navClient.get('/');
48
+
49
+ expect(mockFetch).toHaveBeenCalledWith(
50
+ 'https://demo.dotcms.com/api/v1/nav/',
51
+ requestOptions
52
+ );
53
+
54
+ expect(result).toEqual(mockNavigationData.entity);
55
+ });
56
+
57
+ it('should fetch navigation with custom path', async () => {
58
+ const navClient = new NavigationClient(validConfig, requestOptions);
59
+ const path = '/products';
60
+
61
+ await navClient.get(path);
62
+
63
+ expect(mockFetch).toHaveBeenCalledWith(
64
+ 'https://demo.dotcms.com/api/v1/nav/products',
65
+ expect.anything()
66
+ );
67
+ });
68
+
69
+ it('should fetch navigation with custom depth', async () => {
70
+ const navClient = new NavigationClient(validConfig, requestOptions);
71
+ const depth = 3;
72
+
73
+ await navClient.get('/', { depth });
74
+
75
+ expect(mockFetch).toHaveBeenCalledWith(
76
+ 'https://demo.dotcms.com/api/v1/nav/?depth=3',
77
+ expect.anything()
78
+ );
79
+ });
80
+
81
+ it('should normalize path by removing leading slash', async () => {
82
+ const navClient = new NavigationClient(validConfig, requestOptions);
83
+
84
+ await navClient.get('/about/');
85
+
86
+ expect(mockFetch).toHaveBeenCalledWith(
87
+ 'https://demo.dotcms.com/api/v1/nav/about/',
88
+ expect.anything()
89
+ );
90
+ });
91
+
92
+ it('should handle root path correctly', async () => {
93
+ const navClient = new NavigationClient(validConfig, requestOptions);
94
+
95
+ await navClient.get('/');
96
+
97
+ expect(mockFetch).toHaveBeenCalledWith(
98
+ 'https://demo.dotcms.com/api/v1/nav/',
99
+ expect.anything()
100
+ );
101
+ });
102
+
103
+ it('should handle fetch errors', async () => {
104
+ mockFetch.mockRejectedValue(new Error('Network error'));
105
+
106
+ const navClient = new NavigationClient(validConfig, requestOptions);
107
+
108
+ await expect(navClient.get('/')).rejects.toThrow('Network error');
109
+ });
110
+
111
+ it('should handle non-OK responses', async () => {
112
+ mockFetch.mockResolvedValue({
113
+ ok: false,
114
+ status: 404,
115
+ statusText: 'Not Found'
116
+ });
117
+
118
+ const navClient = new NavigationClient(validConfig, requestOptions);
119
+
120
+ await expect(navClient.get('/')).rejects.toThrow(
121
+ `Failed to fetch navigation data: Not Found - 404`
122
+ );
123
+ });
124
+
125
+ it('should include authorization headers in request', async () => {
126
+ const navClient = new NavigationClient(validConfig, requestOptions);
127
+
128
+ await navClient.get('/');
129
+
130
+ expect(mockFetch).toHaveBeenCalledWith(
131
+ expect.any(String),
132
+ expect.objectContaining({
133
+ headers: expect.objectContaining({
134
+ Authorization: 'Bearer test-token'
135
+ })
136
+ })
137
+ );
138
+ });
139
+
140
+ it('should merge additional request options', async () => {
141
+ const optionsWithCache: RequestOptions = {
142
+ ...requestOptions,
143
+ cache: 'no-cache',
144
+ credentials: 'include'
145
+ };
146
+
147
+ const navClient = new NavigationClient(validConfig, optionsWithCache);
148
+
149
+ await navClient.get('/');
150
+
151
+ expect(mockFetch).toHaveBeenCalledWith(
152
+ 'https://demo.dotcms.com/api/v1/nav/',
153
+ optionsWithCache
154
+ );
155
+ });
156
+
157
+ it('should fetch navigation with multiple options', async () => {
158
+ const navClient = new NavigationClient(validConfig, requestOptions);
159
+
160
+ await navClient.get('/', { depth: 3, languageId: 2 });
161
+
162
+ expect(mockFetch).toHaveBeenCalledWith(
163
+ 'https://demo.dotcms.com/api/v1/nav/?depth=3&language_id=2',
164
+ requestOptions
165
+ );
166
+ });
167
+ });