decap-cms-lib-util 3.2.0 → 3.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/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/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
package/src/API.ts
DELETED
|
@@ -1,376 +0,0 @@
|
|
|
1
|
-
import { asyncLock } from './asyncLock';
|
|
2
|
-
import unsentRequest from './unsentRequest';
|
|
3
|
-
import APIError from './APIError';
|
|
4
|
-
|
|
5
|
-
import type { AsyncLock } from './asyncLock';
|
|
6
|
-
|
|
7
|
-
export interface FetchError extends Error {
|
|
8
|
-
status: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface API {
|
|
12
|
-
rateLimiter?: AsyncLock;
|
|
13
|
-
buildRequest: (req: ApiRequest) => ApiRequest | Promise<ApiRequest>;
|
|
14
|
-
requestFunction?: (req: ApiRequest) => Promise<Response>;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export type ApiRequestObject = {
|
|
18
|
-
url: string;
|
|
19
|
-
params?: Record<string, string | boolean | number>;
|
|
20
|
-
method?: 'POST' | 'PUT' | 'DELETE' | 'HEAD' | 'PATCH';
|
|
21
|
-
headers?: Record<string, string>;
|
|
22
|
-
body?: string | FormData;
|
|
23
|
-
cache?: 'no-store';
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export type ApiRequest = ApiRequestObject | string;
|
|
27
|
-
|
|
28
|
-
class RateLimitError extends Error {
|
|
29
|
-
resetSeconds: number;
|
|
30
|
-
|
|
31
|
-
constructor(message: string, resetSeconds: number) {
|
|
32
|
-
super(message);
|
|
33
|
-
if (resetSeconds < 0) {
|
|
34
|
-
this.resetSeconds = 1;
|
|
35
|
-
} else if (resetSeconds > 60 * 60) {
|
|
36
|
-
this.resetSeconds = 60 * 60;
|
|
37
|
-
} else {
|
|
38
|
-
this.resetSeconds = resetSeconds;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function parseJsonResponse(response: Response) {
|
|
44
|
-
const json = await response.json();
|
|
45
|
-
if (!response.ok) {
|
|
46
|
-
return Promise.reject(json);
|
|
47
|
-
}
|
|
48
|
-
return json;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function parseResponse(response: Response) {
|
|
52
|
-
const contentType = response.headers.get('Content-Type');
|
|
53
|
-
if (contentType && contentType.match(/json/)) {
|
|
54
|
-
return parseJsonResponse(response);
|
|
55
|
-
}
|
|
56
|
-
const textPromise = response.text().then(text => {
|
|
57
|
-
if (!response.ok) return Promise.reject(text);
|
|
58
|
-
return text;
|
|
59
|
-
});
|
|
60
|
-
return textPromise;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function requestWithBackoff(
|
|
64
|
-
api: API,
|
|
65
|
-
req: ApiRequest,
|
|
66
|
-
attempt = 1,
|
|
67
|
-
): Promise<Response> {
|
|
68
|
-
if (api.rateLimiter) {
|
|
69
|
-
await api.rateLimiter.acquire();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const builtRequest = await api.buildRequest(req);
|
|
74
|
-
const requestFunction = api.requestFunction || unsentRequest.performRequest;
|
|
75
|
-
const response: Response = await requestFunction(builtRequest);
|
|
76
|
-
if (response.status === 429) {
|
|
77
|
-
// GitLab/Bitbucket too many requests
|
|
78
|
-
const text = await response.text().catch(() => 'Too many requests');
|
|
79
|
-
throw new Error(text);
|
|
80
|
-
} else if (response.status === 403) {
|
|
81
|
-
// GitHub too many requests
|
|
82
|
-
const json = await response.json().catch(() => ({ message: '' }));
|
|
83
|
-
if (json.message.match('API rate limit exceeded')) {
|
|
84
|
-
const now = new Date();
|
|
85
|
-
const nextWindowInSeconds = response.headers.has('X-RateLimit-Reset')
|
|
86
|
-
? parseInt(response.headers.get('X-RateLimit-Reset')!)
|
|
87
|
-
: now.getTime() / 1000 + 60;
|
|
88
|
-
|
|
89
|
-
throw new RateLimitError(json.message, nextWindowInSeconds);
|
|
90
|
-
}
|
|
91
|
-
response.json = () => Promise.resolve(json);
|
|
92
|
-
}
|
|
93
|
-
return response;
|
|
94
|
-
} catch (err) {
|
|
95
|
-
if (attempt > 5 || err.message === "Can't refresh access token when using implicit auth") {
|
|
96
|
-
throw err;
|
|
97
|
-
} else {
|
|
98
|
-
if (!api.rateLimiter) {
|
|
99
|
-
const timeout = err.resetSeconds || attempt * attempt;
|
|
100
|
-
console.log(
|
|
101
|
-
`Pausing requests for ${timeout} ${
|
|
102
|
-
attempt === 1 ? 'second' : 'seconds'
|
|
103
|
-
} due to fetch failures:`,
|
|
104
|
-
err.message,
|
|
105
|
-
);
|
|
106
|
-
api.rateLimiter = asyncLock();
|
|
107
|
-
api.rateLimiter.acquire();
|
|
108
|
-
setTimeout(() => {
|
|
109
|
-
api.rateLimiter?.release();
|
|
110
|
-
api.rateLimiter = undefined;
|
|
111
|
-
console.log(`Done pausing requests`);
|
|
112
|
-
}, 1000 * timeout);
|
|
113
|
-
}
|
|
114
|
-
return requestWithBackoff(api, req, attempt + 1);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Options is an object which contains all the standard network request properties
|
|
120
|
-
// for modifying HTTP requests and may contains `params` property
|
|
121
|
-
|
|
122
|
-
type Param = string | number;
|
|
123
|
-
|
|
124
|
-
type ParamObject = Record<string, Param>;
|
|
125
|
-
|
|
126
|
-
type HeaderObj = Record<string, string>;
|
|
127
|
-
|
|
128
|
-
type HeaderConfig = {
|
|
129
|
-
headers?: HeaderObj;
|
|
130
|
-
token?: string | undefined;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
type Backend = 'github' | 'gitlab' | 'bitbucket';
|
|
134
|
-
|
|
135
|
-
// RequestConfig contains all the standard properties of a Request object and
|
|
136
|
-
// several custom properties:
|
|
137
|
-
// - "headers" property is an object whose properties and values are string types
|
|
138
|
-
// - `token` property to allow passing tokens for users using a private repo.
|
|
139
|
-
// - `params` property for customizing response
|
|
140
|
-
// - `backend`(compulsory) to specify which backend to be used: Github, Gitlab etc.
|
|
141
|
-
|
|
142
|
-
type RequestConfig = Omit<RequestInit, 'headers'> &
|
|
143
|
-
HeaderConfig & {
|
|
144
|
-
backend: Backend;
|
|
145
|
-
apiRoot?: string;
|
|
146
|
-
params?: ParamObject;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
export const apiRoots = {
|
|
150
|
-
github: 'https://api.github.com',
|
|
151
|
-
gitlab: 'https://gitlab.com/api/v4',
|
|
152
|
-
bitbucket: 'https://api.bitbucket.org/2.0',
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
export const endpointConstants = {
|
|
156
|
-
singleRepo: {
|
|
157
|
-
bitbucket: '/repositories',
|
|
158
|
-
github: '/repos',
|
|
159
|
-
gitlab: '/projects',
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const api = {
|
|
164
|
-
buildRequest(req: ApiRequest) {
|
|
165
|
-
return req;
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
function constructUrlWithParams(url: string, params?: ParamObject) {
|
|
170
|
-
if (params) {
|
|
171
|
-
const paramList = [];
|
|
172
|
-
for (const key in params) {
|
|
173
|
-
paramList.push(`${key}=${encodeURIComponent(params[key])}`);
|
|
174
|
-
}
|
|
175
|
-
if (paramList.length) {
|
|
176
|
-
url += `?${paramList.join('&')}`;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return url;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function constructRequestHeaders(headerConfig: HeaderConfig) {
|
|
183
|
-
const { token, headers } = headerConfig;
|
|
184
|
-
const baseHeaders: HeaderObj = { 'Content-Type': 'application/json; charset=utf-8', ...headers };
|
|
185
|
-
if (token) {
|
|
186
|
-
baseHeaders['Authorization'] = `Bearer ${token}`;
|
|
187
|
-
}
|
|
188
|
-
return Promise.resolve(baseHeaders);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function handleRequestError(error: FetchError, responseStatus: number, backend: Backend) {
|
|
192
|
-
throw new APIError(error.message, responseStatus, backend);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export async function apiRequest(
|
|
196
|
-
path: string,
|
|
197
|
-
config: RequestConfig,
|
|
198
|
-
parser = (response: Response) => parseResponse(response),
|
|
199
|
-
) {
|
|
200
|
-
const { token, backend, ...props } = config;
|
|
201
|
-
const options = { cache: 'no-cache', ...props };
|
|
202
|
-
const headers = await constructRequestHeaders({ headers: options.headers || {}, token });
|
|
203
|
-
const baseUrl = config.apiRoot ?? apiRoots[backend];
|
|
204
|
-
const url = constructUrlWithParams(`${baseUrl}${path}`, options.params);
|
|
205
|
-
let responseStatus = 500;
|
|
206
|
-
try {
|
|
207
|
-
const req = unsentRequest.fromFetchArguments(url, {
|
|
208
|
-
...options,
|
|
209
|
-
headers,
|
|
210
|
-
}) as unknown as ApiRequest;
|
|
211
|
-
const response = await requestWithBackoff(api, req);
|
|
212
|
-
responseStatus = response.status;
|
|
213
|
-
const parsedResponse = await parser(response);
|
|
214
|
-
return parsedResponse;
|
|
215
|
-
} catch (error) {
|
|
216
|
-
return handleRequestError(error, responseStatus, backend);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export async function getDefaultBranchName(configs: {
|
|
221
|
-
backend: Backend;
|
|
222
|
-
repo: string;
|
|
223
|
-
token?: string;
|
|
224
|
-
apiRoot?: string;
|
|
225
|
-
}) {
|
|
226
|
-
let apiPath;
|
|
227
|
-
const { token, backend, repo, apiRoot } = configs;
|
|
228
|
-
switch (backend) {
|
|
229
|
-
case 'gitlab': {
|
|
230
|
-
apiPath = `/projects/${encodeURIComponent(repo)}`;
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
case 'bitbucket': {
|
|
234
|
-
apiPath = `/repositories/${repo}`;
|
|
235
|
-
break;
|
|
236
|
-
}
|
|
237
|
-
default: {
|
|
238
|
-
apiPath = `/repos/${repo}`;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
const repoInfo = await apiRequest(apiPath, { token, backend, apiRoot });
|
|
242
|
-
let defaultBranchName;
|
|
243
|
-
if (backend === 'bitbucket') {
|
|
244
|
-
const {
|
|
245
|
-
mainbranch: { name },
|
|
246
|
-
} = repoInfo;
|
|
247
|
-
defaultBranchName = name;
|
|
248
|
-
} else {
|
|
249
|
-
const { default_branch } = repoInfo;
|
|
250
|
-
defaultBranchName = default_branch;
|
|
251
|
-
}
|
|
252
|
-
return defaultBranchName;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
export async function readFile(
|
|
256
|
-
id: string | null | undefined,
|
|
257
|
-
fetchContent: () => Promise<string | Blob>,
|
|
258
|
-
localForage: LocalForage,
|
|
259
|
-
isText: boolean,
|
|
260
|
-
) {
|
|
261
|
-
const key = id ? (isText ? `gh.${id}` : `gh.${id}.blob`) : null;
|
|
262
|
-
const cached = key ? await localForage.getItem<string | Blob>(key) : null;
|
|
263
|
-
if (cached) {
|
|
264
|
-
return cached;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const content = await fetchContent();
|
|
268
|
-
if (key) {
|
|
269
|
-
await localForage.setItem(key, content);
|
|
270
|
-
}
|
|
271
|
-
return content;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export type FileMetadata = {
|
|
275
|
-
author: string;
|
|
276
|
-
updatedOn: string;
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
function getFileMetadataKey(id: string) {
|
|
280
|
-
return `gh.${id}.meta`;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export async function readFileMetadata(
|
|
284
|
-
id: string | null | undefined,
|
|
285
|
-
fetchMetadata: () => Promise<FileMetadata>,
|
|
286
|
-
localForage: LocalForage,
|
|
287
|
-
) {
|
|
288
|
-
const key = id ? getFileMetadataKey(id) : null;
|
|
289
|
-
const cached = key && (await localForage.getItem<FileMetadata>(key));
|
|
290
|
-
if (cached) {
|
|
291
|
-
return cached;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const metadata = await fetchMetadata();
|
|
295
|
-
if (key) {
|
|
296
|
-
await localForage.setItem<FileMetadata>(key, metadata);
|
|
297
|
-
}
|
|
298
|
-
return metadata;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Keywords for inferring a status that will provide a deploy preview URL.
|
|
303
|
-
*/
|
|
304
|
-
const PREVIEW_CONTEXT_KEYWORDS = ['deploy'];
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Check a given status context string to determine if it provides a link to a
|
|
308
|
-
* deploy preview. Checks for an exact match against `previewContext` if given,
|
|
309
|
-
* otherwise checks for inclusion of a value from `PREVIEW_CONTEXT_KEYWORDS`.
|
|
310
|
-
*/
|
|
311
|
-
export function isPreviewContext(context: string, previewContext: string) {
|
|
312
|
-
if (previewContext) {
|
|
313
|
-
return context === previewContext;
|
|
314
|
-
}
|
|
315
|
-
return PREVIEW_CONTEXT_KEYWORDS.some(keyword => context.includes(keyword));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export enum PreviewState {
|
|
319
|
-
Other = 'other',
|
|
320
|
-
Success = 'success',
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Retrieve a deploy preview URL from an array of statuses. By default, a
|
|
325
|
-
* matching status is inferred via `isPreviewContext`.
|
|
326
|
-
*/
|
|
327
|
-
export function getPreviewStatus(
|
|
328
|
-
statuses: {
|
|
329
|
-
context: string;
|
|
330
|
-
target_url: string;
|
|
331
|
-
state: PreviewState;
|
|
332
|
-
}[],
|
|
333
|
-
previewContext: string,
|
|
334
|
-
) {
|
|
335
|
-
return statuses.find(({ context }) => {
|
|
336
|
-
return isPreviewContext(context, previewContext);
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function getConflictingBranches(branchName: string) {
|
|
341
|
-
// for cms/posts/post-1, conflicting branches are cms/posts, cms
|
|
342
|
-
const parts = branchName.split('/');
|
|
343
|
-
parts.pop();
|
|
344
|
-
|
|
345
|
-
const conflictingBranches = parts.reduce((acc, _, index) => {
|
|
346
|
-
acc = [...acc, parts.slice(0, index + 1).join('/')];
|
|
347
|
-
return acc;
|
|
348
|
-
}, [] as string[]);
|
|
349
|
-
|
|
350
|
-
return conflictingBranches;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
export async function throwOnConflictingBranches(
|
|
354
|
-
branchName: string,
|
|
355
|
-
getBranch: (name: string) => Promise<{ name: string }>,
|
|
356
|
-
apiName: string,
|
|
357
|
-
) {
|
|
358
|
-
const possibleConflictingBranches = getConflictingBranches(branchName);
|
|
359
|
-
|
|
360
|
-
const conflictingBranches = await Promise.all(
|
|
361
|
-
possibleConflictingBranches.map(b =>
|
|
362
|
-
getBranch(b)
|
|
363
|
-
.then(b => b.name)
|
|
364
|
-
.catch(() => ''),
|
|
365
|
-
),
|
|
366
|
-
);
|
|
367
|
-
|
|
368
|
-
const conflictingBranch = conflictingBranches.filter(Boolean)[0];
|
|
369
|
-
if (conflictingBranch) {
|
|
370
|
-
throw new APIError(
|
|
371
|
-
`Failed creating branch '${branchName}' since there is already a branch named '${conflictingBranch}'. Please delete the '${conflictingBranch}' branch and try again`,
|
|
372
|
-
500,
|
|
373
|
-
apiName,
|
|
374
|
-
);
|
|
375
|
-
}
|
|
376
|
-
}
|
package/src/APIError.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export const API_ERROR = 'API_ERROR';
|
|
2
|
-
|
|
3
|
-
export default class APIError extends Error {
|
|
4
|
-
message: string;
|
|
5
|
-
status: null | number;
|
|
6
|
-
api: string;
|
|
7
|
-
meta: {};
|
|
8
|
-
|
|
9
|
-
constructor(message: string, status: null | number, api: string, meta = {}) {
|
|
10
|
-
super(message);
|
|
11
|
-
this.message = message;
|
|
12
|
-
this.status = status;
|
|
13
|
-
this.api = api;
|
|
14
|
-
this.name = API_ERROR;
|
|
15
|
-
this.meta = meta;
|
|
16
|
-
}
|
|
17
|
-
}
|
package/src/APIUtils.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export const CMS_BRANCH_PREFIX = 'cms';
|
|
2
|
-
export const DEFAULT_PR_BODY = 'Automatically generated by Decap CMS';
|
|
3
|
-
export const MERGE_COMMIT_MESSAGE = 'Automatically generated. Merged on Decap CMS.';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_DECAP_CMS_LABEL_PREFIX = 'decap-cms/';
|
|
6
|
-
|
|
7
|
-
function getLabelPrefix(labelPrefix: string) {
|
|
8
|
-
return labelPrefix || DEFAULT_DECAP_CMS_LABEL_PREFIX;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function isCMSLabel(label: string, labelPrefix: string) {
|
|
12
|
-
return label.startsWith(getLabelPrefix(labelPrefix));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function labelToStatus(label: string, labelPrefix: string) {
|
|
16
|
-
return label.slice(getLabelPrefix(labelPrefix).length);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function statusToLabel(status: string, labelPrefix: string) {
|
|
20
|
-
return `${getLabelPrefix(labelPrefix)}${status}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function generateContentKey(collectionName: string, slug: string) {
|
|
24
|
-
return `${collectionName}/${slug}`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function parseContentKey(contentKey: string) {
|
|
28
|
-
const index = contentKey.indexOf('/');
|
|
29
|
-
return { collection: contentKey.slice(0, index), slug: contentKey.slice(index + 1) };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function contentKeyFromBranch(branch: string) {
|
|
33
|
-
return branch.slice(`${CMS_BRANCH_PREFIX}/`.length);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function branchFromContentKey(contentKey: string) {
|
|
37
|
-
return `${CMS_BRANCH_PREFIX}/${contentKey}`;
|
|
38
|
-
}
|
package/src/AccessTokenError.ts
DELETED
package/src/Cursor.ts
DELETED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
import { fromJS, Map, Set } from 'immutable';
|
|
2
|
-
|
|
3
|
-
type CursorStoreObject = {
|
|
4
|
-
actions: Set<string>;
|
|
5
|
-
data: Map<string, unknown>;
|
|
6
|
-
meta: Map<string, unknown>;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export type CursorStore = {
|
|
10
|
-
get<K extends keyof CursorStoreObject>(
|
|
11
|
-
key: K,
|
|
12
|
-
defaultValue?: CursorStoreObject[K],
|
|
13
|
-
): CursorStoreObject[K];
|
|
14
|
-
getIn<V>(path: string[]): V;
|
|
15
|
-
set<K extends keyof CursorStoreObject, V extends CursorStoreObject[K]>(
|
|
16
|
-
key: K,
|
|
17
|
-
value: V,
|
|
18
|
-
): CursorStoreObject[K];
|
|
19
|
-
setIn(path: string[], value: unknown): CursorStore;
|
|
20
|
-
hasIn(path: string[]): boolean;
|
|
21
|
-
mergeIn(path: string[], value: unknown): CursorStore;
|
|
22
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
-
update: (...args: any[]) => CursorStore;
|
|
24
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
-
updateIn: (...args: any[]) => CursorStore;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type ActionHandler = (action: string) => unknown;
|
|
29
|
-
|
|
30
|
-
function jsToMap(obj: {}) {
|
|
31
|
-
if (obj === undefined) {
|
|
32
|
-
return Map();
|
|
33
|
-
}
|
|
34
|
-
const immutableObj = fromJS(obj);
|
|
35
|
-
if (!Map.isMap(immutableObj)) {
|
|
36
|
-
throw new Error('Object must be equivalent to a Map.');
|
|
37
|
-
}
|
|
38
|
-
return immutableObj;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const knownMetaKeys = Set([
|
|
42
|
-
'index',
|
|
43
|
-
'page',
|
|
44
|
-
'count',
|
|
45
|
-
'pageSize',
|
|
46
|
-
'pageCount',
|
|
47
|
-
'usingOldPaginationAPI',
|
|
48
|
-
'extension',
|
|
49
|
-
'folder',
|
|
50
|
-
'depth',
|
|
51
|
-
]);
|
|
52
|
-
|
|
53
|
-
function filterUnknownMetaKeys(meta: Map<string, string>) {
|
|
54
|
-
return meta.filter((_v, k) => knownMetaKeys.has(k as string));
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/*
|
|
58
|
-
createCursorMap takes one of three signatures:
|
|
59
|
-
- () -> cursor with empty actions, data, and meta
|
|
60
|
-
- (cursorMap: <object/Map with optional actions, data, and meta keys>) -> cursor
|
|
61
|
-
- (actions: <array/List>, data: <object/Map>, meta: <optional object/Map>) -> cursor
|
|
62
|
-
*/
|
|
63
|
-
function createCursorStore(...args: {}[]) {
|
|
64
|
-
const { actions, data, meta } =
|
|
65
|
-
args.length === 1
|
|
66
|
-
? jsToMap(args[0]).toObject()
|
|
67
|
-
: { actions: args[0], data: args[1], meta: args[2] };
|
|
68
|
-
return Map({
|
|
69
|
-
// actions are a Set, rather than a List, to ensure an efficient .has
|
|
70
|
-
actions: Set(actions),
|
|
71
|
-
|
|
72
|
-
// data and meta are Maps
|
|
73
|
-
data: jsToMap(data),
|
|
74
|
-
meta: jsToMap(meta).update(filterUnknownMetaKeys),
|
|
75
|
-
}) as CursorStore;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function hasAction(store: CursorStore, action: string) {
|
|
79
|
-
return store.hasIn(['actions', action]);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function getActionHandlers(store: CursorStore, handler: ActionHandler) {
|
|
83
|
-
return store
|
|
84
|
-
.get('actions', Set<string>())
|
|
85
|
-
.toMap()
|
|
86
|
-
.map(action => handler(action as string));
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// The cursor logic is entirely functional, so this class simply
|
|
90
|
-
// provides a chainable interface
|
|
91
|
-
export default class Cursor {
|
|
92
|
-
store?: CursorStore;
|
|
93
|
-
actions?: Set<string>;
|
|
94
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
-
data?: Map<string, any>;
|
|
96
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
-
meta?: Map<string, any>;
|
|
98
|
-
|
|
99
|
-
static create(...args: {}[]) {
|
|
100
|
-
return new Cursor(...args);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
constructor(...args: {}[]) {
|
|
104
|
-
if (args[0] instanceof Cursor) {
|
|
105
|
-
return args[0] as Cursor;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
this.store = createCursorStore(...args);
|
|
109
|
-
this.actions = this.store.get('actions');
|
|
110
|
-
this.data = this.store.get('data');
|
|
111
|
-
this.meta = this.store.get('meta');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
-
updateStore(...args: any[]) {
|
|
116
|
-
return new Cursor(this.store!.update(...args));
|
|
117
|
-
}
|
|
118
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
119
|
-
updateInStore(...args: any[]) {
|
|
120
|
-
return new Cursor(this.store!.updateIn(...args));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
hasAction(action: string) {
|
|
124
|
-
return hasAction(this.store!, action);
|
|
125
|
-
}
|
|
126
|
-
addAction(action: string) {
|
|
127
|
-
return this.updateStore('actions', (actions: Set<string>) => actions.add(action));
|
|
128
|
-
}
|
|
129
|
-
removeAction(action: string) {
|
|
130
|
-
return this.updateStore('actions', (actions: Set<string>) => actions.delete(action));
|
|
131
|
-
}
|
|
132
|
-
setActions(actions: Iterable<string>) {
|
|
133
|
-
return this.updateStore((store: CursorStore) => store.set('actions', Set<string>(actions)));
|
|
134
|
-
}
|
|
135
|
-
mergeActions(actions: Set<string>) {
|
|
136
|
-
return this.updateStore('actions', (oldActions: Set<string>) => oldActions.union(actions));
|
|
137
|
-
}
|
|
138
|
-
getActionHandlers(handler: ActionHandler) {
|
|
139
|
-
return getActionHandlers(this.store!, handler);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
setData(data: {}) {
|
|
143
|
-
return new Cursor(this.store!.set('data', jsToMap(data)));
|
|
144
|
-
}
|
|
145
|
-
mergeData(data: {}) {
|
|
146
|
-
return new Cursor(this.store!.mergeIn(['data'], jsToMap(data)));
|
|
147
|
-
}
|
|
148
|
-
wrapData(data: {}) {
|
|
149
|
-
return this.updateStore('data', (oldData: Map<string, unknown>) =>
|
|
150
|
-
jsToMap(data).set('wrapped_cursor_data', oldData),
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
unwrapData() {
|
|
154
|
-
return [
|
|
155
|
-
this.store!.get('data').delete('wrapped_cursor_data'),
|
|
156
|
-
this.updateStore('data', (data: Map<string, unknown>) => data.get('wrapped_cursor_data')),
|
|
157
|
-
] as [Map<string, unknown>, Cursor];
|
|
158
|
-
}
|
|
159
|
-
clearData() {
|
|
160
|
-
return this.updateStore('data', () => Map());
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
setMeta(meta: {}) {
|
|
164
|
-
return this.updateStore((store: CursorStore) => store.set('meta', jsToMap(meta)));
|
|
165
|
-
}
|
|
166
|
-
mergeMeta(meta: {}) {
|
|
167
|
-
return this.updateStore((store: CursorStore) =>
|
|
168
|
-
store.update('meta', (oldMeta: Map<string, unknown>) => oldMeta.merge(jsToMap(meta))),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// This is a temporary hack to allow cursors to be added to the
|
|
174
|
-
// interface between backend.js and backends without modifying old
|
|
175
|
-
// backends at all. This should be removed in favor of wrapping old
|
|
176
|
-
// backends with a compatibility layer, as part of the backend API
|
|
177
|
-
// refactor.
|
|
178
|
-
export const CURSOR_COMPATIBILITY_SYMBOL = Symbol('cursor key for compatibility with old backends');
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
export const EDITORIAL_WORKFLOW_ERROR = 'EDITORIAL_WORKFLOW_ERROR';
|
|
2
|
-
|
|
3
|
-
export default class EditorialWorkflowError extends Error {
|
|
4
|
-
message: string;
|
|
5
|
-
notUnderEditorialWorkflow: boolean;
|
|
6
|
-
constructor(message: string, notUnderEditorialWorkflow: boolean) {
|
|
7
|
-
super(message);
|
|
8
|
-
this.message = message;
|
|
9
|
-
this.notUnderEditorialWorkflow = notUnderEditorialWorkflow;
|
|
10
|
-
this.name = EDITORIAL_WORKFLOW_ERROR;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import * as api from '../API';
|
|
2
|
-
|
|
3
|
-
describe('Api', () => {
|
|
4
|
-
describe('getPreviewStatus', () => {
|
|
5
|
-
it('should return preview status on matching context', () => {
|
|
6
|
-
expect(api.getPreviewStatus([{ context: 'deploy' }])).toEqual({ context: 'deploy' });
|
|
7
|
-
});
|
|
8
|
-
|
|
9
|
-
it('should return undefined on matching context', () => {
|
|
10
|
-
expect(api.getPreviewStatus([{ context: 'other' }])).toBeUndefined();
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
});
|