@jungvonmatt/contentful-migrations 5.6.2 → 6.0.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 +52 -19
- package/cli.js +105 -53
- package/lib/config.js +53 -144
- package/lib/content.js +2 -2
- package/lib/contentful.js +3 -4
- package/lib/helpers/validation.d.ts +26 -3
- package/lib/helpers/validation.js +113 -23
- package/lib/helpers/validation.test.js +212 -2
- package/lib/migration.js +4 -4
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -28,23 +28,26 @@ This initializes migrations and stores the config values in the `package.json` o
|
|
|
28
28
|
|
|
29
29
|
The default configuration file name is `.migrationsrc`. But since we use [cosmiconfig](https://github.com/davidtheclark/cosmiconfig#cosmiconfig) you can also use the other supported config file formats and name patterns like `migrationsrc.json` or `migrations.config.js`.
|
|
30
30
|
|
|
31
|
-
You can also use any config file path by adding the `-f <path/to/confg>` or `--config-file <path/to/config>` command line argument. The extensions `.json`, `.yaml`, `.yml`, `.js`, or `.cjs` are supported.
|
|
31
|
+
You can also use any config file path by adding the `-f <path/to/confg>` or `--config-file <path/to/config>` command line argument. The extensions `.json`, `.yaml`, `.yml`, `.js`, `.ts`, `.mjs`, or `.cjs` are supported.
|
|
32
32
|
|
|
33
33
|
By specifying the config file path you can use multiple config files for different environments or spaces in your project.
|
|
34
34
|
|
|
35
35
|
#### Configuration values
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
45
|
-
|
|
|
46
|
-
|
|
|
47
|
-
|
|
|
37
|
+
All configuration values can also be set via environment variables
|
|
38
|
+
|
|
39
|
+
| Name | Environment Variable | Default | Description |
|
|
40
|
+
| ---------------------- | ------------------------------------------ | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
41
|
+
| managementToken | `CONTENTFUL_MANAGEMENT_TOKEN` | `undefined` | Contentful Management Token. Just run `npx contentful login` and you're done. |
|
|
42
|
+
| -accessToken- | | `undefined` | Not supported anymore. Please use `managementToken` instead. |
|
|
43
|
+
| spaceId | `CONTENTFUL_SPACE_ID` | `undefined` | Contentful Space id. Will fallback to `process.env.CONTENTFUL_SPACE_ID` if not set. |
|
|
44
|
+
| environmentId | `CONTENTFUL_ENVIRONMENT_ID` | `undefined` | Contentful Environment id. Will fallback to `process.env.CONTENTFUL_ENVIRONMENT_ID` if not set.<br/>If neither `environmentId` nor `CONTENTFUL_ENVIRONMENT_ID` is available we search for environment whose id matches the current git branch |
|
|
45
|
+
| host | `CONTENTFUL_HOST` | `undefined` | Allows configuring the Contentful CLI for EU usage. Will fallback to the global contentful config in your `.contentfulrc.json` |
|
|
46
|
+
| requestBatchSize | `CONTENTFUL_MIGRATIONS_REQUEST_BATCH_SIZE` | `undefined` | The batch size used for loading data from contentful. Contentful uses a default of 100. Use a smaller value when you get `Response size too big` errors (for example caused by very large rich text fields). Be aware that contentful migrations might hide the response size error and only print `The provided space does not exist or you do not have access` |
|
|
47
|
+
| storage | `CONTENTFUL_MIGRATIONS_STORAGE` | `undefined` | We need to keep a hint to the executed migrations inside Contentful. You can choose between **content\* and **tag**. <br/><br/>**Content** will add a new content type to your Contentful environment and stores the state of every migration as content entry (recommended approach) <br/>**tag\*\* Will only store the latest version inside a tag. You need to preserve the right order yourself. When you add a new migration with an older version number it will not be executed. |
|
|
48
|
+
| fieldId | `'CONTENTFUL_MIGRATIONS_FIELD_ID'` | `'migration'` | Id of the tag where the migration version is stored (only used with storage `tag`) |
|
|
49
|
+
| migrationContentTypeId | `CONTENTFUL_MIGRATIONS_CONTENT_TYPE_ID` | `'contentful-migrations'` | Id of the migration content-type (only used with storage `content`) |
|
|
50
|
+
| directory | `CONTENTFUL_MIGRATIONS_DIRECTORY` | `'./migrations'` | Directory where the migration files are stored |
|
|
48
51
|
|
|
49
52
|
<br/>
|
|
50
53
|
<br/>
|
|
@@ -87,7 +90,7 @@ npx migrations generate
|
|
|
87
90
|
|
|
88
91
|
Sometimes you may not want to start a migration from scratch. You can create a new Content type in the [contentful web app](https://www.contentful.com/help/content-modelling-basics/) and import it using the `fetch` command.
|
|
89
92
|
|
|
90
|
-
|
|
93
|
+
_When you want to use the contentful web app to configure your content types you should do so in a separate environment because the migration will fail if the content-type is already present at the time you run the migration script_
|
|
91
94
|
|
|
92
95
|
```bash
|
|
93
96
|
# Generate migration scripts for all content types from the current environment
|
|
@@ -127,7 +130,7 @@ npx migrations execute <path/to/migration.js> -e <environment-id>
|
|
|
127
130
|
## Managing the migration versions stored in contentful
|
|
128
131
|
|
|
129
132
|
Sometimes you may need to manually mark a migration as migrated or not. You can use the `version` command for this.
|
|
130
|
-
|
|
133
|
+
_Use caution when using the version command. If you delete a version from the table and then run the migrate command, that migration version will be executed again._
|
|
131
134
|
|
|
132
135
|
**This command is only available when using the content storage**
|
|
133
136
|
|
|
@@ -178,8 +181,8 @@ npx migrations doc -e <environment> -p <path/to/docs>
|
|
|
178
181
|
`--template`: Use a custom template for docs. `.js` with default export or `.mustache` is allowed<br/>
|
|
179
182
|
`--extension`: Use a custom file extension (default is `.md`)<br/>
|
|
180
183
|
|
|
181
|
-
|
|
182
184
|
## Migration helpers
|
|
185
|
+
|
|
183
186
|
We provide you with a few smaller migration helpers. There aren't many at the moment, but there may be more in the future.
|
|
184
187
|
|
|
185
188
|
To use the helpers you just need to wrap your migration with the provided `withHelpers` function which makes the helpers available as 3rd parameter
|
|
@@ -205,7 +208,7 @@ module.exports = withHelpers(async (migration, context, helpers) => {
|
|
|
205
208
|
|
|
206
209
|
// Add or remove values from "in" validations without knowing all the other elements in the array
|
|
207
210
|
await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value']); // add at the end
|
|
208
|
-
await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value'], { mode: 'sorted'}); // add and sort
|
|
211
|
+
await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value'], { mode: 'sorted' }); // add and sort
|
|
209
212
|
await helpers.validation.removeInValues('contentTypeId', 'fieldId', ['value']);
|
|
210
213
|
await helpers.validation.modifyInValues('contentTypeId', 'fieldId', (existing) => {
|
|
211
214
|
const result = existing.filter((value) => value.startsWith('prefix')); // keep values with prefix
|
|
@@ -213,6 +216,37 @@ module.exports = withHelpers(async (migration, context, helpers) => {
|
|
|
213
216
|
return result; // possible duplicate values are removed afterwards
|
|
214
217
|
});
|
|
215
218
|
|
|
219
|
+
// Add or remove enabled marks for a rich text field without knowing all the active marks
|
|
220
|
+
// See: https://github.com/contentful/rich-text/blob/master/packages/rich-text-types/src/marks.ts
|
|
221
|
+
await helpers.validation.richText.addEnabledMarksValues('contentTypeId', 'fieldId', ['bold']);
|
|
222
|
+
await helpers.validation.richText.removeEnabledMarksValues('contentTypeId', 'fieldId', ['underline']);
|
|
223
|
+
await helpers.validation.richText.modifyEnabledMarksValues('contentTypeId', 'fieldId', (existing) => {
|
|
224
|
+
const result = existing.filter((value) => value !== 'code'); // keep values with prefix
|
|
225
|
+
result.push('italic'); // and add one
|
|
226
|
+
return result; // possible duplicate values are removed afterwards
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Add or remove enabled node types for a rich text field without knowing all the active node types
|
|
230
|
+
// See https://github.com/contentful/rich-text/blob/master/packages/rich-text-types/src/blocks.ts
|
|
231
|
+
// and https://github.com/contentful/rich-text/blob/master/packages/rich-text-types/src/inlines.ts
|
|
232
|
+
await helpers.validation.richText.addEnabledNodeTypeValues('contentTypeId', 'fieldId', ['blockquote']);
|
|
233
|
+
await helpers.validation.richText.removeEnabledNodeTypeValues('contentTypeId', 'fieldId', ['hyperlink']);
|
|
234
|
+
await helpers.validation.richText.modifyEnabledNodeTypeValues('contentTypeId', 'fieldId', (existing) => {
|
|
235
|
+
const result = existing.filter((value) => !value.startsWith('heading-')); // filter out headings like 'heading-1'
|
|
236
|
+
result.push('quote'); // and add one
|
|
237
|
+
return result; // possible duplicate values are removed afterwards
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Add or remove embedded or linked content types for a rich text field without knowing all the allowed content types
|
|
241
|
+
// The possible node types are 'entry-hyperlink', 'embedded-entry-block' and 'embedded-entry-inline'.
|
|
242
|
+
await helpers.validation.richText.addNodeContentTypeValues('contentTypeId', 'fieldId', 'embedded-entry-block', ['a-content-type']);
|
|
243
|
+
await helpers.validation.richText.removeNodeContentTypeValues('contentTypeId', 'fieldId', 'embedded-entry-inline', ['a-content-type']);
|
|
244
|
+
await helpers.validation.richText.modifyNodeContentTypeValues('contentTypeId', 'fieldId', 'entry-hyperlink', (existing) => {
|
|
245
|
+
const result = existing.filter((value) => value.startsWith('t-')); // filter out content types that not start with 't-'
|
|
246
|
+
result.push('t-article'); // and add one
|
|
247
|
+
return result; // possible duplicate values are removed afterwards
|
|
248
|
+
});
|
|
249
|
+
|
|
216
250
|
});
|
|
217
251
|
```
|
|
218
252
|
|
|
@@ -222,11 +256,10 @@ Of course. We appreciate all of our [contributors](https://github.com/jungvonmat
|
|
|
222
256
|
welcome contributions to improve the project further. If you're uncertain whether an addition should be made, feel
|
|
223
257
|
free to open up an issue and we can discuss it.
|
|
224
258
|
|
|
225
|
-
|
|
226
|
-
|
|
227
259
|
## Contributors
|
|
260
|
+
|
|
228
261
|
<a href="https://github.com/jungvonmatt/contentful-migrations/graphs/contributors">
|
|
229
|
-
<img src="https://contrib.rocks/image?repo=jungvonmatt/contentful-migrations" />
|
|
262
|
+
<img alt="Contributor profile pictures" src="https://contrib.rocks/image?repo=jungvonmatt/contentful-migrations" />
|
|
230
263
|
</a>
|
|
231
264
|
|
|
232
265
|
[npm-url]: https://www.npmjs.com/package/@jungvonmatt/contentful-migrations
|
package/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ const { versionDelete, versionAdd } = require('./lib/version');
|
|
|
13
13
|
const { transferContent } = require('./lib/content');
|
|
14
14
|
const { createOfflineDocs } = require('./lib/doc');
|
|
15
15
|
const { createEnvironment, removeEnvironment, resetEnvironment } = require('./lib/environment');
|
|
16
|
-
const { getConfig,
|
|
16
|
+
const { getConfig, confirm, STORAGE_CONTENT, STORAGE_TAG } = require('./lib/config');
|
|
17
17
|
const pkg = require('./package.json');
|
|
18
18
|
|
|
19
19
|
require('dotenv').config();
|
|
@@ -23,7 +23,7 @@ const parseArgs = (cmd) => {
|
|
|
23
23
|
const directory = cmd.path || parent.path;
|
|
24
24
|
return {
|
|
25
25
|
...cmd,
|
|
26
|
-
configFile: cmd.
|
|
26
|
+
configFile: cmd.config,
|
|
27
27
|
environment: cmd.env || parent.env,
|
|
28
28
|
directory: directory ? path.resolve(directory) : undefined,
|
|
29
29
|
sourceEnvironmentId: cmd.sourceEnvironmentId || parent.sourceEnvironmentId,
|
|
@@ -59,37 +59,52 @@ const program = new Command();
|
|
|
59
59
|
program.version(pkg.version);
|
|
60
60
|
program
|
|
61
61
|
.command('init')
|
|
62
|
+
.option('-s, --space-id <space-id>', 'Contentful space id')
|
|
62
63
|
.option('--host <host>', 'Management API host')
|
|
64
|
+
.option('--config <path/to/config>', 'Config file path (disables auto detect)')
|
|
65
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
63
66
|
.description('Initialize contentful-migrations')
|
|
64
67
|
.action(
|
|
65
68
|
actionRunner(async (cmd) => {
|
|
66
|
-
const config = await getConfig(parseArgs(cmd || {})
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const config = await getConfig(parseArgs(cmd || {}), [
|
|
70
|
+
'managementToken',
|
|
71
|
+
'spaceId',
|
|
72
|
+
'environmentId',
|
|
73
|
+
'storage',
|
|
74
|
+
'fieldId',
|
|
75
|
+
'migrationContentTypeId',
|
|
76
|
+
'directory',
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const { managementToken, accessToken, environmentId, spaceId, ...rest } = config;
|
|
69
80
|
|
|
70
|
-
if (
|
|
71
|
-
await initializeContentModel(
|
|
72
|
-
await migrateToContentStorage(
|
|
81
|
+
if (config.storage === STORAGE_CONTENT) {
|
|
82
|
+
await initializeContentModel(config);
|
|
83
|
+
await migrateToContentStorage(config);
|
|
73
84
|
}
|
|
74
|
-
if (
|
|
75
|
-
await migrateToTagStorage(
|
|
85
|
+
if (config.storage === STORAGE_TAG) {
|
|
86
|
+
await migrateToTagStorage(config);
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
if (!process.env.CONTENTFUL_SPACE_ID) {
|
|
79
90
|
rest.spaceId = spaceId;
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
const storeConfig = await confirm({ message: 'Do you want to store the configuration?' });
|
|
94
|
+
|
|
95
|
+
if (storeConfig) {
|
|
96
|
+
// try to store in package.json
|
|
97
|
+
const { pkgUp } = await import('pkg-up');
|
|
98
|
+
const localPkg = await pkgUp();
|
|
99
|
+
if (localPkg) {
|
|
100
|
+
const packageJson = await fs.readJson(localPkg);
|
|
101
|
+
rest.directory = path.relative(path.dirname(localPkg), rest.directory);
|
|
102
|
+
packageJson.migrations = rest;
|
|
103
|
+
await fs.outputJson(localPkg, packageJson, { spaces: 2 });
|
|
104
|
+
} else {
|
|
105
|
+
// store in .migrationsrc if no package.json is available
|
|
106
|
+
await fs.outputJson(path.join(process.cwd(), '.migrationsrc'), rest, { spaces: 2 });
|
|
107
|
+
}
|
|
93
108
|
}
|
|
94
109
|
})
|
|
95
110
|
);
|
|
@@ -103,12 +118,17 @@ program
|
|
|
103
118
|
.option('-v, --verbose', 'Verbosity')
|
|
104
119
|
.option('--host <host>', 'Management API host')
|
|
105
120
|
.option('--config <path/to/config>', 'Config file path (disables auto detect)')
|
|
121
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
106
122
|
.description('Generate a new Contentful migration from content type')
|
|
107
123
|
.action(
|
|
108
124
|
actionRunner(async (cmd) => {
|
|
109
|
-
const config = await getConfig(parseArgs(cmd || {})
|
|
110
|
-
|
|
111
|
-
|
|
125
|
+
const config = await getConfig(parseArgs(cmd || {}), [
|
|
126
|
+
'managementToken',
|
|
127
|
+
'spaceId',
|
|
128
|
+
'environmentId',
|
|
129
|
+
'directory',
|
|
130
|
+
]);
|
|
131
|
+
await fetchMigration({ ...config, contentType: cmd.contentType });
|
|
112
132
|
})
|
|
113
133
|
);
|
|
114
134
|
|
|
@@ -120,12 +140,17 @@ program
|
|
|
120
140
|
.option('-v, --verbose', 'Verbosity')
|
|
121
141
|
.option('--host <host>', 'Management API host')
|
|
122
142
|
.option('--config <path/to/config>', 'Config file path (disables auto detect)')
|
|
143
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
123
144
|
.description('Generate a new Contentful migration')
|
|
124
145
|
.action(
|
|
125
146
|
actionRunner(async (cmd) => {
|
|
126
|
-
const config = await getConfig(parseArgs(cmd || {})
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
const config = await getConfig(parseArgs(cmd || {}), [
|
|
148
|
+
'managementToken',
|
|
149
|
+
'spaceId',
|
|
150
|
+
'environmentId',
|
|
151
|
+
'directory',
|
|
152
|
+
]);
|
|
153
|
+
await createMigration(config);
|
|
129
154
|
})
|
|
130
155
|
);
|
|
131
156
|
|
|
@@ -138,21 +163,29 @@ program
|
|
|
138
163
|
.option('-y, --yes', 'Assume "yes" as answer to all prompts and run non-interactively.')
|
|
139
164
|
.option('--host <host>', 'Management API host')
|
|
140
165
|
.option('--config <path/to/config>', 'Config file path (disables auto detect)')
|
|
166
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
141
167
|
.option('--bail', 'Abort execution after first failed migration (default: true)', true)
|
|
142
168
|
.option('--no-bail', 'Ignore failed migrations')
|
|
143
169
|
.description('Execute all unexecuted migrations available.')
|
|
144
170
|
.action(
|
|
145
171
|
actionRunner(async (cmd) => {
|
|
146
|
-
const config = await getConfig(parseArgs(cmd || {})
|
|
147
|
-
|
|
172
|
+
const config = await getConfig(parseArgs(cmd || {}), [
|
|
173
|
+
'managementToken',
|
|
174
|
+
'spaceId',
|
|
175
|
+
'environmentId',
|
|
176
|
+
'storage',
|
|
177
|
+
'fieldId',
|
|
178
|
+
'migrationContentTypeId',
|
|
179
|
+
'directory',
|
|
180
|
+
]);
|
|
148
181
|
|
|
149
|
-
const { missingStorageModel } =
|
|
182
|
+
const { missingStorageModel } = config;
|
|
150
183
|
if (missingStorageModel) {
|
|
151
184
|
console.error(pc.red('\nError:'), `Missing migration content type. Run ${pc.cyan('npx migrations init')}`);
|
|
152
185
|
process.exit(1);
|
|
153
186
|
}
|
|
154
187
|
|
|
155
|
-
await runMigrations(
|
|
188
|
+
await runMigrations(config);
|
|
156
189
|
}, false)
|
|
157
190
|
);
|
|
158
191
|
|
|
@@ -164,19 +197,26 @@ program
|
|
|
164
197
|
.option('-y, --yes', 'Assume "yes" as answer to all prompts and run non-interactively.')
|
|
165
198
|
.option('--host <host>', 'Management API host')
|
|
166
199
|
.option('--config <path/to/config>', 'Config file path (disables auto detect)')
|
|
200
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
167
201
|
.description('Execute a single migration.')
|
|
168
202
|
.action(
|
|
169
203
|
actionRunner(async (file, options) => {
|
|
170
|
-
const config = await getConfig(parseArgs(
|
|
171
|
-
|
|
204
|
+
const config = await getConfig(parseArgs(cmd || {}), [
|
|
205
|
+
'managementToken',
|
|
206
|
+
'spaceId',
|
|
207
|
+
'environmentId',
|
|
208
|
+
'storage',
|
|
209
|
+
'fieldId',
|
|
210
|
+
'migrationContentTypeId',
|
|
211
|
+
'directory',
|
|
212
|
+
]);
|
|
172
213
|
|
|
173
|
-
const { missingStorageModel } =
|
|
214
|
+
const { missingStorageModel } = config;
|
|
174
215
|
if (missingStorageModel) {
|
|
175
216
|
console.error(pc.red('\nError:'), `Missing migration content type. Run ${pc.cyan('npx migrations init')}`);
|
|
176
217
|
process.exit(1);
|
|
177
218
|
}
|
|
178
|
-
|
|
179
|
-
await executeMigration(path.resolve(file), verified);
|
|
219
|
+
await executeMigration(path.resolve(file), config);
|
|
180
220
|
}, false)
|
|
181
221
|
);
|
|
182
222
|
|
|
@@ -189,27 +229,35 @@ program
|
|
|
189
229
|
.option('--config <path/to/config>', 'Config file path (disables auto detect)')
|
|
190
230
|
.option('--add', 'Mark migration as migrated')
|
|
191
231
|
.option('--remove', 'Delete migration entry in Contentful')
|
|
232
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
192
233
|
.description('Manually mark a migration as migrated or not. (Only available with the Content-model storage)')
|
|
193
234
|
.action(
|
|
194
235
|
actionRunner(async (file, options) => {
|
|
195
236
|
const { remove, add } = options;
|
|
196
|
-
const config = await getConfig(parseArgs(
|
|
197
|
-
|
|
237
|
+
const config = await getConfig(parseArgs(cmd || {}), [
|
|
238
|
+
'managementToken',
|
|
239
|
+
'spaceId',
|
|
240
|
+
'environmentId',
|
|
241
|
+
'storage',
|
|
242
|
+
'fieldId',
|
|
243
|
+
'migrationContentTypeId',
|
|
244
|
+
'directory',
|
|
245
|
+
]);
|
|
198
246
|
|
|
199
|
-
const { missingStorageModel } =
|
|
247
|
+
const { missingStorageModel } = config;
|
|
200
248
|
if (missingStorageModel) {
|
|
201
249
|
console.error(pc.red('\nError:'), `Missing migration content type. Run ${pc.cyan('npx migrations init')}`);
|
|
202
250
|
process.exit(1);
|
|
203
251
|
}
|
|
204
252
|
|
|
205
|
-
const { storage } =
|
|
253
|
+
const { storage } = config || {};
|
|
206
254
|
if (storage === STORAGE_TAG) {
|
|
207
255
|
throw new Error('The version command is not available for the "tag" storage');
|
|
208
256
|
}
|
|
209
257
|
if (remove) {
|
|
210
|
-
await versionDelete(file,
|
|
258
|
+
await versionDelete(file, config);
|
|
211
259
|
} else if (add) {
|
|
212
|
-
await versionAdd(file,
|
|
260
|
+
await versionAdd(file, config);
|
|
213
261
|
}
|
|
214
262
|
}, true)
|
|
215
263
|
);
|
|
@@ -224,23 +272,27 @@ program
|
|
|
224
272
|
.option('--remove', 'Delete contentful environment')
|
|
225
273
|
.option('--reset', 'Reset contentful environment')
|
|
226
274
|
.option('--source-environment-id <environment-id>', 'Set the source environment to clone new environment from')
|
|
275
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
227
276
|
.description('Add or remove a contentful environment for migrations')
|
|
228
277
|
.action(
|
|
229
278
|
actionRunner(async (environmentId, options) => {
|
|
230
279
|
const { remove, create, reset } = options;
|
|
231
|
-
const config = await getConfig(parseArgs({ ...(options || {}), environmentId })
|
|
232
|
-
|
|
280
|
+
const config = await getConfig(parseArgs({ ...(options || {}), environmentId }), [
|
|
281
|
+
'managementToken',
|
|
282
|
+
'spaceId',
|
|
283
|
+
'environmentId',
|
|
284
|
+
]);
|
|
233
285
|
|
|
234
286
|
if (create) {
|
|
235
|
-
return createEnvironment(environmentId,
|
|
287
|
+
return createEnvironment(environmentId, config);
|
|
236
288
|
}
|
|
237
289
|
|
|
238
290
|
if (remove) {
|
|
239
|
-
return removeEnvironment(environmentId,
|
|
291
|
+
return removeEnvironment(environmentId, config);
|
|
240
292
|
}
|
|
241
293
|
|
|
242
294
|
if (reset) {
|
|
243
|
-
return resetEnvironment(environmentId,
|
|
295
|
+
return resetEnvironment(environmentId, config);
|
|
244
296
|
}
|
|
245
297
|
}, true)
|
|
246
298
|
);
|
|
@@ -255,12 +307,12 @@ program
|
|
|
255
307
|
.option('--host <host>', 'Management API host')
|
|
256
308
|
.option('--config <path/to/config>', 'Config file path (disables auto detect)')
|
|
257
309
|
.option('--extension <file-extension>', 'Use custom file extension (default is `md`)')
|
|
310
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
258
311
|
.description('Generate offline docs from content-types')
|
|
259
312
|
.action(
|
|
260
313
|
actionRunner(async (cmd) => {
|
|
261
|
-
const config = await getConfig(parseArgs(cmd || {}));
|
|
262
|
-
|
|
263
|
-
await createOfflineDocs(verified);
|
|
314
|
+
const config = await getConfig(parseArgs(cmd || {}), ['managementToken', 'spaceId', 'environmentId']);
|
|
315
|
+
await createOfflineDocs(config);
|
|
264
316
|
}, true)
|
|
265
317
|
);
|
|
266
318
|
|
|
@@ -277,14 +329,14 @@ program
|
|
|
277
329
|
.option('--diff', 'Manually choose skip/overwrite for every conflict')
|
|
278
330
|
.option('--force', 'No manual diffing. Overwrites all conflicting entries/assets')
|
|
279
331
|
.description('Transfer content from source environment to destination environment')
|
|
332
|
+
.option('--cwd <directory>', 'Working directory. Defaults to process.cwd()')
|
|
280
333
|
.action(
|
|
281
334
|
actionRunner(async (cmd) => {
|
|
282
|
-
const config = await getConfig(parseArgs(cmd || {}));
|
|
283
|
-
const verified = await askMissing({ ...config, environmentId: 'not-used' });
|
|
335
|
+
const config = await getConfig(parseArgs(cmd || {}), ['managementToken', 'spaceId', 'storage']);
|
|
284
336
|
|
|
285
337
|
// run migrations on destination environment
|
|
286
338
|
await transferContent({
|
|
287
|
-
...
|
|
339
|
+
...config,
|
|
288
340
|
contentType: cmd.contentType || '',
|
|
289
341
|
forceOverwrite: cmd.force || false,
|
|
290
342
|
diffConflicts: cmd.diff || false,
|
package/lib/config.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
-
const
|
|
3
|
-
const mergeOptions = require('merge-options').bind({ ignoreUndefined: true });
|
|
4
|
-
const { cosmiconfig } = require('cosmiconfig');
|
|
2
|
+
const { Confirm } = require('enquirer');
|
|
5
3
|
|
|
6
4
|
const { getSpaces, getEnvironments } = require('./contentful');
|
|
7
5
|
|
|
@@ -15,102 +13,50 @@ const STATE_FAILURE = 'failure';
|
|
|
15
13
|
* Get configuration
|
|
16
14
|
* @param {Object} args
|
|
17
15
|
*/
|
|
18
|
-
const getConfig = async (args) => {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.log('Error:', error.message);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let configFileOptions = {};
|
|
52
|
-
try {
|
|
53
|
-
// get configuration from migrations rc file
|
|
54
|
-
const explorer = cosmiconfig('migrations');
|
|
55
|
-
const explorerResult = args.config ? await explorer.load(args.config) : await explorer.search();
|
|
56
|
-
if (explorerResult !== null) {
|
|
57
|
-
const { config, filepath } = explorerResult || {};
|
|
58
|
-
|
|
59
|
-
configFileOptions = {
|
|
60
|
-
directory: path.resolve(path.dirname(filepath || ''), args.directory || 'migrations'),
|
|
61
|
-
...(config || {}),
|
|
62
|
-
};
|
|
16
|
+
const getConfig = async (args, required = []) => {
|
|
17
|
+
const { configFile, cwd, ...overrides } = args;
|
|
18
|
+
const { loadContentfulConfig } = await import('@jungvonmatt/contentful-config');
|
|
19
|
+
const result = await loadContentfulConfig('migrations', {
|
|
20
|
+
configFile,
|
|
21
|
+
cwd,
|
|
22
|
+
overrides,
|
|
23
|
+
defaultConfig: {
|
|
24
|
+
fieldId: 'migration',
|
|
25
|
+
migrationContentTypeId: 'contentful-migrations',
|
|
26
|
+
host: 'api.contentful.com',
|
|
27
|
+
directory: path.resolve(cwd || process.cwd(), 'migrations'),
|
|
28
|
+
},
|
|
29
|
+
envMap: {
|
|
30
|
+
CONTENTFUL_SPACE_ID: 'spaceId',
|
|
31
|
+
CONTENTFUL_ENVIRONMENT_ID: 'environmentId',
|
|
32
|
+
CONTENTFUL_MANAGEMENT_TOKEN: 'managementToken',
|
|
33
|
+
CONTENTFUL_HOST: 'host',
|
|
34
|
+
CONTENTFUL_PROXY: 'proxy',
|
|
35
|
+
CONTENTFUL_MIGRATIONS_STORAGE: 'storage',
|
|
36
|
+
CONTENTFUL_MIGRATIONS_FIELD_ID: 'fieldId',
|
|
37
|
+
CONTENTFUL_MIGRATIONS_CONTENT_TYPE_ID: 'migrationContentTypeId',
|
|
38
|
+
CONTENTFUL_MIGRATIONS_DIRECTORY: 'directory',
|
|
39
|
+
CONTENTFUL_MIGRATIONS_REQUEST_BATCH_SIZE: 'requestBatchSize',
|
|
40
|
+
},
|
|
41
|
+
prompts: getPromts,
|
|
42
|
+
required,
|
|
43
|
+
});
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
configFileOptions.directory = path.resolve(path.dirname(filepath || ''), configFileOptions.directory);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.log('Error:', error.message);
|
|
70
|
-
}
|
|
45
|
+
const missingStorage = result.missing.includes('storage');
|
|
71
46
|
|
|
72
|
-
return
|
|
47
|
+
return { ...result.config, missingStorageModel: missingStorage && result.config.storage === STORAGE_CONTENT };
|
|
73
48
|
};
|
|
74
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Add prompts for migration specific config fields.
|
|
52
|
+
* All default contentful prompts are already available via @jungvonmatt/contentful-config
|
|
53
|
+
* @param {*} data
|
|
54
|
+
* @returns
|
|
55
|
+
*/
|
|
75
56
|
const getPromts = (data) => {
|
|
76
57
|
return [
|
|
77
58
|
{
|
|
78
|
-
type: '
|
|
79
|
-
name: 'accessToken',
|
|
80
|
-
message: 'Management Token',
|
|
81
|
-
default: function () {
|
|
82
|
-
return data.accessToken;
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
|
-
{
|
|
86
|
-
type: 'list',
|
|
87
|
-
name: 'spaceId',
|
|
88
|
-
message: 'Space ID',
|
|
89
|
-
choices: async (answers) => {
|
|
90
|
-
const spaces = await getSpaces({ ...(data || {}), ...answers });
|
|
91
|
-
return spaces.map((space) => ({
|
|
92
|
-
name: `${space.name} (${space.sys.id})`,
|
|
93
|
-
value: space.sys.id,
|
|
94
|
-
}));
|
|
95
|
-
},
|
|
96
|
-
default: function () {
|
|
97
|
-
return data.spaceId;
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
{
|
|
101
|
-
type: 'list',
|
|
102
|
-
name: 'environmentId',
|
|
103
|
-
message: 'Environment ID',
|
|
104
|
-
choices: async (answers) => {
|
|
105
|
-
const environments = await getEnvironments({ ...(data || {}), ...answers });
|
|
106
|
-
return environments.map((environment) => environment.sys.id);
|
|
107
|
-
},
|
|
108
|
-
default: function () {
|
|
109
|
-
return data.environmentId;
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
{
|
|
113
|
-
type: 'list',
|
|
59
|
+
type: 'select',
|
|
114
60
|
name: 'storage',
|
|
115
61
|
message: 'How should the migrations be managed',
|
|
116
62
|
choices: [
|
|
@@ -123,87 +69,50 @@ const getPromts = (data) => {
|
|
|
123
69
|
value: STORAGE_TAG,
|
|
124
70
|
},
|
|
125
71
|
],
|
|
126
|
-
|
|
127
|
-
return data.fieldId;
|
|
128
|
-
},
|
|
72
|
+
initial: data.fieldId,
|
|
129
73
|
},
|
|
130
74
|
{
|
|
131
75
|
type: 'input',
|
|
132
76
|
name: 'fieldId',
|
|
133
77
|
message: 'Id of the tag where the the migration version is stored',
|
|
134
|
-
|
|
135
|
-
return answers.storage ===
|
|
136
|
-
},
|
|
137
|
-
default: function () {
|
|
138
|
-
return data.fieldId;
|
|
78
|
+
skip(answers) {
|
|
79
|
+
return answers.storage === STORAGE_CONTENT;
|
|
139
80
|
},
|
|
81
|
+
initial: data.fieldId,
|
|
140
82
|
},
|
|
141
83
|
{
|
|
142
84
|
type: 'input',
|
|
143
85
|
name: 'migrationContentTypeId',
|
|
144
86
|
message: 'Id of the content-type where the the migrations are stored',
|
|
145
|
-
|
|
146
|
-
return answers.storage ===
|
|
147
|
-
},
|
|
148
|
-
default: function () {
|
|
149
|
-
return data.migrationContentTypeId;
|
|
87
|
+
skip(answers) {
|
|
88
|
+
return answers.storage === STORAGE_TAG;
|
|
150
89
|
},
|
|
90
|
+
initial: data.migrationContentTypeId,
|
|
151
91
|
},
|
|
152
92
|
{
|
|
153
93
|
type: 'input',
|
|
154
94
|
name: 'directory',
|
|
155
95
|
message: 'Directory where the migrations are stored',
|
|
156
|
-
|
|
157
|
-
return data.directory;
|
|
158
|
-
},
|
|
96
|
+
initial: data.directory,
|
|
159
97
|
},
|
|
160
98
|
];
|
|
161
99
|
};
|
|
162
100
|
|
|
163
|
-
const askAll = async (data = {}) => {
|
|
164
|
-
console.log('Please verify the following options');
|
|
165
|
-
|
|
166
|
-
const answers = await inquirer.prompt(getPromts(data));
|
|
167
|
-
answers.directory = path.resolve(process.cwd(), answers.directory || data.directory);
|
|
168
|
-
|
|
169
|
-
return answers;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
const askMissing = async (data = {}, requiredFields = undefined) => {
|
|
173
|
-
const allQuestions = getPromts(data);
|
|
174
|
-
if (!requiredFields) {
|
|
175
|
-
requiredFields = allQuestions.map(({ name }) => name);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const missingPromts = getPromts(data).filter(({ name }) => !data[name] && requiredFields.includes(name));
|
|
179
|
-
|
|
180
|
-
// Check if storage changed to content and run initialization
|
|
181
|
-
const missingStorage = missingPromts.some((prompt) => prompt.name === 'storage');
|
|
182
|
-
const answers = await inquirer.prompt(missingPromts);
|
|
183
|
-
const { storage } = answers;
|
|
184
|
-
|
|
185
|
-
return { ...data, ...answers, missingStorageModel: missingStorage && storage === STORAGE_CONTENT };
|
|
186
|
-
};
|
|
187
|
-
|
|
188
101
|
const confirm = async (config = {}) => {
|
|
189
102
|
if (config.yes) {
|
|
190
103
|
return true;
|
|
191
104
|
}
|
|
192
|
-
const { check } = await inquirer.prompt([
|
|
193
|
-
{
|
|
194
|
-
type: 'confirm',
|
|
195
|
-
name: 'check',
|
|
196
|
-
message: 'Do you wish to proceed?',
|
|
197
|
-
default: true,
|
|
198
|
-
},
|
|
199
|
-
]);
|
|
200
105
|
|
|
201
|
-
|
|
106
|
+
const prompt = new Confirm({
|
|
107
|
+
name: 'check',
|
|
108
|
+
message: config?.message || 'Do you wish to proceed?',
|
|
109
|
+
initial: true,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return prompt.run();
|
|
202
113
|
};
|
|
203
114
|
|
|
204
115
|
module.exports.getConfig = getConfig;
|
|
205
|
-
module.exports.askAll = askAll;
|
|
206
|
-
module.exports.askMissing = askMissing;
|
|
207
116
|
module.exports.confirm = confirm;
|
|
208
117
|
module.exports.STORAGE_TAG = STORAGE_TAG;
|
|
209
118
|
module.exports.STORAGE_CONTENT = STORAGE_CONTENT;
|
package/lib/content.js
CHANGED
|
@@ -52,7 +52,7 @@ const transferContent = async (config) => {
|
|
|
52
52
|
destEnvironmentId,
|
|
53
53
|
spaceId,
|
|
54
54
|
contentType,
|
|
55
|
-
|
|
55
|
+
managementToken,
|
|
56
56
|
} = config || {};
|
|
57
57
|
// Check migration version
|
|
58
58
|
const sourceVersion = await getLatestVersion({ ...config, environmentId: sourceEnvironmentId });
|
|
@@ -133,7 +133,7 @@ const transferContent = async (config) => {
|
|
|
133
133
|
});
|
|
134
134
|
|
|
135
135
|
// just a small helper to add a line break after the inquiry
|
|
136
|
-
const br = diffConflicts
|
|
136
|
+
const br = diffConflicts ? '\n' : '';
|
|
137
137
|
|
|
138
138
|
if (assets.length === 0 && entries.length === 0) {
|
|
139
139
|
console.log(pc.green(`${br}All done`), '🚀');
|
package/lib/contentful.js
CHANGED
|
@@ -20,7 +20,6 @@ const LINK_TYPE_ASSET = 'Asset';
|
|
|
20
20
|
const LINK_TYPE_ENTRY = 'Entry';
|
|
21
21
|
|
|
22
22
|
const MAX_ALLOWED_LIMIT = 1000;
|
|
23
|
-
const DEFAULT_ENVIRONMENT_ID = 'master';
|
|
24
23
|
|
|
25
24
|
const getContentTypeId = (node) => {
|
|
26
25
|
const { sys } = node || {};
|
|
@@ -63,21 +62,21 @@ const getContentName = (node, displayField) => {
|
|
|
63
62
|
};
|
|
64
63
|
|
|
65
64
|
const getClient = async (options) => {
|
|
66
|
-
const {
|
|
65
|
+
const { managementToken, host } = options || {};
|
|
67
66
|
|
|
68
67
|
if (client) {
|
|
69
68
|
return client;
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
const params = {
|
|
73
|
-
accessToken,
|
|
72
|
+
accessToken: managementToken,
|
|
74
73
|
};
|
|
75
74
|
|
|
76
75
|
if (host) {
|
|
77
76
|
params.host = host;
|
|
78
77
|
}
|
|
79
78
|
|
|
80
|
-
if (accessToken) {
|
|
79
|
+
if (params.accessToken) {
|
|
81
80
|
client = await contentful.createClient(params);
|
|
82
81
|
return client;
|
|
83
82
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type Migration from "contentful-migration";
|
|
2
2
|
import type { MigrationContext } from "contentful-migration";
|
|
3
3
|
|
|
4
|
-
export type ValueMappingFunction = (values:
|
|
4
|
+
export type ValueMappingFunction<T extends string = string> = (values: T[]) => T[];
|
|
5
5
|
|
|
6
6
|
export type AddValuesOptionMode = 'sorted' | 'start' | 'end' | 'before' | 'after';
|
|
7
7
|
|
|
@@ -10,13 +10,36 @@ export interface AddValuesOptions {
|
|
|
10
10
|
ref?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// define some known mark and node values here to support code completion, but allow any string as well
|
|
14
|
+
export type RichTextMarks = 'bold' | 'italic' | 'underline' | 'code' | 'superscript' | 'subscript' | 'strikethrough' | string & {};
|
|
15
|
+
export type RichTextNodeType = 'document' | 'paragraph' | 'heading-1' | 'heading-2' | 'heading-3' | 'heading-4' | 'heading-5' | 'heading-6' | 'ordered-list' | 'unordered-list' | 'list-item' | 'hr' | 'blockquote' | 'embedded-entry-block' | 'embedded-asset-block' | 'embedded-resource-block' | 'table' | 'table-row' | 'table-cell' | 'table-header-cell' | 'asset-hyperlink' | 'embedded-entry-inline' | 'embedded-resource-inline' | 'entry-hyperlink' | 'hyperlink' | 'resource-hyperlink' | string & {};
|
|
16
|
+
|
|
17
|
+
export type RichTextLinkedNodeType = 'entry-hyperlink' | 'embedded-entry-block' | 'embedded-entry-inline'
|
|
18
|
+
|
|
19
|
+
export interface RichTextValidationHelpers {
|
|
20
|
+
addEnabledMarksValues(contentTypeId: string, fieldId: string, values: RichTextMarks | RichTextMarks[]): Promise<void>;
|
|
21
|
+
removeEnabledMarksValues(contentTypeId: string, fieldId: string, values: RichTextMarks | RichTextMarks[]): Promise<void>;
|
|
22
|
+
modifyEnabledMarksValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction<RichTextMarks>): Promise<void>;
|
|
23
|
+
|
|
24
|
+
addEnabledNodeTypeValues(contentTypeId: string, fieldId: string, values: RichTextNodeType | RichTextNodeType[]): Promise<void>;
|
|
25
|
+
removeEnabledNodeTypeValues(contentTypeId: string, fieldId: string, values: RichTextNodeType | RichTextNodeType[]): Promise<void>;
|
|
26
|
+
modifyEnabledNodeTypeValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction<RichTextNodeType>): Promise<void>;
|
|
27
|
+
|
|
28
|
+
addNodeContentTypeValues(contentTypeId: string, fieldId: string, nodeType: RichTextLinkedNodeType, values: string | string[]): Promise<void>;
|
|
29
|
+
removeNodeContentTypeValues(contentTypeId: string, fieldId: string, nodeType: RichTextLinkedNodeType, values: string | string[]): Promise<void>;
|
|
30
|
+
modifyNodeContentTypeValues(contentTypeId: string, fieldId: string, nodeType: RichTextLinkedNodeType, valueMappingFunction: ValueMappingFunction): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
|
|
13
33
|
export interface ValidationHelpers {
|
|
14
34
|
addLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
|
|
15
|
-
addInValues(contentTypeId: string, fieldId: string, values: string | string[], options?: AddValuesOptions): Promise<void>;
|
|
16
35
|
removeLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
|
|
17
|
-
removeInValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
|
|
18
36
|
modifyLinkContentTypeValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction): Promise<void>;
|
|
37
|
+
|
|
38
|
+
addInValues(contentTypeId: string, fieldId: string, values: string | string[], options?: AddValuesOptions): Promise<void>;
|
|
39
|
+
removeInValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
|
|
19
40
|
modifyInValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction): Promise<void>;
|
|
41
|
+
|
|
42
|
+
richText: RichTextValidationHelpers;
|
|
20
43
|
}
|
|
21
44
|
|
|
22
45
|
export function getValidationHelpers(migration: Migration, context: MigrationContext): ValidationHelpers;
|
|
@@ -27,26 +27,57 @@ const getValidationHelpers = (migration, context) => {
|
|
|
27
27
|
const removeValidationValues = (existingValues, newValues = []) =>
|
|
28
28
|
existingValues.filter((x) => !newValues.includes(x));
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Ensure the complete key path is available.
|
|
32
|
+
* If more than two keys are given the first node is treated as an Object.
|
|
33
|
+
* Return the container node, which should be an array.
|
|
34
|
+
* @param root {Object} The root container (the validations array)
|
|
35
|
+
* @param keyPath {string[]} The names of the nodes that must exist
|
|
36
|
+
* @return {Array} A tuple with the container object and the last key.
|
|
37
|
+
*/
|
|
38
|
+
const ensureObjectPath = (root, keyPath) => {
|
|
39
|
+
let container = undefined;
|
|
40
|
+
let node = root;
|
|
41
|
+
keyPath.forEach((key, index) => {
|
|
42
|
+
container = node;
|
|
43
|
+
const isObjectNode = keyPath.length > 2 && index === 0;
|
|
44
|
+
if (Array.isArray(node)) {
|
|
45
|
+
let entry = node.find((someEntry) => someEntry[key]);
|
|
46
|
+
if (!entry) {
|
|
47
|
+
entry = { [key]: isObjectNode ? {} : [] };
|
|
48
|
+
node.push(entry);
|
|
35
49
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
50
|
+
container = entry;
|
|
51
|
+
node = entry[key];
|
|
52
|
+
} else {
|
|
53
|
+
if (!node[key]) {
|
|
54
|
+
node[key] = isObjectNode ? {} : [];
|
|
42
55
|
}
|
|
43
|
-
|
|
56
|
+
node = node[key];
|
|
44
57
|
}
|
|
45
|
-
|
|
46
|
-
return validation;
|
|
47
58
|
});
|
|
59
|
+
return [container, keyPath[keyPath.length - 1]];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Modify a validation property.
|
|
64
|
+
* @param {string[]} validations The list of validation entries
|
|
65
|
+
* @param {function} method The modification function
|
|
66
|
+
* @param {string[]} keyPaths The key paths where the first is the id of the validation root.
|
|
67
|
+
* @param {string | string[]} values
|
|
68
|
+
* @returns {*}
|
|
69
|
+
*/
|
|
70
|
+
const modifyValidations = (validations, method, keyPaths, values) => {
|
|
71
|
+
const [containerObject, validationKey] = ensureObjectPath(validations, keyPaths);
|
|
72
|
+
|
|
73
|
+
containerObject[validationKey] = method(containerObject[validationKey], values);
|
|
74
|
+
|
|
75
|
+
return validations;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const modifyValidationValuesForType = async (validationKeyPaths, method, contentTypeId, fieldId, valueOrValues) => {
|
|
79
|
+
const values = Array.isArray(valueOrValues) ? valueOrValues : [valueOrValues];
|
|
48
80
|
|
|
49
|
-
const modifyValidationValuesForType = async (validationKey, method, contentTypeId, fieldId, values) => {
|
|
50
81
|
// Fetch content type
|
|
51
82
|
const { fields } = await makeRequest({
|
|
52
83
|
method: 'GET',
|
|
@@ -62,11 +93,11 @@ const getValidationHelpers = (migration, context) => {
|
|
|
62
93
|
const ct = migration.editContentType(contentTypeId);
|
|
63
94
|
ct.editField(fieldId).items({
|
|
64
95
|
...items,
|
|
65
|
-
validations: modifyValidations(items?.validations, method,
|
|
96
|
+
validations: modifyValidations(items?.validations || [], method, validationKeyPaths, values),
|
|
66
97
|
});
|
|
67
98
|
} else {
|
|
68
99
|
const ct = migration.editContentType(contentTypeId);
|
|
69
|
-
ct.editField(fieldId).validations(modifyValidations(validations, method,
|
|
100
|
+
ct.editField(fieldId).validations(modifyValidations(validations, method, validationKeyPaths, values));
|
|
70
101
|
}
|
|
71
102
|
};
|
|
72
103
|
|
|
@@ -75,7 +106,7 @@ const getValidationHelpers = (migration, context) => {
|
|
|
75
106
|
* Add the specified values to the list of allowed content type values
|
|
76
107
|
*/
|
|
77
108
|
async addLinkContentTypeValues(contentTypeId, fieldId, values) {
|
|
78
|
-
await modifyValidationValuesForType('linkContentType', addValidationValues, contentTypeId, fieldId, values);
|
|
109
|
+
await modifyValidationValuesForType(['linkContentType'], addValidationValues, contentTypeId, fieldId, values);
|
|
79
110
|
},
|
|
80
111
|
|
|
81
112
|
/**
|
|
@@ -85,21 +116,21 @@ const getValidationHelpers = (migration, context) => {
|
|
|
85
116
|
*/
|
|
86
117
|
async addInValues(contentTypeId, fieldId, values, options = {}) {
|
|
87
118
|
const addValuesWithOptions = (existingValues, newValues = []) => addValues(existingValues, newValues, options);
|
|
88
|
-
await modifyValidationValuesForType('in', addValuesWithOptions, contentTypeId, fieldId, values);
|
|
119
|
+
await modifyValidationValuesForType(['in'], addValuesWithOptions, contentTypeId, fieldId, values);
|
|
89
120
|
},
|
|
90
121
|
|
|
91
122
|
/**
|
|
92
123
|
* Remove the specified values from the list of allowed content type values
|
|
93
124
|
*/
|
|
94
125
|
async removeLinkContentTypeValues(contentTypeId, fieldId, values) {
|
|
95
|
-
await modifyValidationValuesForType('linkContentType', removeValidationValues, contentTypeId, fieldId, values);
|
|
126
|
+
await modifyValidationValuesForType(['linkContentType'], removeValidationValues, contentTypeId, fieldId, values);
|
|
96
127
|
},
|
|
97
128
|
|
|
98
129
|
/**
|
|
99
130
|
* Remove the specified values from the list of allowed values
|
|
100
131
|
*/
|
|
101
132
|
async removeInValues(contentTypeId, fieldId, values) {
|
|
102
|
-
await modifyValidationValuesForType('in', removeValidationValues, contentTypeId, fieldId, values);
|
|
133
|
+
await modifyValidationValuesForType(['in'], removeValidationValues, contentTypeId, fieldId, values);
|
|
103
134
|
},
|
|
104
135
|
|
|
105
136
|
/**
|
|
@@ -108,7 +139,7 @@ const getValidationHelpers = (migration, context) => {
|
|
|
108
139
|
*/
|
|
109
140
|
async modifyLinkContentTypeValues(contentTypeId, fieldId, valueMappingFunction) {
|
|
110
141
|
const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
|
|
111
|
-
await modifyValidationValuesForType('linkContentType', uniqueMappingFunction, contentTypeId, fieldId, []);
|
|
142
|
+
await modifyValidationValuesForType(['linkContentType'], uniqueMappingFunction, contentTypeId, fieldId, []);
|
|
112
143
|
},
|
|
113
144
|
|
|
114
145
|
/**
|
|
@@ -117,7 +148,66 @@ const getValidationHelpers = (migration, context) => {
|
|
|
117
148
|
*/
|
|
118
149
|
async modifyInValues(contentTypeId, fieldId, valueMappingFunction) {
|
|
119
150
|
const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
|
|
120
|
-
await modifyValidationValuesForType('in', uniqueMappingFunction, contentTypeId, fieldId, []);
|
|
151
|
+
await modifyValidationValuesForType(['in'], uniqueMappingFunction, contentTypeId, fieldId, []);
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
richText: {
|
|
155
|
+
async addEnabledMarksValues(contentTypeId, fieldId, values) {
|
|
156
|
+
await modifyValidationValuesForType(['enabledMarks'], addValidationValues, contentTypeId, fieldId, values);
|
|
157
|
+
},
|
|
158
|
+
async removeEnabledMarksValues(contentTypeId, fieldId, values) {
|
|
159
|
+
await modifyValidationValuesForType(['enabledMarks'], removeValidationValues, contentTypeId, fieldId, values);
|
|
160
|
+
},
|
|
161
|
+
async modifyEnabledMarksValues(contentTypeId, fieldId, valueMappingFunction) {
|
|
162
|
+
const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
|
|
163
|
+
await modifyValidationValuesForType(['enabledMarks'], uniqueMappingFunction, contentTypeId, fieldId, []);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async addEnabledNodeTypeValues(contentTypeId, fieldId, values) {
|
|
167
|
+
await modifyValidationValuesForType(['enabledNodeTypes'], addValidationValues, contentTypeId, fieldId, values);
|
|
168
|
+
},
|
|
169
|
+
async removeEnabledNodeTypeValues(contentTypeId, fieldId, values) {
|
|
170
|
+
await modifyValidationValuesForType(
|
|
171
|
+
['enabledNodeTypes'],
|
|
172
|
+
removeValidationValues,
|
|
173
|
+
contentTypeId,
|
|
174
|
+
fieldId,
|
|
175
|
+
values
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
async modifyEnabledNodeTypeValues(contentTypeId, fieldId, valueMappingFunction) {
|
|
179
|
+
const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
|
|
180
|
+
await modifyValidationValuesForType(['enabledNodeTypes'], uniqueMappingFunction, contentTypeId, fieldId, []);
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
async addNodeContentTypeValues(contentTypeId, fieldId, nodeType, values) {
|
|
184
|
+
await modifyValidationValuesForType(
|
|
185
|
+
['nodes', nodeType, 'linkContentType'],
|
|
186
|
+
addValidationValues,
|
|
187
|
+
contentTypeId,
|
|
188
|
+
fieldId,
|
|
189
|
+
values
|
|
190
|
+
);
|
|
191
|
+
},
|
|
192
|
+
async removeNodeContentTypeValues(contentTypeId, fieldId, nodeType, values) {
|
|
193
|
+
await modifyValidationValuesForType(
|
|
194
|
+
['nodes', nodeType, 'linkContentType'],
|
|
195
|
+
removeValidationValues,
|
|
196
|
+
contentTypeId,
|
|
197
|
+
fieldId,
|
|
198
|
+
values
|
|
199
|
+
);
|
|
200
|
+
},
|
|
201
|
+
async modifyNodeContentTypeValues(contentTypeId, fieldId, nodeType, valueMappingFunction) {
|
|
202
|
+
const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
|
|
203
|
+
await modifyValidationValuesForType(
|
|
204
|
+
['nodes', nodeType, 'linkContentType'],
|
|
205
|
+
uniqueMappingFunction,
|
|
206
|
+
contentTypeId,
|
|
207
|
+
fieldId,
|
|
208
|
+
[]
|
|
209
|
+
);
|
|
210
|
+
},
|
|
121
211
|
},
|
|
122
212
|
};
|
|
123
213
|
};
|
|
@@ -6,7 +6,6 @@ describe('getValidationHelpers', () => {
|
|
|
6
6
|
{
|
|
7
7
|
id: 'selectWithOtherValidationProps',
|
|
8
8
|
type: 'Text',
|
|
9
|
-
items: [],
|
|
10
9
|
validations: [
|
|
11
10
|
{
|
|
12
11
|
foo: 'test',
|
|
@@ -20,7 +19,6 @@ describe('getValidationHelpers', () => {
|
|
|
20
19
|
{
|
|
21
20
|
id: 'select',
|
|
22
21
|
type: 'Text',
|
|
23
|
-
items: [],
|
|
24
22
|
validations: [
|
|
25
23
|
{
|
|
26
24
|
in: ['foo', 'bar', 'baz'],
|
|
@@ -36,11 +34,51 @@ describe('getValidationHelpers', () => {
|
|
|
36
34
|
},
|
|
37
35
|
],
|
|
38
36
|
},
|
|
37
|
+
{
|
|
38
|
+
id: 'string',
|
|
39
|
+
type: 'Text',
|
|
40
|
+
},
|
|
39
41
|
{
|
|
40
42
|
id: 'string-array',
|
|
41
43
|
type: 'Array',
|
|
42
44
|
items: [],
|
|
43
45
|
},
|
|
46
|
+
{
|
|
47
|
+
id: 'richTextNoValidations',
|
|
48
|
+
type: 'RichText',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'richTextFullValidations',
|
|
52
|
+
type: 'RichText',
|
|
53
|
+
validations: [
|
|
54
|
+
{
|
|
55
|
+
enabledMarks: ['bold', 'italic'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
enabledNodeTypes: [
|
|
59
|
+
'blockquote',
|
|
60
|
+
'embedded-entry-block',
|
|
61
|
+
'hyperlink',
|
|
62
|
+
'entry-hyperlink',
|
|
63
|
+
'embedded-entry-inline',
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
nodes: {
|
|
68
|
+
'embedded-entry-block': [
|
|
69
|
+
{
|
|
70
|
+
linkContentType: ['foo', 'baz'],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
'embedded-entry-inline': [
|
|
74
|
+
{
|
|
75
|
+
linkContentType: ['foo'],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
},
|
|
44
82
|
],
|
|
45
83
|
});
|
|
46
84
|
|
|
@@ -98,6 +136,16 @@ describe('getValidationHelpers', () => {
|
|
|
98
136
|
},
|
|
99
137
|
]);
|
|
100
138
|
});
|
|
139
|
+
|
|
140
|
+
it('should add values where none where defined', async () => {
|
|
141
|
+
const validations = getValidationHelpers(migration, context);
|
|
142
|
+
await validations.addInValues('some-content-type', 'string', 'foo');
|
|
143
|
+
expect(resultValidation).toEqual([
|
|
144
|
+
{
|
|
145
|
+
in: ['foo'],
|
|
146
|
+
},
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
101
149
|
});
|
|
102
150
|
|
|
103
151
|
describe('removeInValues', () => {
|
|
@@ -168,4 +216,166 @@ describe('getValidationHelpers', () => {
|
|
|
168
216
|
]);
|
|
169
217
|
});
|
|
170
218
|
});
|
|
219
|
+
|
|
220
|
+
describe('richText', () => {
|
|
221
|
+
describe('addEnabledMarksValue', () => {
|
|
222
|
+
it('should add values at the end', async () => {
|
|
223
|
+
const validations = getValidationHelpers(migration, context);
|
|
224
|
+
await validations.richText.addEnabledMarksValues('some-content-type', 'richTextFullValidations', 'code');
|
|
225
|
+
expect(resultValidation).toContainEqual({
|
|
226
|
+
enabledMarks: ['bold', 'italic', 'code'],
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('removeEnabledMarksValue', () => {
|
|
232
|
+
it('should remove values', async () => {
|
|
233
|
+
const validations = getValidationHelpers(migration, context);
|
|
234
|
+
await validations.richText.removeEnabledMarksValues('some-content-type', 'richTextFullValidations', 'bold');
|
|
235
|
+
expect(resultValidation).toContainEqual({
|
|
236
|
+
enabledMarks: ['italic'],
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('modifyEnabledMarksValue', () => {
|
|
242
|
+
it('should modify unique values with custom function', async () => {
|
|
243
|
+
const validations = getValidationHelpers(migration, context);
|
|
244
|
+
await validations.richText.modifyEnabledMarksValues(
|
|
245
|
+
'some-content-type',
|
|
246
|
+
'richTextFullValidations',
|
|
247
|
+
(values) => {
|
|
248
|
+
const result = values.slice(0, values.length - 1); // remove italic
|
|
249
|
+
result.push('code');
|
|
250
|
+
result.push('bold'); // should be removed since it exists
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
);
|
|
254
|
+
expect(resultValidation).toContainEqual({
|
|
255
|
+
enabledMarks: ['bold', 'code'],
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
describe('addEnabledNodeTypeValue', () => {
|
|
261
|
+
it('should add values at the end', async () => {
|
|
262
|
+
const validations = getValidationHelpers(migration, context);
|
|
263
|
+
await validations.richText.addEnabledNodeTypeValues('some-content-type', 'richTextFullValidations', 'hr');
|
|
264
|
+
expect(resultValidation).toContainEqual({
|
|
265
|
+
enabledNodeTypes: [
|
|
266
|
+
'blockquote',
|
|
267
|
+
'embedded-entry-block',
|
|
268
|
+
'hyperlink',
|
|
269
|
+
'entry-hyperlink',
|
|
270
|
+
'embedded-entry-inline',
|
|
271
|
+
'hr',
|
|
272
|
+
],
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('removeEnabledNodeTypeValue', () => {
|
|
278
|
+
it('should remove values', async () => {
|
|
279
|
+
const validations = getValidationHelpers(migration, context);
|
|
280
|
+
await validations.richText.removeEnabledNodeTypeValues(
|
|
281
|
+
'some-content-type',
|
|
282
|
+
'richTextFullValidations',
|
|
283
|
+
'blockquote'
|
|
284
|
+
);
|
|
285
|
+
expect(resultValidation).toContainEqual({
|
|
286
|
+
enabledNodeTypes: ['embedded-entry-block', 'hyperlink', 'entry-hyperlink', 'embedded-entry-inline'],
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('modifyEnabledNodeTypeValue', () => {
|
|
292
|
+
it('should modify unique values with custom function', async () => {
|
|
293
|
+
const validations = getValidationHelpers(migration, context);
|
|
294
|
+
await validations.richText.modifyEnabledNodeTypeValues(
|
|
295
|
+
'some-content-type',
|
|
296
|
+
'richTextFullValidations',
|
|
297
|
+
(values) => {
|
|
298
|
+
const result = values.slice(0, values.length - 1); // remove embedded-entry-inline
|
|
299
|
+
result.push('hr');
|
|
300
|
+
result.push('blockquote'); // should be removed since it exists
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
expect(resultValidation).toContainEqual({
|
|
305
|
+
enabledNodeTypes: ['blockquote', 'embedded-entry-block', 'hyperlink', 'entry-hyperlink', 'hr'],
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('addNodeContentTypeValues', () => {
|
|
311
|
+
it('should add values at the end', async () => {
|
|
312
|
+
const validations = getValidationHelpers(migration, context);
|
|
313
|
+
await validations.richText.addNodeContentTypeValues(
|
|
314
|
+
'some-content-type',
|
|
315
|
+
'richTextFullValidations',
|
|
316
|
+
'embedded-entry-block',
|
|
317
|
+
'bar'
|
|
318
|
+
);
|
|
319
|
+
expect(resultValidation.find((v) => v.nodes).nodes['embedded-entry-block']).toEqual([
|
|
320
|
+
{
|
|
321
|
+
linkContentType: ['foo', 'baz', 'bar'],
|
|
322
|
+
},
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should add complete entry if not existing', async () => {
|
|
327
|
+
const validations = getValidationHelpers(migration, context);
|
|
328
|
+
await validations.richText.addNodeContentTypeValues(
|
|
329
|
+
'some-content-type',
|
|
330
|
+
'richTextFullValidations',
|
|
331
|
+
'entry-hyperlink',
|
|
332
|
+
'bar'
|
|
333
|
+
);
|
|
334
|
+
expect(resultValidation.find((v) => v.nodes).nodes['entry-hyperlink']).toEqual([
|
|
335
|
+
{
|
|
336
|
+
linkContentType: ['bar'],
|
|
337
|
+
},
|
|
338
|
+
]);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('removeNodeContentTypeValues', () => {
|
|
343
|
+
it('should remove values', async () => {
|
|
344
|
+
const validations = getValidationHelpers(migration, context);
|
|
345
|
+
await validations.richText.removeNodeContentTypeValues(
|
|
346
|
+
'some-content-type',
|
|
347
|
+
'richTextFullValidations',
|
|
348
|
+
'embedded-entry-block',
|
|
349
|
+
'foo'
|
|
350
|
+
);
|
|
351
|
+
expect(resultValidation.find((v) => v.nodes).nodes['embedded-entry-block']).toEqual([
|
|
352
|
+
{
|
|
353
|
+
linkContentType: ['baz'],
|
|
354
|
+
},
|
|
355
|
+
]);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe('modifyNodeContentTypeValues', () => {
|
|
360
|
+
it('should modify unique values with custom function', async () => {
|
|
361
|
+
const validations = getValidationHelpers(migration, context);
|
|
362
|
+
await validations.richText.modifyNodeContentTypeValues(
|
|
363
|
+
'some-content-type',
|
|
364
|
+
'richTextFullValidations',
|
|
365
|
+
'embedded-entry-block',
|
|
366
|
+
(values) => {
|
|
367
|
+
const result = values.slice(0, values.length - 1); // remove foo
|
|
368
|
+
result.push('bar');
|
|
369
|
+
result.push('baz'); // should be removed since it exists
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
expect(resultValidation.find((v) => v.nodes).nodes['embedded-entry-block']).toEqual([
|
|
374
|
+
{
|
|
375
|
+
linkContentType: ['foo', 'bar', 'baz'],
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
171
381
|
});
|
package/lib/migration.js
CHANGED
|
@@ -138,15 +138,15 @@ const fetchMigration = async (config) => {
|
|
|
138
138
|
* @returns
|
|
139
139
|
*/
|
|
140
140
|
const executeMigration = async (file, config) => {
|
|
141
|
-
const {
|
|
141
|
+
const { managementToken, requestBatchSize, spaceId } = config || {};
|
|
142
142
|
const client = await getEnvironment(config);
|
|
143
143
|
const environmentId = client.sys.id;
|
|
144
144
|
const name = path.basename(file);
|
|
145
145
|
const [, version] = /^(\d+)-/.exec(name);
|
|
146
146
|
|
|
147
147
|
const options = {
|
|
148
|
-
filePath: file,
|
|
149
|
-
accessToken,
|
|
148
|
+
filePath: path.resolve(file),
|
|
149
|
+
accessToken: managementToken,
|
|
150
150
|
spaceId,
|
|
151
151
|
environmentId,
|
|
152
152
|
requestBatchSize,
|
|
@@ -154,7 +154,7 @@ const executeMigration = async (file, config) => {
|
|
|
154
154
|
};
|
|
155
155
|
|
|
156
156
|
if (config.host) {
|
|
157
|
-
options.host = config.host
|
|
157
|
+
options.host = config.host;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
console.log(`\nRun migration ${pc.green(version)} in environment ${pc.green(environmentId)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungvonmatt/contentful-migrations",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Helper to handle migrations in contentful",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"author": "Ben Zörb <benjamin.zoerb@jvm.de>",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"engines": {
|
|
29
|
-
"node": ">=
|
|
29
|
+
"node": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"contentful",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@contentful/rich-text-plain-text-renderer": "^15.12.1",
|
|
38
|
+
"@jungvonmatt/contentful-config": "^3.0.1",
|
|
38
39
|
"array.prototype.flatmap": "^1.3.3",
|
|
39
40
|
"ascii-tree": "^0.3.0",
|
|
40
41
|
"cli-progress": "^3.11.2",
|
|
@@ -48,9 +49,9 @@
|
|
|
48
49
|
"deep-diff": "^1.0.2",
|
|
49
50
|
"diff": "^5.1.0",
|
|
50
51
|
"dotenv": "^10.0.0",
|
|
52
|
+
"enquirer": "^2.4.1",
|
|
51
53
|
"fs-extra": "^10.1.0",
|
|
52
54
|
"globby": "^12.0.2",
|
|
53
|
-
"inquirer": "^8.2.0",
|
|
54
55
|
"markdown-table": "^3.0.4",
|
|
55
56
|
"merge-options": "^3.0.4",
|
|
56
57
|
"mustache": "^4.2.0",
|