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