decap-cms-lib-util 3.2.0 → 3.3.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/README.md +1 -1
- package/dist/decap-cms-lib-util.js +1 -1
- package/dist/decap-cms-lib-util.js.LICENSE.txt +9 -0
- package/dist/decap-cms-lib-util.js.map +1 -1
- package/dist/esm/API.js +17 -25
- package/dist/esm/APIError.js +0 -7
- package/dist/esm/AccessTokenError.js +0 -4
- package/dist/esm/Cursor.js +4 -9
- package/dist/esm/EditorialWorkflowError.js +0 -5
- package/dist/esm/backendUtil.js +6 -7
- package/dist/esm/getBlobSHA.js +3 -3
- package/dist/esm/git-lfs.js +21 -29
- package/dist/esm/implementation.js +14 -17
- package/dist/esm/unsentRequest.js +9 -12
- package/package.json +5 -2
- package/CHANGELOG.md +0 -382
- package/dist/esm/stega.js +0 -129
- package/src/API.ts +0 -376
- package/src/APIError.ts +0 -17
- package/src/APIUtils.ts +0 -38
- package/src/AccessTokenError.ts +0 -11
- package/src/Cursor.ts +0 -178
- package/src/EditorialWorkflowError.ts +0 -12
- package/src/__tests__/api.spec.js +0 -13
- package/src/__tests__/apiUtils.spec.js +0 -74
- package/src/__tests__/asyncLock.spec.js +0 -85
- package/src/__tests__/backendUtil.spec.js +0 -97
- package/src/__tests__/implementation.spec.js +0 -58
- package/src/__tests__/path.spec.js +0 -53
- package/src/__tests__/unsentRequest.spec.js +0 -19
- package/src/asyncLock.ts +0 -43
- package/src/backendUtil.ts +0 -120
- package/src/getBlobSHA.ts +0 -12
- package/src/git-lfs.ts +0 -133
- package/src/implementation.ts +0 -575
- package/src/index.ts +0 -213
- package/src/loadScript.js +0 -24
- package/src/localForage.ts +0 -21
- package/src/path.ts +0 -86
- package/src/promise.ts +0 -21
- package/src/stega.ts +0 -134
- package/src/types/semaphore.d.ts +0 -5
- package/src/types.ts +0 -9
- package/src/unsentRequest.js +0 -133
- package/webpack.config.js +0 -3
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import * as apiUtils from '../APIUtils';
|
|
2
|
-
describe('APIUtils', () => {
|
|
3
|
-
describe('generateContentKey', () => {
|
|
4
|
-
it('should generate content key', () => {
|
|
5
|
-
expect(apiUtils.generateContentKey('posts', 'dir1/dir2/post-title')).toBe(
|
|
6
|
-
'posts/dir1/dir2/post-title',
|
|
7
|
-
);
|
|
8
|
-
});
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
describe('parseContentKey', () => {
|
|
12
|
-
it('should parse content key', () => {
|
|
13
|
-
expect(apiUtils.parseContentKey('posts/dir1/dir2/post-title')).toEqual({
|
|
14
|
-
collection: 'posts',
|
|
15
|
-
slug: 'dir1/dir2/post-title',
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
describe('isCMSLabel', () => {
|
|
21
|
-
it('should return true for CMS label', () => {
|
|
22
|
-
expect(apiUtils.isCMSLabel('decap-cms/draft', 'decap-cms/')).toBe(true);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('should return false for non CMS label', () => {
|
|
26
|
-
expect(apiUtils.isCMSLabel('other/label', 'decap-cms/')).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should return true if the prefix not provided for CMS label', () => {
|
|
30
|
-
expect(apiUtils.isCMSLabel('decap-cms/draft', '')).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should return false if a different prefix provided for CMS label', () => {
|
|
34
|
-
expect(apiUtils.isCMSLabel('decap-cms/draft', 'other/')).toBe(false);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should return true for CMS label when undefined prefix is passed', () => {
|
|
38
|
-
expect(apiUtils.isCMSLabel('decap-cms/draft', undefined)).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('labelToStatus', () => {
|
|
43
|
-
it('should get status from label when default prefix is passed', () => {
|
|
44
|
-
expect(apiUtils.labelToStatus('decap-cms/draft', 'decap-cms/')).toBe('draft');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should get status from label when custom prefix is passed', () => {
|
|
48
|
-
expect(apiUtils.labelToStatus('other/draft', 'other/')).toBe('draft');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should get status from label when empty prefix is passed', () => {
|
|
52
|
-
expect(apiUtils.labelToStatus('decap-cms/draft', '')).toBe('draft');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('should get status from label when undefined prefix is passed', () => {
|
|
56
|
-
expect(apiUtils.labelToStatus('decap-cms/draft', undefined)).toBe('draft');
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
describe('statusToLabel', () => {
|
|
61
|
-
it('should generate label from status when default prefix is passed', () => {
|
|
62
|
-
expect(apiUtils.statusToLabel('draft', 'decap-cms/')).toBe('decap-cms/draft');
|
|
63
|
-
});
|
|
64
|
-
it('should generate label from status when custom prefix is passed', () => {
|
|
65
|
-
expect(apiUtils.statusToLabel('draft', 'other/')).toBe('other/draft');
|
|
66
|
-
});
|
|
67
|
-
it('should generate label from status when empty prefix is passed', () => {
|
|
68
|
-
expect(apiUtils.statusToLabel('draft', '')).toBe('decap-cms/draft');
|
|
69
|
-
});
|
|
70
|
-
it('should generate label from status when undefined prefix is passed', () => {
|
|
71
|
-
expect(apiUtils.statusToLabel('draft', undefined)).toBe('decap-cms/draft');
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { asyncLock } from '../asyncLock';
|
|
2
|
-
|
|
3
|
-
jest.useFakeTimers();
|
|
4
|
-
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
5
|
-
|
|
6
|
-
describe('asyncLock', () => {
|
|
7
|
-
it('should be able to acquire a new lock', async () => {
|
|
8
|
-
const lock = asyncLock();
|
|
9
|
-
|
|
10
|
-
const acquired = await lock.acquire();
|
|
11
|
-
|
|
12
|
-
expect(acquired).toBe(true);
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it('should not be able to acquire an acquired lock', async () => {
|
|
16
|
-
const lock = asyncLock();
|
|
17
|
-
await lock.acquire();
|
|
18
|
-
|
|
19
|
-
const promise = lock.acquire();
|
|
20
|
-
|
|
21
|
-
// advance by default lock timeout
|
|
22
|
-
jest.advanceTimersByTime(15000);
|
|
23
|
-
|
|
24
|
-
const acquired = await promise;
|
|
25
|
-
|
|
26
|
-
expect(acquired).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should be able to acquire an acquired lock that was released', async () => {
|
|
30
|
-
const lock = asyncLock();
|
|
31
|
-
await lock.acquire();
|
|
32
|
-
|
|
33
|
-
const promise = lock.acquire();
|
|
34
|
-
|
|
35
|
-
// release the lock in the "future"
|
|
36
|
-
setTimeout(() => lock.release(), 100);
|
|
37
|
-
|
|
38
|
-
// advance to the time where the lock will be released
|
|
39
|
-
jest.advanceTimersByTime(100);
|
|
40
|
-
|
|
41
|
-
const acquired = await promise;
|
|
42
|
-
|
|
43
|
-
expect(acquired).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should accept a timeout for acquire', async () => {
|
|
47
|
-
const lock = asyncLock();
|
|
48
|
-
await lock.acquire();
|
|
49
|
-
|
|
50
|
-
const promise = lock.acquire(50);
|
|
51
|
-
|
|
52
|
-
/// advance by lock timeout
|
|
53
|
-
jest.advanceTimersByTime(50);
|
|
54
|
-
|
|
55
|
-
const acquired = await promise;
|
|
56
|
-
|
|
57
|
-
expect(acquired).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should be able to re-acquire a lock after a timeout', async () => {
|
|
61
|
-
const lock = asyncLock();
|
|
62
|
-
await lock.acquire();
|
|
63
|
-
|
|
64
|
-
const promise = lock.acquire();
|
|
65
|
-
|
|
66
|
-
// advance by default lock timeout
|
|
67
|
-
jest.advanceTimersByTime(15000);
|
|
68
|
-
|
|
69
|
-
let acquired = await promise;
|
|
70
|
-
|
|
71
|
-
expect(acquired).toBe(false);
|
|
72
|
-
|
|
73
|
-
acquired = await lock.acquire();
|
|
74
|
-
expect(acquired).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('should suppress "leave called too many times" error', async () => {
|
|
78
|
-
const lock = asyncLock();
|
|
79
|
-
|
|
80
|
-
await expect(() => lock.release()).not.toThrow();
|
|
81
|
-
|
|
82
|
-
expect(console.warn).toHaveBeenCalledTimes(1);
|
|
83
|
-
expect(console.warn).toHaveBeenCalledWith('leave called too many times.');
|
|
84
|
-
});
|
|
85
|
-
});
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { oneLine } from 'common-tags';
|
|
2
|
-
import nock from 'nock';
|
|
3
|
-
|
|
4
|
-
import { parseLinkHeader, getAllResponses, getPathDepth, filterByExtension } from '../backendUtil';
|
|
5
|
-
|
|
6
|
-
describe('parseLinkHeader', () => {
|
|
7
|
-
it('should return the right rel urls', () => {
|
|
8
|
-
const url = 'https://api.github.com/resource';
|
|
9
|
-
const link = oneLine`
|
|
10
|
-
<${url}?page=1>; rel="first",
|
|
11
|
-
<${url}?page=2>; rel="prev",
|
|
12
|
-
<${url}?page=4>; rel="next",
|
|
13
|
-
<${url}?page=5>; rel="last"
|
|
14
|
-
`;
|
|
15
|
-
const linkHeader = parseLinkHeader(link);
|
|
16
|
-
|
|
17
|
-
expect(linkHeader.next).toBe(`${url}?page=4`);
|
|
18
|
-
expect(linkHeader.last).toBe(`${url}?page=5`);
|
|
19
|
-
expect(linkHeader.first).toBe(`${url}?page=1`);
|
|
20
|
-
expect(linkHeader.prev).toBe(`${url}?page=2`);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
describe('getAllResponses', () => {
|
|
25
|
-
function generatePulls(length) {
|
|
26
|
-
return Array.from({ length }, (_, id) => {
|
|
27
|
-
return { id: id + 1, number: `134${id}`, state: 'open' };
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function createLinkHeaders({ page, pageCount }) {
|
|
32
|
-
const pageNum = parseInt(page, 10);
|
|
33
|
-
const pageCountNum = parseInt(pageCount, 10);
|
|
34
|
-
const url = 'https://api.github.com/pulls';
|
|
35
|
-
|
|
36
|
-
function link(linkPage) {
|
|
37
|
-
return `<${url}?page=${linkPage}>`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const linkHeader = oneLine`
|
|
41
|
-
${pageNum === 1 ? '' : `${link(1)}; rel="first",`}
|
|
42
|
-
${pageNum === pageCountNum ? '' : `${link(pageCount)}; rel="last",`}
|
|
43
|
-
${pageNum === 1 ? '' : `${link(pageNum - 1)}; rel="prev",`}
|
|
44
|
-
${pageNum === pageCountNum ? '' : `${link(pageNum + 1)}; rel="next",`}
|
|
45
|
-
`.slice(0, -1);
|
|
46
|
-
|
|
47
|
-
return { Link: linkHeader };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function interceptCall({ perPage = 30, repeat = 1, data = [] } = {}) {
|
|
51
|
-
nock('https://api.github.com')
|
|
52
|
-
.get('/pulls')
|
|
53
|
-
.query(true)
|
|
54
|
-
.times(repeat)
|
|
55
|
-
.reply(uri => {
|
|
56
|
-
const searchParams = new URLSearchParams(uri.split('?')[1]);
|
|
57
|
-
const page = searchParams.get('page') || 1;
|
|
58
|
-
const pageCount = data.length <= perPage ? 1 : Math.ceil(data.length / perPage);
|
|
59
|
-
const pageLastIndex = page * perPage;
|
|
60
|
-
const pageFirstIndex = pageLastIndex - perPage;
|
|
61
|
-
const resp = data.slice(pageFirstIndex, pageLastIndex);
|
|
62
|
-
return [200, resp, createLinkHeaders({ page, pageCount })];
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
it('should return all paged response', async () => {
|
|
67
|
-
interceptCall({ repeat: 3, data: generatePulls(70) });
|
|
68
|
-
const res = await getAllResponses('https://api.github.com/pulls', {}, 'next', url => url);
|
|
69
|
-
const pages = await Promise.all(res.map(res => res.json()));
|
|
70
|
-
|
|
71
|
-
expect(pages[0]).toHaveLength(30);
|
|
72
|
-
expect(pages[1]).toHaveLength(30);
|
|
73
|
-
expect(pages[2]).toHaveLength(10);
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
describe('getPathDepth', () => {
|
|
78
|
-
it('should return 1 for empty string', () => {
|
|
79
|
-
expect(getPathDepth('')).toBe(1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('should return 2 for path of one nested folder', () => {
|
|
83
|
-
expect(getPathDepth('{{year}}/{{slug}}')).toBe(2);
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
describe('filterByExtension', () => {
|
|
88
|
-
it('should return true when extension matches', () => {
|
|
89
|
-
expect(filterByExtension({ path: 'file.html.md' }, '.html.md')).toBe(true);
|
|
90
|
-
expect(filterByExtension({ path: 'file.html.md' }, 'html.md')).toBe(true);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("should return false when extension doesn't match", () => {
|
|
94
|
-
expect(filterByExtension({ path: 'file.json' }, '.html.md')).toBe(false);
|
|
95
|
-
expect(filterByExtension({ path: 'file.json' }, 'html.md')).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { getMediaAsBlob, getMediaDisplayURL } from '../implementation';
|
|
2
|
-
|
|
3
|
-
describe('implementation', () => {
|
|
4
|
-
describe('getMediaAsBlob', () => {
|
|
5
|
-
it('should return response blob on non svg file', async () => {
|
|
6
|
-
const blob = {};
|
|
7
|
-
const readFile = jest.fn().mockResolvedValue(blob);
|
|
8
|
-
|
|
9
|
-
await expect(getMediaAsBlob('static/media/image.png', 'sha', readFile)).resolves.toBe(blob);
|
|
10
|
-
|
|
11
|
-
expect(readFile).toHaveBeenCalledTimes(1);
|
|
12
|
-
expect(readFile).toHaveBeenCalledWith('static/media/image.png', 'sha', {
|
|
13
|
-
parseText: false,
|
|
14
|
-
});
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('should return text blob on svg file', async () => {
|
|
18
|
-
const text = 'svg';
|
|
19
|
-
const readFile = jest.fn().mockResolvedValue(text);
|
|
20
|
-
|
|
21
|
-
await expect(getMediaAsBlob('static/media/logo.svg', 'sha', readFile)).resolves.toEqual(
|
|
22
|
-
new Blob([text], { type: 'image/svg+xml' }),
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
expect(readFile).toHaveBeenCalledTimes(1);
|
|
26
|
-
expect(readFile).toHaveBeenCalledWith('static/media/logo.svg', 'sha', {
|
|
27
|
-
parseText: true,
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('getMediaDisplayURL', () => {
|
|
33
|
-
it('should return createObjectURL result', async () => {
|
|
34
|
-
const blob = {};
|
|
35
|
-
const readFile = jest.fn().mockResolvedValue(blob);
|
|
36
|
-
const semaphore = { take: jest.fn(callback => callback()), leave: jest.fn() };
|
|
37
|
-
|
|
38
|
-
global.URL.createObjectURL = jest
|
|
39
|
-
.fn()
|
|
40
|
-
.mockResolvedValue('blob:http://localhost:8080/blob-id');
|
|
41
|
-
|
|
42
|
-
await expect(
|
|
43
|
-
getMediaDisplayURL({ path: 'static/media/image.png', id: 'sha' }, readFile, semaphore),
|
|
44
|
-
).resolves.toBe('blob:http://localhost:8080/blob-id');
|
|
45
|
-
|
|
46
|
-
expect(semaphore.take).toHaveBeenCalledTimes(1);
|
|
47
|
-
expect(semaphore.leave).toHaveBeenCalledTimes(1);
|
|
48
|
-
|
|
49
|
-
expect(readFile).toHaveBeenCalledTimes(1);
|
|
50
|
-
expect(readFile).toHaveBeenCalledWith('static/media/image.png', 'sha', {
|
|
51
|
-
parseText: false,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(global.URL.createObjectURL).toHaveBeenCalledTimes(1);
|
|
55
|
-
expect(global.URL.createObjectURL).toHaveBeenCalledWith(blob);
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { fileExtensionWithSeparator, fileExtension } from '../path';
|
|
2
|
-
|
|
3
|
-
describe('fileExtensionWithSeparator', () => {
|
|
4
|
-
it('should return the extension of a file', () => {
|
|
5
|
-
expect(fileExtensionWithSeparator('index.html')).toEqual('.html');
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
it('should return the extension of a file path', () => {
|
|
9
|
-
expect(fileExtensionWithSeparator('/src/main/index.html')).toEqual('.html');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should return the extension of a file path with trailing slash', () => {
|
|
13
|
-
expect(fileExtensionWithSeparator('/src/main/index.html/')).toEqual('.html');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('should return the extension for an extension with two ..', () => {
|
|
17
|
-
expect(fileExtensionWithSeparator('/src/main/index..html')).toEqual('.html');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it('should return an empty string for the parent path ..', () => {
|
|
21
|
-
expect(fileExtensionWithSeparator('..')).toEqual('');
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should return an empty string if the file has no extension', () => {
|
|
25
|
-
expect(fileExtensionWithSeparator('/src/main/index')).toEqual('');
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('fileExtension', () => {
|
|
30
|
-
it('should return the extension of a file', () => {
|
|
31
|
-
expect(fileExtension('index.html')).toEqual('html');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('should return the extension of a file path', () => {
|
|
35
|
-
expect(fileExtension('/src/main/index.html')).toEqual('html');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should return the extension of a file path with trailing slash', () => {
|
|
39
|
-
expect(fileExtension('/src/main/index.html/')).toEqual('html');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should return the extension for an extension with two ..', () => {
|
|
43
|
-
expect(fileExtension('/src/main/index..html')).toEqual('html');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should return an empty string for the parent path ..', () => {
|
|
47
|
-
expect(fileExtension('..')).toEqual('');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('should return an empty string if the file has no extension', () => {
|
|
51
|
-
expect(fileExtension('/src/main/index')).toEqual('');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import unsentRequest from '../unsentRequest';
|
|
2
|
-
|
|
3
|
-
describe('unsentRequest', () => {
|
|
4
|
-
describe('withHeaders', () => {
|
|
5
|
-
it('should create new request with headers', () => {
|
|
6
|
-
expect(unsentRequest.withHeaders({ Authorization: 'token' })('path').toJS()).toEqual({
|
|
7
|
-
url: 'path',
|
|
8
|
-
headers: { Authorization: 'token' },
|
|
9
|
-
});
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
it('should add headers to existing request', () => {
|
|
13
|
-
expect(unsentRequest.withHeaders({ Authorization: 'token' }, 'path').toJS()).toEqual({
|
|
14
|
-
url: 'path',
|
|
15
|
-
headers: { Authorization: 'token' },
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
});
|
|
19
|
-
});
|
package/src/asyncLock.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import semaphore from 'semaphore';
|
|
2
|
-
|
|
3
|
-
export type AsyncLock = { release: () => void; acquire: () => Promise<boolean> };
|
|
4
|
-
|
|
5
|
-
export function asyncLock(): AsyncLock {
|
|
6
|
-
let lock = semaphore(1);
|
|
7
|
-
|
|
8
|
-
function acquire(timeout = 15000) {
|
|
9
|
-
const promise = new Promise<boolean>(resolve => {
|
|
10
|
-
// this makes sure a caller doesn't gets stuck forever awaiting on the lock
|
|
11
|
-
const timeoutId = setTimeout(() => {
|
|
12
|
-
// we reset the lock in that case to allow future consumers to use it without being blocked
|
|
13
|
-
lock = semaphore(1);
|
|
14
|
-
resolve(false);
|
|
15
|
-
}, timeout);
|
|
16
|
-
|
|
17
|
-
lock.take(() => {
|
|
18
|
-
clearTimeout(timeoutId);
|
|
19
|
-
resolve(true);
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
return promise;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function release() {
|
|
27
|
-
try {
|
|
28
|
-
// suppress too many calls to leave error
|
|
29
|
-
lock.leave();
|
|
30
|
-
} catch (e) {
|
|
31
|
-
// calling 'leave' too many times might not be good behavior
|
|
32
|
-
// but there is no reason to completely fail on it
|
|
33
|
-
if (e.message !== 'leave called too many times.') {
|
|
34
|
-
throw e;
|
|
35
|
-
} else {
|
|
36
|
-
console.warn('leave called too many times.');
|
|
37
|
-
lock = semaphore(1);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return { acquire, release };
|
|
43
|
-
}
|
package/src/backendUtil.ts
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { flow, fromPairs } from 'lodash';
|
|
2
|
-
import { map } from 'lodash/fp';
|
|
3
|
-
import { fromJS } from 'immutable';
|
|
4
|
-
|
|
5
|
-
import unsentRequest from './unsentRequest';
|
|
6
|
-
import APIError from './APIError';
|
|
7
|
-
|
|
8
|
-
type Formatter = (res: Response) => Promise<string | Blob | unknown>;
|
|
9
|
-
|
|
10
|
-
export function filterByExtension(file: { path: string }, extension: string) {
|
|
11
|
-
const path = file?.path || '';
|
|
12
|
-
return path.endsWith(extension.startsWith('.') ? extension : `.${extension}`);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function catchFormatErrors(format: string, formatter: Formatter) {
|
|
16
|
-
return (res: Response) => {
|
|
17
|
-
try {
|
|
18
|
-
return formatter(res);
|
|
19
|
-
} catch (err) {
|
|
20
|
-
throw new Error(
|
|
21
|
-
`Response cannot be parsed into the expected format (${format}): ${err.message}`,
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const responseFormatters = fromJS({
|
|
28
|
-
json: async (res: Response) => {
|
|
29
|
-
const contentType = res.headers.get('Content-Type') || '';
|
|
30
|
-
if (!contentType.startsWith('application/json') && !contentType.startsWith('text/json')) {
|
|
31
|
-
throw new Error(`${contentType} is not a valid JSON Content-Type`);
|
|
32
|
-
}
|
|
33
|
-
return res.json();
|
|
34
|
-
},
|
|
35
|
-
text: async (res: Response) => res.text(),
|
|
36
|
-
blob: async (res: Response) => res.blob(),
|
|
37
|
-
}).mapEntries(([format, formatter]: [string, Formatter]) => [
|
|
38
|
-
format,
|
|
39
|
-
catchFormatErrors(format, formatter),
|
|
40
|
-
]);
|
|
41
|
-
|
|
42
|
-
export async function parseResponse(
|
|
43
|
-
res: Response,
|
|
44
|
-
{ expectingOk = true, format = 'text', apiName = '' },
|
|
45
|
-
) {
|
|
46
|
-
let body;
|
|
47
|
-
try {
|
|
48
|
-
const formatter = responseFormatters.get(format, false);
|
|
49
|
-
if (!formatter) {
|
|
50
|
-
throw new Error(`${format} is not a supported response format.`);
|
|
51
|
-
}
|
|
52
|
-
body = await formatter(res);
|
|
53
|
-
} catch (err) {
|
|
54
|
-
throw new APIError(err.message, res.status, apiName);
|
|
55
|
-
}
|
|
56
|
-
if (expectingOk && !res.ok) {
|
|
57
|
-
const isJSON = format === 'json';
|
|
58
|
-
const message = isJSON ? body.message || body.msg || body.error?.message : body;
|
|
59
|
-
throw new APIError(isJSON && message ? message : body, res.status, apiName);
|
|
60
|
-
}
|
|
61
|
-
return body;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function responseParser(options: {
|
|
65
|
-
expectingOk?: boolean;
|
|
66
|
-
format: string;
|
|
67
|
-
apiName: string;
|
|
68
|
-
}) {
|
|
69
|
-
return (res: Response) => parseResponse(res, options);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function parseLinkHeader(header: string | null) {
|
|
73
|
-
if (!header) {
|
|
74
|
-
return {};
|
|
75
|
-
}
|
|
76
|
-
return flow([
|
|
77
|
-
linksString => linksString.split(','),
|
|
78
|
-
map((str: string) => str.trim().split(';')),
|
|
79
|
-
map(([linkStr, keyStr]) => [
|
|
80
|
-
keyStr.match(/rel="(.*?)"/)[1],
|
|
81
|
-
linkStr
|
|
82
|
-
.trim()
|
|
83
|
-
.match(/<(.*?)>/)[1]
|
|
84
|
-
.replace(/\+/g, '%20'),
|
|
85
|
-
]),
|
|
86
|
-
fromPairs,
|
|
87
|
-
])(header);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export async function getAllResponses(
|
|
91
|
-
url: string,
|
|
92
|
-
options: { headers?: {} } = {},
|
|
93
|
-
linkHeaderRelName: string,
|
|
94
|
-
nextUrlProcessor: (url: string) => string,
|
|
95
|
-
) {
|
|
96
|
-
const maxResponses = 30;
|
|
97
|
-
let responseCount = 1;
|
|
98
|
-
|
|
99
|
-
let req = unsentRequest.fromFetchArguments(url, options);
|
|
100
|
-
|
|
101
|
-
const pageResponses = [];
|
|
102
|
-
|
|
103
|
-
while (req && responseCount < maxResponses) {
|
|
104
|
-
const pageResponse = await unsentRequest.performRequest(req);
|
|
105
|
-
const linkHeader = pageResponse.headers.get('Link');
|
|
106
|
-
const nextURL = linkHeader && parseLinkHeader(linkHeader)[linkHeaderRelName];
|
|
107
|
-
|
|
108
|
-
const { headers = {} } = options;
|
|
109
|
-
req = nextURL && unsentRequest.fromFetchArguments(nextUrlProcessor(nextURL), { headers });
|
|
110
|
-
pageResponses.push(pageResponse);
|
|
111
|
-
responseCount++;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return pageResponses;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function getPathDepth(path: string) {
|
|
118
|
-
const depth = path.split('/').length;
|
|
119
|
-
return depth;
|
|
120
|
-
}
|
package/src/getBlobSHA.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { sha256 } from 'js-sha256';
|
|
2
|
-
|
|
3
|
-
export default (blob: Blob): Promise<string> =>
|
|
4
|
-
new Promise((resolve, reject) => {
|
|
5
|
-
const fr = new FileReader();
|
|
6
|
-
fr.onload = ({ target }) => resolve(sha256(target?.result || ''));
|
|
7
|
-
fr.onerror = err => {
|
|
8
|
-
fr.abort();
|
|
9
|
-
reject(err);
|
|
10
|
-
};
|
|
11
|
-
fr.readAsArrayBuffer(blob);
|
|
12
|
-
});
|