@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,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
|
|
31
|
-
#
|
|
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
|
|
12
|
-
OR =
|
|
13
|
-
AND =
|
|
14
|
-
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
53
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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> =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
+
});
|