@cntrl-site/sdk 0.2.14 → 0.3.0
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/lib/Client/Client.js
CHANGED
|
@@ -20,35 +20,36 @@ class Client {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
async getProject() {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
try {
|
|
24
|
+
const response = await this.fetchProject();
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
const project = core_1.ProjectSchema.parse(data);
|
|
27
|
+
return project;
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
throw e;
|
|
27
31
|
}
|
|
28
|
-
const data = await response.json();
|
|
29
|
-
const project = core_1.ProjectSchema.parse(data);
|
|
30
|
-
return project;
|
|
31
32
|
}
|
|
32
33
|
async getPageArticle(pageSlug) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
throw new Error(`Page with a slug ${pageSlug} was not found in project with id #${this.projectId}`);
|
|
34
|
+
try {
|
|
35
|
+
const projectResponse = await this.fetchProject();
|
|
36
|
+
const data = await projectResponse.json();
|
|
37
|
+
const project = core_1.ProjectSchema.parse(data);
|
|
38
|
+
const articleId = this.findArticleIdByPageSlug(pageSlug, project.pages);
|
|
39
|
+
const articleResponse = await this.fetchArticle(articleId);
|
|
40
|
+
const articleData = await articleResponse.json();
|
|
41
|
+
const article = core_1.ArticleSchema.parse(articleData);
|
|
42
|
+
return article;
|
|
43
43
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!articleResponse.ok) {
|
|
47
|
-
throw new Error(`Failed to fetch article with id #${page.articleId}: ${articleResponse.statusText}`);
|
|
44
|
+
catch (e) {
|
|
45
|
+
throw e;
|
|
48
46
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
}
|
|
48
|
+
async getTypePresets() {
|
|
49
|
+
const response = await this.fetchTypePresets();
|
|
50
|
+
const data = await response.json();
|
|
51
|
+
const typePresets = core_1.TypePresetsSchema.parse(data);
|
|
52
|
+
return typePresets;
|
|
52
53
|
}
|
|
53
54
|
static getPageMeta(projectMeta, pageMeta) {
|
|
54
55
|
return pageMeta.enabled ? {
|
|
@@ -59,5 +60,36 @@ class Client {
|
|
|
59
60
|
favicon: projectMeta.favicon
|
|
60
61
|
} : projectMeta;
|
|
61
62
|
}
|
|
63
|
+
async fetchProject() {
|
|
64
|
+
const url = new url_1.URL(`/projects/${this.projectId}`, this.APIUrl);
|
|
65
|
+
const response = await this.fetchImpl(url.href);
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new Error(`Failed to fetch project with id #${this.projectId}: ${response.statusText}`);
|
|
68
|
+
}
|
|
69
|
+
return response;
|
|
70
|
+
}
|
|
71
|
+
async fetchArticle(articleId) {
|
|
72
|
+
const url = new url_1.URL(`/articles/${articleId}`, this.APIUrl);
|
|
73
|
+
const response = await this.fetchImpl(url.href);
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Failed to fetch article with id #${articleId}: ${response.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
async fetchTypePresets() {
|
|
80
|
+
const url = new url_1.URL(`/projects/${this.projectId}/type-presets`, this.APIUrl);
|
|
81
|
+
const response = await this.fetchImpl(url.href);
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`Failed to fetch type presets for the project with id #${this.projectId}: ${response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
return response;
|
|
86
|
+
}
|
|
87
|
+
findArticleIdByPageSlug(slug, pages) {
|
|
88
|
+
const page = pages.find((page) => page.slug === slug);
|
|
89
|
+
if (!page) {
|
|
90
|
+
throw new Error(`Page with a slug ${slug} was not found in project with id #${this.projectId}`);
|
|
91
|
+
}
|
|
92
|
+
return page.articleId;
|
|
93
|
+
}
|
|
62
94
|
}
|
|
63
95
|
exports.Client = Client;
|
package/package.json
CHANGED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { Client } from './Client';
|
|
2
|
+
import { projectMock } from './__mock__/projectMock';
|
|
3
|
+
import { articleMock } from './__mock__/articleMock';
|
|
4
|
+
import { typePresetsMock } from './__mock__/typePresetsMock';
|
|
5
|
+
import { TMeta, TPageMeta } from '@cntrl-site/core';
|
|
6
|
+
|
|
7
|
+
describe('Client', () => {
|
|
8
|
+
it('returns project', async () => {
|
|
9
|
+
let fetchCalledTimes = 0;
|
|
10
|
+
const projectId = 'projectId';
|
|
11
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
12
|
+
const fetch = async (url: string) => {
|
|
13
|
+
fetchCalledTimes += 1;
|
|
14
|
+
expect(url).toBe(`${apiUrl}projects/${projectId}`);
|
|
15
|
+
return Promise.resolve({
|
|
16
|
+
ok: true,
|
|
17
|
+
json: () => Promise.resolve(projectMock),
|
|
18
|
+
statusText: ''
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
22
|
+
const project = await client.getProject();
|
|
23
|
+
expect(fetchCalledTimes).toBe(1);
|
|
24
|
+
expect(project).toEqual(projectMock);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('throws an error upon project fetch failure', async () => {
|
|
28
|
+
const projectId = 'projectId';
|
|
29
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
30
|
+
const fetch = async () => Promise.resolve({
|
|
31
|
+
ok: false,
|
|
32
|
+
statusText: 'reason',
|
|
33
|
+
json: () => Promise.resolve()
|
|
34
|
+
});
|
|
35
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
36
|
+
await expect(client.getProject()).rejects.toEqual(new Error('Failed to fetch project with id #projectId: reason'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns article by page slug', async () => {
|
|
40
|
+
let fetchCalledTimes = 0;
|
|
41
|
+
const projectId = 'projectId';
|
|
42
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
43
|
+
const projectApiUrl = `${apiUrl}projects/projectId`;
|
|
44
|
+
const articleApiUrl = `${apiUrl}articles/articleId`;
|
|
45
|
+
const fetch = async (url: string) => {
|
|
46
|
+
fetchCalledTimes += 1;
|
|
47
|
+
if (fetchCalledTimes === 1) {
|
|
48
|
+
expect(url).toBe(projectApiUrl);
|
|
49
|
+
}
|
|
50
|
+
if (fetchCalledTimes === 2) {
|
|
51
|
+
expect(url).toBe(articleApiUrl);
|
|
52
|
+
}
|
|
53
|
+
return Promise.resolve({
|
|
54
|
+
ok: true,
|
|
55
|
+
json: () => Promise.resolve(url === projectApiUrl ? projectMock : articleMock),
|
|
56
|
+
statusText: ''
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
60
|
+
const article = await client.getPageArticle('/');
|
|
61
|
+
expect(fetchCalledTimes).toBe(2);
|
|
62
|
+
expect(article).toEqual(articleMock);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('throws an error upon project fetch failure when trying to get article by slug', async () => {
|
|
66
|
+
const projectId = 'projectId';
|
|
67
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
68
|
+
const fetch = async () => Promise.resolve({
|
|
69
|
+
ok: false,
|
|
70
|
+
statusText: 'reason',
|
|
71
|
+
json: () => Promise.resolve()
|
|
72
|
+
});
|
|
73
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
74
|
+
await expect(client.getPageArticle('/'))
|
|
75
|
+
.rejects.toEqual(new Error('Failed to fetch project with id #projectId: reason'));
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('throws an error upon article fetch failure when trying to get article by slug', async () => {
|
|
79
|
+
const projectId = 'projectId';
|
|
80
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
81
|
+
const projectApiUrl = `${apiUrl}projects/projectId`;
|
|
82
|
+
const fetch = (url: string) => {
|
|
83
|
+
return Promise.resolve({
|
|
84
|
+
ok: url === projectApiUrl,
|
|
85
|
+
json: () => Promise.resolve(projectMock),
|
|
86
|
+
statusText: 'reason'
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
90
|
+
await expect(client.getPageArticle('/'))
|
|
91
|
+
.rejects.toEqual(new Error('Failed to fetch article with id #articleId: reason'));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('throws an error when trying to fetch article by nonexistent slug', async () => {
|
|
95
|
+
const projectId = 'projectId';
|
|
96
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
97
|
+
const projectApiUrl = `${apiUrl}projects/projectId`;
|
|
98
|
+
const slug = '/nonexistent-slug';
|
|
99
|
+
const fetch = (url: string) => {
|
|
100
|
+
return Promise.resolve({
|
|
101
|
+
ok: url === projectApiUrl,
|
|
102
|
+
json: () => Promise.resolve(projectMock),
|
|
103
|
+
statusText: 'reason'
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
107
|
+
await expect(client.getPageArticle(slug))
|
|
108
|
+
.rejects.toEqual(new Error(`Page with a slug ${slug} was not found in project with id #${projectId}`));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('returns type presets by project id', async () => {
|
|
112
|
+
let fetchCalledTimes = 0;
|
|
113
|
+
const projectId = 'projectId';
|
|
114
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
115
|
+
const fetch = (url: string) => {
|
|
116
|
+
fetchCalledTimes += 1;
|
|
117
|
+
expect(url).toBe(`${apiUrl}projects/${projectId}/type-presets`);
|
|
118
|
+
return Promise.resolve({
|
|
119
|
+
ok: true,
|
|
120
|
+
json: () => Promise.resolve(typePresetsMock),
|
|
121
|
+
statusText: ''
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
125
|
+
const presets = await client.getTypePresets();
|
|
126
|
+
expect(presets).toEqual(typePresetsMock);
|
|
127
|
+
expect(fetchCalledTimes).toEqual(1);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('throws an error upon type presets fetch failure', async () => {
|
|
131
|
+
const projectId = 'projectId';
|
|
132
|
+
const apiUrl = 'https://api.cntrl.site/';
|
|
133
|
+
const fetch = () => {
|
|
134
|
+
return Promise.resolve({
|
|
135
|
+
ok: false,
|
|
136
|
+
json: () => Promise.resolve(),
|
|
137
|
+
statusText: 'reason'
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const client = new Client(projectId, apiUrl, fetch);
|
|
141
|
+
await expect(client.getTypePresets()).rejects.toEqual(
|
|
142
|
+
new Error(`Failed to fetch type presets for the project with id #${projectId}: reason`)
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('merges two meta objects into one with priority of page-based over project-based', () => {
|
|
147
|
+
const pageMeta: TPageMeta = {
|
|
148
|
+
enabled: true,
|
|
149
|
+
description: 'page-desc',
|
|
150
|
+
title: 'page-title'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const projectMeta: TMeta = {
|
|
154
|
+
opengraphThumbnail: 'proj-og',
|
|
155
|
+
description: 'proj-desc',
|
|
156
|
+
title: 'proj-title',
|
|
157
|
+
keywords: 'project, keywords'
|
|
158
|
+
};
|
|
159
|
+
const meta = Client.getPageMeta(projectMeta, pageMeta);
|
|
160
|
+
expect(meta.keywords).toBe(projectMeta.keywords);
|
|
161
|
+
expect(meta.opengraphThumbnail).toBe(projectMeta.opengraphThumbnail);
|
|
162
|
+
expect(meta.description).toBe(pageMeta.description);
|
|
163
|
+
expect(meta.title).toBe(pageMeta.title);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('ignores page meta when `enabled` is set to `false` and uses only generic project meta', () => {
|
|
167
|
+
const pageMeta: TPageMeta = {
|
|
168
|
+
enabled: false,
|
|
169
|
+
description: 'page-desc',
|
|
170
|
+
title: 'page-title'
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const projectMeta: TMeta = {
|
|
174
|
+
opengraphThumbnail: 'proj-og',
|
|
175
|
+
keywords: 'project, keywords'
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const meta = Client.getPageMeta(projectMeta, pageMeta);
|
|
179
|
+
expect(meta.keywords).toBe(projectMeta.keywords);
|
|
180
|
+
expect(meta.opengraphThumbnail).toBe(projectMeta.opengraphThumbnail);
|
|
181
|
+
expect(meta.description).toBeUndefined();
|
|
182
|
+
expect(meta.title).toBeUndefined();
|
|
183
|
+
});
|
|
184
|
+
});
|
package/src/Client/Client.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
TProject,
|
|
3
|
+
ProjectSchema,
|
|
4
|
+
TArticle,
|
|
5
|
+
ArticleSchema,
|
|
6
|
+
TMeta,
|
|
7
|
+
TPageMeta,
|
|
8
|
+
TTypePresets,
|
|
9
|
+
TypePresetsSchema,
|
|
10
|
+
TPage
|
|
11
|
+
} from '@cntrl-site/core';
|
|
2
12
|
import fetch from 'isomorphic-fetch';
|
|
3
13
|
import { URL } from 'url';
|
|
4
14
|
|
|
@@ -6,7 +16,7 @@ export class Client {
|
|
|
6
16
|
constructor(
|
|
7
17
|
private projectId: string,
|
|
8
18
|
private APIUrl: string,
|
|
9
|
-
private fetchImpl = fetch
|
|
19
|
+
private fetchImpl: FetchImpl = fetch
|
|
10
20
|
) {
|
|
11
21
|
if (projectId.length === 0) {
|
|
12
22
|
throw new Error('CNTRL SDK: Project ID is empty. Did you forget to pass it?');
|
|
@@ -17,37 +27,36 @@ export class Client {
|
|
|
17
27
|
}
|
|
18
28
|
|
|
19
29
|
async getProject(): Promise<TProject> {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
30
|
+
try {
|
|
31
|
+
const response = await this.fetchProject();
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
const project = ProjectSchema.parse(data);
|
|
34
|
+
return project;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
throw e;
|
|
24
37
|
}
|
|
25
|
-
const data = await response.json();
|
|
26
|
-
const project = ProjectSchema.parse(data);
|
|
27
|
-
return project;
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
async getPageArticle(pageSlug: string): Promise<TArticle> {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
try {
|
|
42
|
+
const projectResponse = await this.fetchProject();
|
|
43
|
+
const data = await projectResponse.json();
|
|
44
|
+
const project = ProjectSchema.parse(data);
|
|
45
|
+
const articleId = this.findArticleIdByPageSlug(pageSlug, project.pages);
|
|
46
|
+
const articleResponse = await this.fetchArticle(articleId);
|
|
47
|
+
const articleData = await articleResponse.json();
|
|
48
|
+
const article = ArticleSchema.parse(articleData);
|
|
49
|
+
return article;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
throw e;
|
|
41
52
|
}
|
|
42
|
-
|
|
43
|
-
const articleResponse = await this.fetchImpl(url.href);
|
|
44
|
-
if (!articleResponse.ok) {
|
|
45
|
-
throw new Error(`Failed to fetch article with id #${page.articleId}: ${articleResponse.statusText}`);
|
|
46
|
-
}
|
|
47
|
-
const articleData = await articleResponse.json();
|
|
48
|
-
const article = ArticleSchema.parse(articleData);
|
|
53
|
+
}
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
async getTypePresets(): Promise<TTypePresets> {
|
|
56
|
+
const response = await this.fetchTypePresets();
|
|
57
|
+
const data = await response.json();
|
|
58
|
+
const typePresets = TypePresetsSchema.parse(data);
|
|
59
|
+
return typePresets;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
public static getPageMeta(projectMeta: TMeta, pageMeta: TPageMeta): TMeta {
|
|
@@ -59,4 +68,49 @@ export class Client {
|
|
|
59
68
|
favicon: projectMeta.favicon
|
|
60
69
|
} : projectMeta;
|
|
61
70
|
}
|
|
71
|
+
|
|
72
|
+
private async fetchProject(): Promise<FetchImplResponse> {
|
|
73
|
+
const url = new URL(`/projects/${this.projectId}`, this.APIUrl);
|
|
74
|
+
const response = await this.fetchImpl(url.href);
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`Failed to fetch project with id #${this.projectId}: ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
return response;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private async fetchArticle(articleId: string): Promise<FetchImplResponse> {
|
|
82
|
+
const url = new URL(`/articles/${articleId}`, this.APIUrl);
|
|
83
|
+
const response = await this.fetchImpl(url.href);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error(`Failed to fetch article with id #${articleId}: ${response.statusText}`);
|
|
86
|
+
}
|
|
87
|
+
return response;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async fetchTypePresets(): Promise<FetchImplResponse> {
|
|
91
|
+
const url = new URL(`/projects/${this.projectId}/type-presets`, this.APIUrl);
|
|
92
|
+
const response = await this.fetchImpl(url.href);
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Failed to fetch type presets for the project with id #${this.projectId}: ${response.statusText}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return response;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private findArticleIdByPageSlug(slug: string, pages: TPage[]): string {
|
|
102
|
+
const page = pages.find((page) => page.slug === slug);
|
|
103
|
+
if (!page) {
|
|
104
|
+
throw new Error(`Page with a slug ${slug} was not found in project with id #${this.projectId}`);
|
|
105
|
+
}
|
|
106
|
+
return page.articleId;
|
|
107
|
+
}
|
|
62
108
|
}
|
|
109
|
+
|
|
110
|
+
interface FetchImplResponse {
|
|
111
|
+
ok: boolean;
|
|
112
|
+
json(): Promise<any>;
|
|
113
|
+
statusText: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type FetchImpl = (url: string) => Promise<FetchImplResponse>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { TProject } from '@cntrl-site/core';
|
|
2
|
+
|
|
3
|
+
export const projectMock: TProject = {
|
|
4
|
+
id: 'projectId',
|
|
5
|
+
layouts: [],
|
|
6
|
+
fonts: {
|
|
7
|
+
google: '',
|
|
8
|
+
adobe: '',
|
|
9
|
+
custom: []
|
|
10
|
+
},
|
|
11
|
+
html: {
|
|
12
|
+
beforeBodyClose: '',
|
|
13
|
+
afterBodyOpen: '',
|
|
14
|
+
head: ''
|
|
15
|
+
},
|
|
16
|
+
meta: {
|
|
17
|
+
favicon: undefined,
|
|
18
|
+
title: undefined,
|
|
19
|
+
opengraphThumbnail: undefined,
|
|
20
|
+
keywords: undefined,
|
|
21
|
+
description: undefined
|
|
22
|
+
},
|
|
23
|
+
grid: {
|
|
24
|
+
color: 'rgba(0, 0, 0, 1)'
|
|
25
|
+
},
|
|
26
|
+
pages: [{
|
|
27
|
+
id: 'pageId',
|
|
28
|
+
title: 'Page',
|
|
29
|
+
articleId: 'articleId',
|
|
30
|
+
slug: '/',
|
|
31
|
+
isPublished: true,
|
|
32
|
+
meta: {
|
|
33
|
+
opengraphThumbnail: 'page-thumbnail',
|
|
34
|
+
title: 'page-title',
|
|
35
|
+
description: 'page-description',
|
|
36
|
+
enabled: true,
|
|
37
|
+
keywords: 'page-keywords'
|
|
38
|
+
}
|
|
39
|
+
}]
|
|
40
|
+
};
|