@jungvonmatt/contentful-ssg 1.4.0 → 1.7.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 +32 -0
- package/dist/cli.js +7 -6
- package/dist/index.js +19 -10
- package/dist/lib/config.js +1 -1
- package/dist/lib/error.d.ts +4 -0
- package/dist/lib/error.js +8 -0
- package/dist/lib/stats.d.ts +1 -1
- package/dist/lib/stats.js +3 -3
- package/dist/lib/utils.d.ts +1 -0
- package/dist/lib/utils.js +74 -0
- package/dist/types.d.ts +7 -0
- package/package.json +3 -2
- package/src/cli.ts +8 -7
- package/src/index.test.ts +58 -0
- package/src/index.ts +29 -12
- package/src/lib/config.ts +1 -1
- package/src/lib/error.ts +10 -0
- package/src/lib/stats.ts +5 -4
- package/src/lib/utils.test.ts +168 -6
- package/src/lib/utils.ts +99 -0
- package/src/types.ts +9 -0
package/README.md
CHANGED
|
@@ -209,6 +209,33 @@ Get values from linked pages to e.g. build an url out of parent slugs.
|
|
|
209
209
|
|
|
210
210
|
The same as collectValues just without the value from the current entry
|
|
211
211
|
|
|
212
|
+
###### waitFor
|
|
213
|
+
|
|
214
|
+
Wait for specific entry to be transformed.
|
|
215
|
+
Be aware that this can lead to deadlocks when you're awaiting something which
|
|
216
|
+
itself is waiting for the current entry to be transformed.
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
{
|
|
220
|
+
transform: (context) => {
|
|
221
|
+
const { utils } = context;
|
|
222
|
+
try {
|
|
223
|
+
// You can overwrite the default wait timeout of 20000ms using the second parameter
|
|
224
|
+
const linkedContext = await utils.waitFor('<contentful-id>', 5000);
|
|
225
|
+
|
|
226
|
+
// Do something usefull with the transformed data
|
|
227
|
+
// which you can't do with context.entryMap.get('<contentful-id>')
|
|
228
|
+
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// Entry isn't available, the transform method for the entry throws an error
|
|
231
|
+
// or we encountered a cyclic dependency
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { ...content };
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
212
239
|
### fetch
|
|
213
240
|
|
|
214
241
|
Fetch all content entries and store them as yaml in the configured directory
|
|
@@ -217,6 +244,11 @@ Fetch all content entries and store them as yaml in the configured directory
|
|
|
217
244
|
npx cssg fetch
|
|
218
245
|
```
|
|
219
246
|
|
|
247
|
+
To see all available command line options call
|
|
248
|
+
```bash
|
|
249
|
+
npx cssg help fetch
|
|
250
|
+
```
|
|
251
|
+
|
|
220
252
|
## Example configuration
|
|
221
253
|
|
|
222
254
|
### Grow
|
package/dist/cli.js
CHANGED
|
@@ -14,10 +14,10 @@ import { getConfig, getEnvironmentConfig } from './lib/config.js';
|
|
|
14
14
|
import { run } from './index.js';
|
|
15
15
|
const env = dotenv.config();
|
|
16
16
|
dotenvExpand(env);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const parseFetchArgs = (cmd) => ({
|
|
18
|
+
preview: cmd.preview,
|
|
19
|
+
verbose: cmd.verbose,
|
|
20
|
+
ignoreErrors: cmd.ignoreErrors,
|
|
21
21
|
});
|
|
22
22
|
const errorHandler = (error, silence) => {
|
|
23
23
|
if (!silence) {
|
|
@@ -37,7 +37,7 @@ program
|
|
|
37
37
|
.option('--typescript', 'Initialize typescript config')
|
|
38
38
|
.action(actionRunner(async (cmd) => {
|
|
39
39
|
const useTypescript = Boolean(cmd?.typescript ?? false);
|
|
40
|
-
const config = await getConfig(
|
|
40
|
+
const config = await getConfig();
|
|
41
41
|
const verified = await askAll(config);
|
|
42
42
|
const environmentConfig = getEnvironmentConfig();
|
|
43
43
|
const filePath = path.join(process.cwd(), `contentful-ssg.config.${useTypescript ? 'ts' : 'js'}`);
|
|
@@ -88,8 +88,9 @@ program
|
|
|
88
88
|
.description('Fetch content objects')
|
|
89
89
|
.option('-p, --preview', 'Fetch with preview mode')
|
|
90
90
|
.option('-v, --verbose', 'Verbose output')
|
|
91
|
+
.option('--ignore-errors', 'No error return code when transform has errors')
|
|
91
92
|
.action(actionRunner(async (cmd) => {
|
|
92
|
-
const config = await getConfig(
|
|
93
|
+
const config = await getConfig(parseFetchArgs(cmd || {}));
|
|
93
94
|
const verified = await askMissing(config);
|
|
94
95
|
return run(verified);
|
|
95
96
|
}));
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Listr from 'listr';
|
|
2
|
+
import { BehaviorSubject } from 'rxjs';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import { getContentTypeId, getContentId } from './lib/contentful.js';
|
|
4
5
|
import { setup } from './tasks/setup.js';
|
|
@@ -6,7 +7,7 @@ import { fetch } from './tasks/fetch.js';
|
|
|
6
7
|
import { localize } from './tasks/localize.js';
|
|
7
8
|
import { transform } from './tasks/transform.js';
|
|
8
9
|
import { write } from './tasks/write.js';
|
|
9
|
-
import { collectParentValues, collectValues } from './lib/utils.js';
|
|
10
|
+
import { collectParentValues, collectValues, waitFor } from './lib/utils.js';
|
|
10
11
|
import { ValidationError } from './lib/error.js';
|
|
11
12
|
class CustomListrRenderer {
|
|
12
13
|
_tasks;
|
|
@@ -54,10 +55,7 @@ export const run = async (config) => {
|
|
|
54
55
|
{
|
|
55
56
|
title: 'Before Hook',
|
|
56
57
|
skip: (ctx) => !ctx.hooks.has('before'),
|
|
57
|
-
task: async (ctx) =>
|
|
58
|
-
const result = await ctx.hooks.before();
|
|
59
|
-
ctx = { ...ctx, ...(result || {}) };
|
|
60
|
-
},
|
|
58
|
+
task: async (ctx) => ctx.hooks.before(),
|
|
61
59
|
},
|
|
62
60
|
{
|
|
63
61
|
title: 'Writing files',
|
|
@@ -66,6 +64,8 @@ export const run = async (config) => {
|
|
|
66
64
|
const tasks = locales.map((locale) => ({
|
|
67
65
|
title: `${locale.code}`,
|
|
68
66
|
task: async () => {
|
|
67
|
+
const subject = new BehaviorSubject(null);
|
|
68
|
+
const observable = subject.asObservable();
|
|
69
69
|
const data = ctx.localized.get(locale.code);
|
|
70
70
|
const { entries = [] } = data || {};
|
|
71
71
|
const promises = entries.map(async (entry) => {
|
|
@@ -74,6 +74,7 @@ export const run = async (config) => {
|
|
|
74
74
|
const utils = {
|
|
75
75
|
collectValues: collectValues({ ...data, entry }),
|
|
76
76
|
collectParentValues: collectParentValues({ ...data, entry }),
|
|
77
|
+
waitFor: waitFor({ ...data, entry, observable }),
|
|
77
78
|
};
|
|
78
79
|
const transformContext = {
|
|
79
80
|
...data,
|
|
@@ -82,9 +83,11 @@ export const run = async (config) => {
|
|
|
82
83
|
entry,
|
|
83
84
|
locale,
|
|
84
85
|
utils,
|
|
86
|
+
observable,
|
|
85
87
|
};
|
|
86
88
|
try {
|
|
87
89
|
const content = await transform(transformContext, ctx, config);
|
|
90
|
+
subject.next({ ...transformContext, content });
|
|
88
91
|
if (typeof content === 'undefined') {
|
|
89
92
|
return;
|
|
90
93
|
}
|
|
@@ -92,10 +95,16 @@ export const run = async (config) => {
|
|
|
92
95
|
ctx.stats.addSuccess(transformContext);
|
|
93
96
|
}
|
|
94
97
|
catch (error) {
|
|
98
|
+
if (error instanceof Error) {
|
|
99
|
+
subject.next({ ...transformContext, error });
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
subject.next({ ...transformContext, error: new Error(`${error}`) });
|
|
103
|
+
}
|
|
95
104
|
if (error instanceof ValidationError) {
|
|
96
105
|
ctx.stats.addSkipped(transformContext, error);
|
|
97
106
|
}
|
|
98
|
-
else
|
|
107
|
+
else {
|
|
99
108
|
ctx.stats.addError(transformContext, error);
|
|
100
109
|
}
|
|
101
110
|
}
|
|
@@ -109,10 +118,7 @@ export const run = async (config) => {
|
|
|
109
118
|
{
|
|
110
119
|
title: 'After Hook',
|
|
111
120
|
skip: (ctx) => !ctx.hooks.has('after'),
|
|
112
|
-
task: async (ctx) =>
|
|
113
|
-
const result = await ctx.hooks.after();
|
|
114
|
-
ctx = { ...ctx, ...(result || {}) };
|
|
115
|
-
},
|
|
121
|
+
task: async (ctx) => ctx.hooks.after(),
|
|
116
122
|
},
|
|
117
123
|
{
|
|
118
124
|
title: 'Cleanup',
|
|
@@ -122,4 +128,7 @@ export const run = async (config) => {
|
|
|
122
128
|
const ctx = await tasks.run();
|
|
123
129
|
await ctx.stats.print();
|
|
124
130
|
console.log('\n---------------------------------------------');
|
|
131
|
+
if (ctx.stats.errors?.length && !config.ignoreErrors) {
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
125
134
|
};
|
package/dist/lib/config.js
CHANGED
|
@@ -80,7 +80,7 @@ export const getEnvironmentConfig = (strict = true) => removeEmpty({
|
|
|
80
80
|
previewAccessToken: process.env.CONTENTFUL_PREVIEW_TOKEN,
|
|
81
81
|
accessToken: process.env.CONTENTFUL_DELIVERY_TOKEN,
|
|
82
82
|
}, strict);
|
|
83
|
-
export const getConfig = async (args) => {
|
|
83
|
+
export const getConfig = async (args = {}) => {
|
|
84
84
|
const defaultOptions = {
|
|
85
85
|
environmentId: 'master',
|
|
86
86
|
host: 'api.contentful.com',
|
package/dist/lib/error.d.ts
CHANGED
package/dist/lib/error.js
CHANGED
|
@@ -15,3 +15,11 @@ export class ValidationError extends Error {
|
|
|
15
15
|
this.name = 'ValidationError';
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
export class WrappedError extends Error {
|
|
19
|
+
originalError;
|
|
20
|
+
constructor(message, error) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.originalError = error;
|
|
23
|
+
this.name = 'WrappedError';
|
|
24
|
+
}
|
|
25
|
+
}
|
package/dist/lib/stats.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare class Stats {
|
|
|
12
12
|
contentTypeId: string;
|
|
13
13
|
};
|
|
14
14
|
addSuccess(context: TransformContext, message?: string): void;
|
|
15
|
-
addError(context: TransformContext, error:
|
|
15
|
+
addError(context: TransformContext, error: unknown): void;
|
|
16
16
|
addSkipped(context: TransformContext, error: ValidationError): void;
|
|
17
17
|
print(): Promise<void>;
|
|
18
18
|
}
|
package/dist/lib/stats.js
CHANGED
|
@@ -21,11 +21,11 @@ export class Stats {
|
|
|
21
21
|
this.success.push({ message, ...this.toEntry(context) });
|
|
22
22
|
}
|
|
23
23
|
addError(context, error) {
|
|
24
|
-
if (
|
|
25
|
-
this.errors.push({ error
|
|
24
|
+
if (error instanceof Error) {
|
|
25
|
+
this.errors.push({ error, ...this.toEntry(context) });
|
|
26
26
|
}
|
|
27
27
|
else {
|
|
28
|
-
this.errors.push({ error, ...this.toEntry(context) });
|
|
28
|
+
this.errors.push({ error: new Error(`${error}`), ...this.toEntry(context) });
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
addSkipped(context, error) {
|
package/dist/lib/utils.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ import type { CollectOptions, Entry, TransformContext } from '../types.js';
|
|
|
2
2
|
export declare const collectValues: (transformContext: Pick<TransformContext, 'entry' | 'entryMap'>) => (key: any, options?: CollectOptions) => any[];
|
|
3
3
|
export declare const collectParentValues: (transformContext: Pick<TransformContext, 'entry' | 'entryMap'>) => (key: any, options?: CollectOptions) => any[];
|
|
4
4
|
export declare const collect: <T = unknown>(entry: Entry, entryMap: Map<string, Entry>, options: CollectOptions) => T[];
|
|
5
|
+
export declare const waitFor: (transformContext: Pick<TransformContext, 'entry' | 'observable' | 'entryMap'>) => (id: string, waitTimeout?: number) => Promise<unknown>;
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import { getContentId, getContentTypeId } from './contentful.js';
|
|
2
|
+
import { ReplaySubject, map, distinct, filter } from 'rxjs';
|
|
1
3
|
import dlv from 'dlv';
|
|
4
|
+
import { WrappedError } from './error.js';
|
|
5
|
+
const DEFAULT_WAIT_TIMEOUT = 20000;
|
|
2
6
|
export const collectValues = (transformContext) => (key, options) => {
|
|
3
7
|
const { entry: defaultEntry, entryMap: defaultEntryMap } = transformContext;
|
|
4
8
|
const { getNextId, linkField, entry = defaultEntry, entryMap = defaultEntryMap, } = options || {};
|
|
@@ -29,3 +33,73 @@ export const collect = (entry, entryMap, options) => {
|
|
|
29
33
|
}
|
|
30
34
|
return [value];
|
|
31
35
|
};
|
|
36
|
+
const dependencies = {};
|
|
37
|
+
const buildDependencies = (v) => {
|
|
38
|
+
if ((dependencies?.[v.source] ?? []).includes(v.dest)) {
|
|
39
|
+
return dependencies;
|
|
40
|
+
}
|
|
41
|
+
if (dependencies[v.source]) {
|
|
42
|
+
dependencies[v.source] = [...dependencies[v.source], v.dest];
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
dependencies[v.source] = [v.dest];
|
|
46
|
+
}
|
|
47
|
+
Object.entries({ ...dependencies })
|
|
48
|
+
.filter(([, value]) => value.includes(v.source))
|
|
49
|
+
.forEach(([key, value]) => {
|
|
50
|
+
dependencies[key] = [...new Set([...value, v.dest, ...(dependencies[v.dest] || [])])];
|
|
51
|
+
});
|
|
52
|
+
return dependencies;
|
|
53
|
+
};
|
|
54
|
+
const waitForSubject = new ReplaySubject();
|
|
55
|
+
const cyclicErrorObservable = waitForSubject
|
|
56
|
+
.asObservable()
|
|
57
|
+
.pipe(map((value) => buildDependencies(value)));
|
|
58
|
+
export const waitFor = (transformContext) => {
|
|
59
|
+
const sourceEntry = transformContext.entry;
|
|
60
|
+
const sourceId = getContentId(sourceEntry);
|
|
61
|
+
const sourceContentTypeId = getContentTypeId(sourceEntry);
|
|
62
|
+
const source = `${sourceId} (${sourceContentTypeId})`;
|
|
63
|
+
return async (id, waitTimeout = DEFAULT_WAIT_TIMEOUT) => {
|
|
64
|
+
if (sourceId === id) {
|
|
65
|
+
throw new Error(`Can't wait for yourself.
|
|
66
|
+
Entry ${source} waiting for ${source}.`);
|
|
67
|
+
}
|
|
68
|
+
waitForSubject.next({ source: getContentId(sourceEntry), dest: id });
|
|
69
|
+
const destEntry = transformContext.entryMap.get(id);
|
|
70
|
+
const destId = getContentId(destEntry);
|
|
71
|
+
const destContentTypeId = getContentTypeId(destEntry);
|
|
72
|
+
const dest = `${destId} (${destContentTypeId})`;
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
const timeout = setTimeout(() => {
|
|
75
|
+
reject(new Error(`Exceeded timeout of ${waitTimeout} ms while waiting for entry ${id} to complete.
|
|
76
|
+
Entry ${source} waiting for ${dest}.`));
|
|
77
|
+
}, waitTimeout);
|
|
78
|
+
if (transformContext.entryMap.has(id)) {
|
|
79
|
+
transformContext.observable
|
|
80
|
+
.pipe(filter((ctx) => ctx?.entry?.sys?.id === id))
|
|
81
|
+
.subscribe((value) => {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
if (value.error) {
|
|
84
|
+
reject(new WrappedError(`Awaited entry ${dest} errored`, value.error));
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
resolve(value);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
cyclicErrorObservable
|
|
91
|
+
.pipe(filter((v) => v?.[sourceId]?.includes(sourceId)))
|
|
92
|
+
.pipe(distinct())
|
|
93
|
+
.subscribe((v) => {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
const deps = v?.[sourceId] ?? [];
|
|
96
|
+
reject(new Error(`Found cyclic dependency in ${source}: ${[sourceId, ...deps].join(' -> ')}`));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
clearTimeout(timeout);
|
|
101
|
+
reject(new Error(`No entry with id "${id}" available`));
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Options } from '@contentful/rich-text-html-renderer';
|
|
2
2
|
import type { Document } from '@contentful/rich-text-types';
|
|
3
|
+
import type { Observable } from 'rxjs';
|
|
3
4
|
import type { QueryOptions, CollectionProp } from 'contentful-management/types';
|
|
4
5
|
import type { EntryFields, Locale as ContentfulLocale, Asset as ContentfulAsset, Field, Entry as ContentfulEntry, ContentType as ContentfulContentType } from 'contentful';
|
|
5
6
|
import type { ListrTaskObject } from 'listr';
|
|
@@ -50,6 +51,7 @@ export declare type Config = Partial<ContentfulConfig> & Hooks & {
|
|
|
50
51
|
directory: string;
|
|
51
52
|
managedDirectories?: string[];
|
|
52
53
|
verbose?: boolean;
|
|
54
|
+
ignoreErrors?: boolean;
|
|
53
55
|
plugins?: Array<[string, KeyValueMap] | PluginInfo | string>;
|
|
54
56
|
resolvedPlugins?: Hooks[];
|
|
55
57
|
preset?: string;
|
|
@@ -130,6 +132,7 @@ export declare type Task = ListrTaskObject<RuntimeContext>;
|
|
|
130
132
|
export interface TransformHelper {
|
|
131
133
|
collectValues: <T>(key: any, options?: CollectOptions) => T[];
|
|
132
134
|
collectParentValues: <T>(key: any, options?: CollectOptions) => T[];
|
|
135
|
+
waitFor: (id: string, waitTimeout?: number) => Promise<ObservableContext>;
|
|
133
136
|
}
|
|
134
137
|
export declare type TransformContext = LocalizedContent & {
|
|
135
138
|
[x: string]: any;
|
|
@@ -144,7 +147,11 @@ export declare type TransformContext = LocalizedContent & {
|
|
|
144
147
|
fieldSettings?: Field;
|
|
145
148
|
requiredFields?: string[];
|
|
146
149
|
utils: TransformHelper;
|
|
150
|
+
observable: Observable<ObservableContext>;
|
|
147
151
|
};
|
|
152
|
+
export declare type ObservableContext = Readonly<Pick<TransformContext, 'id' | 'contentTypeId' | 'entry' | 'content' | 'locale'> & {
|
|
153
|
+
error?: Error;
|
|
154
|
+
}>;
|
|
148
155
|
export interface Ignore {
|
|
149
156
|
add(pattern: string | Ignore | string[] | Ignore[]): Ignore;
|
|
150
157
|
filter(paths: string[]): string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungvonmatt/contentful-ssg",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
"micromatch": "^4.0.4",
|
|
119
119
|
"prettier": "^2.4.1",
|
|
120
120
|
"read-pkg-up": "^9.0.0",
|
|
121
|
+
"rxjs": "^7.5.5",
|
|
121
122
|
"slash": "^4.0.0",
|
|
122
123
|
"snake-case": "^3.0.4",
|
|
123
124
|
"tempy": "^2.0.0",
|
|
@@ -155,5 +156,5 @@
|
|
|
155
156
|
"module": "es2020"
|
|
156
157
|
}
|
|
157
158
|
},
|
|
158
|
-
"gitHead": "
|
|
159
|
+
"gitHead": "441c4188533375574d0bd461b3e043ba0c7d2655"
|
|
159
160
|
}
|
package/src/cli.ts
CHANGED
|
@@ -15,15 +15,15 @@ import { omitKeys } from './lib/object.js';
|
|
|
15
15
|
|
|
16
16
|
import { getConfig, getEnvironmentConfig } from './lib/config.js';
|
|
17
17
|
import { run } from './index.js';
|
|
18
|
-
import { ContentfulConfig } from './types.js';
|
|
18
|
+
import { Config, ContentfulConfig } from './types.js';
|
|
19
19
|
|
|
20
20
|
const env = dotenv.config();
|
|
21
21
|
dotenvExpand(env);
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
const parseFetchArgs = (cmd): Partial<Config> => ({
|
|
24
|
+
preview: cmd.preview as boolean,
|
|
25
|
+
verbose: cmd.verbose as boolean,
|
|
26
|
+
ignoreErrors: cmd.ignoreErrors as boolean,
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
type CommandError = Error & {
|
|
@@ -54,7 +54,7 @@ program
|
|
|
54
54
|
.action(
|
|
55
55
|
actionRunner(async (cmd) => {
|
|
56
56
|
const useTypescript = Boolean(cmd?.typescript ?? false);
|
|
57
|
-
const config = await getConfig(
|
|
57
|
+
const config = await getConfig();
|
|
58
58
|
const verified = await askAll(config);
|
|
59
59
|
|
|
60
60
|
const environmentConfig = getEnvironmentConfig();
|
|
@@ -148,9 +148,10 @@ program
|
|
|
148
148
|
.description('Fetch content objects')
|
|
149
149
|
.option('-p, --preview', 'Fetch with preview mode')
|
|
150
150
|
.option('-v, --verbose', 'Verbose output')
|
|
151
|
+
.option('--ignore-errors', 'No error return code when transform has errors')
|
|
151
152
|
.action(
|
|
152
153
|
actionRunner(async (cmd) => {
|
|
153
|
-
const config = await getConfig(
|
|
154
|
+
const config = await getConfig(parseFetchArgs(cmd || {}));
|
|
154
155
|
const verified = await askMissing(config);
|
|
155
156
|
|
|
156
157
|
return run(verified);
|
package/src/index.test.ts
CHANGED
|
@@ -86,4 +86,62 @@ describe('Run', () => {
|
|
|
86
86
|
expect(output).toMatch(`${chalk.cyan(0)} entries skipped due to validation issues`);
|
|
87
87
|
expect(output).toMatch(`${chalk.red(0)} errors`);
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
test('fails on exception before/after', async () => {
|
|
91
|
+
console.log = jest.fn();
|
|
92
|
+
const mockError = jest.fn().mockImplementation(() => {
|
|
93
|
+
throw new Error();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await expect(async () => {
|
|
97
|
+
await run({
|
|
98
|
+
directory: 'test',
|
|
99
|
+
before: mockError,
|
|
100
|
+
});
|
|
101
|
+
}).rejects.toThrowError();
|
|
102
|
+
|
|
103
|
+
await expect(async () => {
|
|
104
|
+
await run({
|
|
105
|
+
directory: 'test',
|
|
106
|
+
after: mockError,
|
|
107
|
+
});
|
|
108
|
+
}).rejects.toThrowError();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('fails on transform exception', async () => {
|
|
112
|
+
console.log = jest.fn();
|
|
113
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation((number) => {
|
|
114
|
+
throw new Error('process.exit: ' + number);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await expect(async () => {
|
|
118
|
+
await run({
|
|
119
|
+
directory: 'test',
|
|
120
|
+
transform: async () => {
|
|
121
|
+
throw new Error();
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}).rejects.toThrowError();
|
|
125
|
+
|
|
126
|
+
expect(mockExit).toHaveBeenCalledWith(1);
|
|
127
|
+
mockExit.mockRestore();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('does not fail on transform exception with ignoreErrors option', async () => {
|
|
131
|
+
console.log = jest.fn();
|
|
132
|
+
const mockExit = jest.spyOn(process, 'exit').mockImplementation((number) => {
|
|
133
|
+
throw new Error('process.exit: ' + number);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await run({
|
|
137
|
+
directory: 'test',
|
|
138
|
+
ignoreErrors: true,
|
|
139
|
+
transform: async () => {
|
|
140
|
+
throw new Error();
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(mockExit).toBeCalledTimes(0);
|
|
145
|
+
mockExit.mockRestore();
|
|
146
|
+
});
|
|
89
147
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
Config,
|
|
3
|
+
ObservableContext,
|
|
4
|
+
RuntimeContext,
|
|
5
|
+
Task,
|
|
6
|
+
TransformContext,
|
|
7
|
+
TransformHelper,
|
|
8
|
+
} from './types.js';
|
|
2
9
|
import Listr from 'listr';
|
|
10
|
+
import { BehaviorSubject } from 'rxjs';
|
|
3
11
|
import chalk from 'chalk';
|
|
4
12
|
import { getContentTypeId, getContentId } from './lib/contentful.js';
|
|
5
13
|
import { setup } from './tasks/setup.js';
|
|
@@ -7,7 +15,7 @@ import { fetch } from './tasks/fetch.js';
|
|
|
7
15
|
import { localize } from './tasks/localize.js';
|
|
8
16
|
import { transform } from './tasks/transform.js';
|
|
9
17
|
import { write } from './tasks/write.js';
|
|
10
|
-
import { collectParentValues, collectValues } from './lib/utils.js';
|
|
18
|
+
import { collectParentValues, collectValues, waitFor } from './lib/utils.js';
|
|
11
19
|
import { ValidationError } from './lib/error.js';
|
|
12
20
|
|
|
13
21
|
/**
|
|
@@ -73,10 +81,7 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
73
81
|
{
|
|
74
82
|
title: 'Before Hook',
|
|
75
83
|
skip: (ctx) => !ctx.hooks.has('before'),
|
|
76
|
-
task: async (ctx) =>
|
|
77
|
-
const result = await ctx.hooks.before();
|
|
78
|
-
ctx = { ...ctx, ...(result || {}) };
|
|
79
|
-
},
|
|
84
|
+
task: async (ctx) => ctx.hooks.before(),
|
|
80
85
|
},
|
|
81
86
|
{
|
|
82
87
|
title: 'Writing files',
|
|
@@ -86,6 +91,8 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
86
91
|
const tasks = locales.map((locale) => ({
|
|
87
92
|
title: `${locale.code}`,
|
|
88
93
|
task: async () => {
|
|
94
|
+
const subject = new BehaviorSubject<ObservableContext>(null);
|
|
95
|
+
const observable = subject.asObservable();
|
|
89
96
|
const data = ctx.localized.get(locale.code);
|
|
90
97
|
const { entries = [] } = data || {};
|
|
91
98
|
|
|
@@ -97,6 +104,7 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
97
104
|
const utils = {
|
|
98
105
|
collectValues: collectValues({ ...data, entry }),
|
|
99
106
|
collectParentValues: collectParentValues({ ...data, entry }),
|
|
107
|
+
waitFor: waitFor({ ...data, entry, observable }),
|
|
100
108
|
} as TransformHelper;
|
|
101
109
|
|
|
102
110
|
const transformContext: TransformContext = {
|
|
@@ -106,10 +114,12 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
106
114
|
entry,
|
|
107
115
|
locale,
|
|
108
116
|
utils,
|
|
117
|
+
observable,
|
|
109
118
|
};
|
|
110
119
|
|
|
111
120
|
try {
|
|
112
121
|
const content = await transform(transformContext, ctx, config);
|
|
122
|
+
subject.next({ ...transformContext, content });
|
|
113
123
|
|
|
114
124
|
if (typeof content === 'undefined') {
|
|
115
125
|
return;
|
|
@@ -118,9 +128,16 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
118
128
|
await write({ ...transformContext, content }, ctx, config);
|
|
119
129
|
ctx.stats.addSuccess(transformContext);
|
|
120
130
|
} catch (error: unknown) {
|
|
131
|
+
if (error instanceof Error) {
|
|
132
|
+
subject.next({ ...transformContext, error });
|
|
133
|
+
} else {
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
|
|
135
|
+
subject.next({ ...transformContext, error: new Error(`${error}`) });
|
|
136
|
+
}
|
|
137
|
+
|
|
121
138
|
if (error instanceof ValidationError) {
|
|
122
139
|
ctx.stats.addSkipped(transformContext, error);
|
|
123
|
-
} else
|
|
140
|
+
} else {
|
|
124
141
|
ctx.stats.addError(transformContext, error);
|
|
125
142
|
}
|
|
126
143
|
}
|
|
@@ -135,12 +152,8 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
135
152
|
{
|
|
136
153
|
title: 'After Hook',
|
|
137
154
|
skip: (ctx) => !ctx.hooks.has('after'),
|
|
138
|
-
task: async (ctx) =>
|
|
139
|
-
const result = await ctx.hooks.after();
|
|
140
|
-
ctx = { ...ctx, ...(result || {}) };
|
|
141
|
-
},
|
|
155
|
+
task: async (ctx) => ctx.hooks.after(),
|
|
142
156
|
},
|
|
143
|
-
|
|
144
157
|
{
|
|
145
158
|
title: 'Cleanup',
|
|
146
159
|
task: async (ctx) => ctx.fileManager.cleanup(),
|
|
@@ -152,4 +165,8 @@ export const run = async (config: Config): Promise<void> => {
|
|
|
152
165
|
const ctx = await tasks.run();
|
|
153
166
|
await ctx.stats.print();
|
|
154
167
|
console.log('\n---------------------------------------------');
|
|
168
|
+
|
|
169
|
+
if (ctx.stats.errors?.length && !config.ignoreErrors) {
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
155
172
|
};
|
package/src/lib/config.ts
CHANGED
|
@@ -122,7 +122,7 @@ export const getEnvironmentConfig = (strict = true): ContentfulConfig =>
|
|
|
122
122
|
* Get configuration
|
|
123
123
|
* @param {Object} args
|
|
124
124
|
*/
|
|
125
|
-
export const getConfig = async (args
|
|
125
|
+
export const getConfig = async (args: Partial<Config> = {}): Promise<Config> => {
|
|
126
126
|
const defaultOptions: Config = {
|
|
127
127
|
environmentId: 'master',
|
|
128
128
|
host: 'api.contentful.com',
|
package/src/lib/error.ts
CHANGED
|
@@ -17,3 +17,13 @@ export class ValidationError extends Error {
|
|
|
17
17
|
this.name = 'ValidationError';
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export class WrappedError extends Error {
|
|
22
|
+
originalError: unknown;
|
|
23
|
+
|
|
24
|
+
constructor(message: string, error: unknown) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.originalError = error;
|
|
27
|
+
this.name = 'WrappedError';
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/lib/stats.ts
CHANGED
|
@@ -28,11 +28,12 @@ export class Stats {
|
|
|
28
28
|
this.success.push({ message, ...this.toEntry(context) });
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
addError(context: TransformContext, error:
|
|
32
|
-
if (
|
|
33
|
-
this.errors.push({ error: new Error(error), ...this.toEntry(context) });
|
|
34
|
-
} else {
|
|
31
|
+
addError(context: TransformContext, error: unknown): void {
|
|
32
|
+
if (error instanceof Error) {
|
|
35
33
|
this.errors.push({ error, ...this.toEntry(context) });
|
|
34
|
+
} else {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions
|
|
36
|
+
this.errors.push({ error: new Error(`${error}`), ...this.toEntry(context) });
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
package/src/lib/utils.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Entry, TransformContext } from '../types.js';
|
|
2
|
-
import { collect, collectParentValues, collectValues } from './utils.js';
|
|
2
|
+
import { collect, collectParentValues, collectValues, waitFor } from './utils.js';
|
|
3
|
+
import { BehaviorSubject } from 'rxjs';
|
|
4
|
+
import { WrappedError } from './error.js';
|
|
3
5
|
|
|
4
6
|
const data = new Map([
|
|
5
7
|
['1', { sys: { id: '1' }, fields: { slug: 'a' } }],
|
|
@@ -9,12 +11,66 @@ const data = new Map([
|
|
|
9
11
|
['5', { sys: { id: '5' }, fields: { slug: 'e', parent: '4' } }],
|
|
10
12
|
]) as unknown as Map<string, Entry>;
|
|
11
13
|
|
|
14
|
+
const contentType = { sys: { id: 'test-type' } };
|
|
15
|
+
|
|
12
16
|
const entryMap = new Map([
|
|
13
|
-
['1', { sys: { id: '1' }, fields: { slug: 'a' } }],
|
|
14
|
-
[
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
['1', { sys: { id: '1', contentType }, fields: { slug: 'a' } }],
|
|
18
|
+
[
|
|
19
|
+
'2',
|
|
20
|
+
{
|
|
21
|
+
sys: { id: '2', contentType },
|
|
22
|
+
fields: { slug: 'b', parent: { sys: { id: '1' } }, link: { sys: { id: '1' } } },
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
[
|
|
26
|
+
'3',
|
|
27
|
+
{
|
|
28
|
+
sys: { id: '3', contentType },
|
|
29
|
+
fields: { slug: 'c', parent: { sys: { id: '2' } }, link: { sys: { id: '1' } } },
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
[
|
|
33
|
+
'4',
|
|
34
|
+
{
|
|
35
|
+
sys: { id: '4', contentType },
|
|
36
|
+
fields: { slug: 'd', parent: { sys: { id: '3' } }, link: { sys: { id: '3' } } },
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
'5',
|
|
41
|
+
{
|
|
42
|
+
sys: { id: '5', contentType },
|
|
43
|
+
fields: { slug: 'e', parent: { sys: { id: '4' } }, link: { sys: { id: '3' } } },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
'6',
|
|
48
|
+
{
|
|
49
|
+
sys: { id: '6', contentType },
|
|
50
|
+
fields: { slug: 'f', parent: { sys: { id: '1' } }, link: { sys: { id: '1' } } },
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
'7',
|
|
55
|
+
{
|
|
56
|
+
sys: { id: '7', contentType },
|
|
57
|
+
fields: { slug: 'g', parent: { sys: { id: '2' } }, link: { sys: { id: '1' } } },
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
[
|
|
61
|
+
'8',
|
|
62
|
+
{
|
|
63
|
+
sys: { id: '8', contentType },
|
|
64
|
+
fields: { slug: 'h', parent: { sys: { id: '3' } }, link: { sys: { id: '3' } } },
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
[
|
|
68
|
+
'9',
|
|
69
|
+
{
|
|
70
|
+
sys: { id: '9', contentType },
|
|
71
|
+
fields: { slug: 'i', parent: { sys: { id: '4' } }, link: { sys: { id: '3' } } },
|
|
72
|
+
},
|
|
73
|
+
],
|
|
18
74
|
]) as unknown as Map<string, Entry>;
|
|
19
75
|
|
|
20
76
|
const transformContext = { entryMap } as unknown as TransformContext;
|
|
@@ -82,4 +138,110 @@ describe('Utils', () => {
|
|
|
82
138
|
expect(a).toEqual(['a', 'b', 'c', 'd', 'e']);
|
|
83
139
|
expect(b).toEqual(['e', 'd', 'c', 'b', 'a']);
|
|
84
140
|
});
|
|
141
|
+
|
|
142
|
+
test('waitFor', async () => {
|
|
143
|
+
const subject = new BehaviorSubject<TransformContext>(null);
|
|
144
|
+
const observable = subject.asObservable();
|
|
145
|
+
|
|
146
|
+
// Throw error when waiting for the current entry
|
|
147
|
+
expect(async () => {
|
|
148
|
+
await waitFor({ ...transformContext, entry: entryMap.get('2'), observable })('2');
|
|
149
|
+
}).rejects.toThrowError(/2.*waiting.*2/);
|
|
150
|
+
|
|
151
|
+
// Throw error when waiting for non existing entry
|
|
152
|
+
expect(async () => {
|
|
153
|
+
await waitFor({ ...transformContext, entry: entryMap.get('3'), observable })('10');
|
|
154
|
+
}).rejects.toThrowError('No entry with id "10" available');
|
|
155
|
+
|
|
156
|
+
// // Throw error when waiting timeout is reached
|
|
157
|
+
await expect(async () => {
|
|
158
|
+
await waitFor({ ...transformContext, entry: entryMap.get('1'), observable })('4', 50);
|
|
159
|
+
}).rejects.toThrowError(/Exceeded timeout of 50 ms/);
|
|
160
|
+
|
|
161
|
+
// Mimic 500ms wait time for entry
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
subject.next({ ...transformContext, entry: entryMap.get('4'), observable });
|
|
164
|
+
}, 500);
|
|
165
|
+
|
|
166
|
+
const value = await waitFor({ ...transformContext, entry: entryMap.get('1'), observable })('4');
|
|
167
|
+
expect(value).toEqual({ ...transformContext, entry: entryMap.get('4'), observable });
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('waitFor error', async () => {
|
|
171
|
+
const subject = new BehaviorSubject<TransformContext>(null);
|
|
172
|
+
const observable = subject.asObservable();
|
|
173
|
+
const entry = entryMap.get('3');
|
|
174
|
+
|
|
175
|
+
// Mimic 500ms wait time for entry
|
|
176
|
+
setTimeout(() => {
|
|
177
|
+
subject.next({
|
|
178
|
+
...transformContext,
|
|
179
|
+
entry: entryMap.get('4'),
|
|
180
|
+
observable,
|
|
181
|
+
error: new Error('test error'),
|
|
182
|
+
});
|
|
183
|
+
}, 500);
|
|
184
|
+
|
|
185
|
+
await expect(async () => {
|
|
186
|
+
await waitFor({ ...transformContext, entry, observable })('4');
|
|
187
|
+
}).rejects.toThrowError(WrappedError);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await waitFor({ ...transformContext, entry, observable })('4');
|
|
191
|
+
} catch (error) {
|
|
192
|
+
expect(error).toBeInstanceOf(WrappedError);
|
|
193
|
+
expect(error.message).toMatch('Awaited entry 4 (test-type) errored');
|
|
194
|
+
expect(error.originalError.message).toEqual('test error');
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('detect cyclic dependency', async () => {
|
|
199
|
+
const subject = new BehaviorSubject<TransformContext>(null);
|
|
200
|
+
const observable = subject.asObservable();
|
|
201
|
+
|
|
202
|
+
// let 9 finish regularly
|
|
203
|
+
setTimeout(() => {
|
|
204
|
+
subject.next({
|
|
205
|
+
...transformContext,
|
|
206
|
+
entry: entryMap.get('9'),
|
|
207
|
+
observable,
|
|
208
|
+
});
|
|
209
|
+
}, 100);
|
|
210
|
+
|
|
211
|
+
// mimic behaviour in index.ts
|
|
212
|
+
const waitMock = async (source: string, dest: string, delay: number) => {
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
214
|
+
try {
|
|
215
|
+
await waitFor({ ...transformContext, entry: entryMap.get(source), observable })(dest, 1000);
|
|
216
|
+
subject.next({
|
|
217
|
+
...transformContext,
|
|
218
|
+
entry: entryMap.get(source),
|
|
219
|
+
observable,
|
|
220
|
+
});
|
|
221
|
+
return `SUCCESS ${source}`;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
subject.next({
|
|
224
|
+
...transformContext,
|
|
225
|
+
entry: entryMap.get(source),
|
|
226
|
+
error,
|
|
227
|
+
observable,
|
|
228
|
+
});
|
|
229
|
+
return `${error.message}`;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = await Promise.all([
|
|
234
|
+
waitMock('6', '8', 0),
|
|
235
|
+
waitMock('8', '5', 10),
|
|
236
|
+
waitMock('5', '7', 20),
|
|
237
|
+
waitMock('7', '6', 30),
|
|
238
|
+
waitMock('3', '9', 40),
|
|
239
|
+
]);
|
|
240
|
+
|
|
241
|
+
expect(result[0]).toMatch('Found cyclic dependency in 6 (test-type): 6 -> 8 -> 5 -> 7 -> 6');
|
|
242
|
+
expect(result[1]).toMatch('Found cyclic dependency in 8 (test-type): 8 -> 5 -> 7 -> 6 -> 8');
|
|
243
|
+
expect(result[2]).toMatch('Found cyclic dependency in 5 (test-type): 5 -> 7 -> 6 -> 8 -> 5');
|
|
244
|
+
expect(result[3]).toMatch(/Awaited entry 6 \(test-type\) errored/);
|
|
245
|
+
expect(result[4]).toMatch('SUCCESS 3');
|
|
246
|
+
});
|
|
85
247
|
});
|
package/src/lib/utils.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { CollectOptions, Entry, TransformContext } from '../types.js';
|
|
2
|
+
import { getContentId, getContentTypeId } from './contentful.js';
|
|
3
|
+
import { ReplaySubject, map, distinct, filter } from 'rxjs';
|
|
2
4
|
import dlv from 'dlv';
|
|
5
|
+
import { WrappedError } from './error.js';
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
8
|
+
const DEFAULT_WAIT_TIMEOUT = 20000;
|
|
3
9
|
|
|
4
10
|
export const collectValues =
|
|
5
11
|
(transformContext: Pick<TransformContext, 'entry' | 'entryMap'>) =>
|
|
@@ -55,3 +61,96 @@ export const collect = <T = unknown>(
|
|
|
55
61
|
|
|
56
62
|
return [value];
|
|
57
63
|
};
|
|
64
|
+
|
|
65
|
+
const dependencies: Record<string, string[]> = {};
|
|
66
|
+
|
|
67
|
+
const buildDependencies = (v: { source: string; dest: string }) => {
|
|
68
|
+
if ((dependencies?.[v.source] ?? []).includes(v.dest)) {
|
|
69
|
+
return dependencies;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (dependencies[v.source]) {
|
|
73
|
+
dependencies[v.source] = [...dependencies[v.source], v.dest];
|
|
74
|
+
} else {
|
|
75
|
+
dependencies[v.source] = [v.dest];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Object.entries({ ...dependencies })
|
|
79
|
+
.filter(([, value]) => value.includes(v.source))
|
|
80
|
+
.forEach(([key, value]) => {
|
|
81
|
+
dependencies[key] = [...new Set([...value, v.dest, ...(dependencies[v.dest] || [])])];
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return dependencies;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const waitForSubject = new ReplaySubject<{ source: string; dest: string }>();
|
|
88
|
+
|
|
89
|
+
const cyclicErrorObservable = waitForSubject
|
|
90
|
+
.asObservable()
|
|
91
|
+
.pipe(map((value) => buildDependencies(value)));
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Wait for entry to be transformed
|
|
95
|
+
*/
|
|
96
|
+
export const waitFor = (
|
|
97
|
+
transformContext: Pick<TransformContext, 'entry' | 'observable' | 'entryMap'>
|
|
98
|
+
) => {
|
|
99
|
+
const sourceEntry = transformContext.entry;
|
|
100
|
+
const sourceId = getContentId(sourceEntry);
|
|
101
|
+
const sourceContentTypeId = getContentTypeId(sourceEntry);
|
|
102
|
+
const source = `${sourceId} (${sourceContentTypeId})`;
|
|
103
|
+
|
|
104
|
+
return async (id: string, waitTimeout = DEFAULT_WAIT_TIMEOUT) => {
|
|
105
|
+
// Make sure we don't try to wait for the current entry
|
|
106
|
+
if (sourceId === id) {
|
|
107
|
+
throw new Error(`Can't wait for yourself.
|
|
108
|
+
Entry ${source} waiting for ${source}.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
waitForSubject.next({ source: getContentId(sourceEntry), dest: id });
|
|
112
|
+
const destEntry = transformContext.entryMap.get(id);
|
|
113
|
+
const destId = getContentId(destEntry);
|
|
114
|
+
const destContentTypeId = getContentTypeId(destEntry);
|
|
115
|
+
const dest = `${destId} (${destContentTypeId})`;
|
|
116
|
+
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
// If anything goes wrong, reject promise after waitTimeout
|
|
119
|
+
const timeout = setTimeout(() => {
|
|
120
|
+
reject(
|
|
121
|
+
new Error(
|
|
122
|
+
`Exceeded timeout of ${waitTimeout} ms while waiting for entry ${id} to complete.
|
|
123
|
+
Entry ${source} waiting for ${dest}.`
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
}, waitTimeout);
|
|
127
|
+
|
|
128
|
+
if (transformContext.entryMap.has(id)) {
|
|
129
|
+
transformContext.observable
|
|
130
|
+
.pipe(filter((ctx) => ctx?.entry?.sys?.id === id))
|
|
131
|
+
.subscribe((value) => {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
if (value.error) {
|
|
134
|
+
reject(new WrappedError(`Awaited entry ${dest} errored`, value.error));
|
|
135
|
+
} else {
|
|
136
|
+
resolve(value);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
cyclicErrorObservable
|
|
141
|
+
.pipe(filter((v) => v?.[sourceId]?.includes(sourceId)))
|
|
142
|
+
.pipe(distinct())
|
|
143
|
+
.subscribe((v) => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
const deps = v?.[sourceId] ?? [];
|
|
146
|
+
reject(
|
|
147
|
+
new Error(`Found cyclic dependency in ${source}: ${[sourceId, ...deps].join(' -> ')}`)
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
} else {
|
|
151
|
+
clearTimeout(timeout);
|
|
152
|
+
reject(new Error(`No entry with id "${id}" available`));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
};
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Options } from '@contentful/rich-text-html-renderer';
|
|
2
2
|
import type { Document } from '@contentful/rich-text-types';
|
|
3
|
+
import type { Observable } from 'rxjs';
|
|
3
4
|
import type { QueryOptions, CollectionProp } from 'contentful-management/types';
|
|
4
5
|
|
|
5
6
|
import type {
|
|
@@ -85,6 +86,7 @@ export type Config = Partial<ContentfulConfig> &
|
|
|
85
86
|
directory: string;
|
|
86
87
|
managedDirectories?: string[];
|
|
87
88
|
verbose?: boolean;
|
|
89
|
+
ignoreErrors?: boolean;
|
|
88
90
|
plugins?: Array<[string, KeyValueMap] | PluginInfo | string>;
|
|
89
91
|
resolvedPlugins?: Hooks[];
|
|
90
92
|
preset?: string;
|
|
@@ -201,6 +203,7 @@ export type Task = ListrTaskObject<RuntimeContext>;
|
|
|
201
203
|
export interface TransformHelper {
|
|
202
204
|
collectValues: <T>(key, options?: CollectOptions) => T[];
|
|
203
205
|
collectParentValues: <T>(key, options?: CollectOptions) => T[];
|
|
206
|
+
waitFor: (id: string, waitTimeout?: number) => Promise<ObservableContext>;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
export type TransformContext = LocalizedContent & {
|
|
@@ -216,8 +219,14 @@ export type TransformContext = LocalizedContent & {
|
|
|
216
219
|
fieldSettings?: Field;
|
|
217
220
|
requiredFields?: string[];
|
|
218
221
|
utils: TransformHelper;
|
|
222
|
+
observable: Observable<ObservableContext>;
|
|
219
223
|
};
|
|
220
224
|
|
|
225
|
+
export type ObservableContext = Readonly<
|
|
226
|
+
Pick<TransformContext, 'id' | 'contentTypeId' | 'entry' | 'content' | 'locale'> & {
|
|
227
|
+
error?: Error;
|
|
228
|
+
}
|
|
229
|
+
>;
|
|
221
230
|
export interface Ignore {
|
|
222
231
|
add(pattern: string | Ignore | string[] | Ignore[]): Ignore;
|
|
223
232
|
filter(paths: string[]): string[];
|