@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 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
- | Name | Default | Description |
38
- | ---------------------- |---------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
39
- | accessToken | `undefined` | Contentful Management Token. Just run `npx contentful login` and you're done. |
40
- | spaceId | `undefined` | Contentful Space id. Will fallback to `process.env.CONTENTFUL_SPACE_ID` if not set. |
41
- | environmentId | `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 |
42
- | host | `undefined` | Allows configuring the Contentful CLI for EU usage. Will fallback to the global contentful config in your `.contentfulrc.json` |
43
- | requestBatchSize | `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` |
44
- | 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. |
45
- | fieldId | `'migration'` | Id of the tag where the migration version is stored (only used with storage `tag`) |
46
- | migrationContentTypeId | `'contentful-migrations'` | Id of the migration content-type (only used with storage `content`) |
47
- | directory | `'./migrations'` | Directory where the migration files are stored |
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
- *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*
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
- *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.*
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, askAll, askMissing, STORAGE_CONTENT, STORAGE_TAG } = require('./lib/config');
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.configFile,
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
- const verified = await askAll(config);
68
- const { managementToken, accessToken, environmentId, spaceId, ...rest } = verified;
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 (verified.storage === STORAGE_CONTENT) {
71
- await initializeContentModel({ ...config, ...verified });
72
- await migrateToContentStorage({ ...config, ...verified });
81
+ if (config.storage === STORAGE_CONTENT) {
82
+ await initializeContentModel(config);
83
+ await migrateToContentStorage(config);
73
84
  }
74
- if (verified.storage === STORAGE_TAG) {
75
- await migrateToTagStorage({ ...config, ...verified });
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
- // try to store in package.json
83
- const { pkgUp } = await import('pkg-up');
84
- const localPkg = await pkgUp();
85
- if (localPkg) {
86
- const packageJson = await fs.readJson(localPkg);
87
- rest.directory = path.relative(path.dirname(localPkg), rest.directory);
88
- packageJson.migrations = rest;
89
- await fs.outputJson(localPkg, packageJson, { spaces: 2 });
90
- } else {
91
- // store in .migrationsrc if no package.json is available
92
- await fs.outputJson(path.join(process.cwd(), '.migrationsrc'), rest, { spaces: 2 });
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
- const verified = await askMissing(config);
111
- await fetchMigration({ ...verified, contentType: cmd.contentType });
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
- const verified = await askMissing(config);
128
- await createMigration(verified);
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
- const verified = await askMissing(config);
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 } = verified;
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(verified);
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(options || {}));
171
- const verified = await askMissing(config);
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 } = verified;
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(options || {}));
197
- const verified = await askMissing(config);
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 } = verified;
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 } = verified || {};
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, verified);
258
+ await versionDelete(file, config);
211
259
  } else if (add) {
212
- await versionAdd(file, verified);
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
- const verified = await askMissing(config, ['accessToken', 'spaceId', 'environmentId']);
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, verified);
287
+ return createEnvironment(environmentId, config);
236
288
  }
237
289
 
238
290
  if (remove) {
239
- return removeEnvironment(environmentId, verified);
291
+ return removeEnvironment(environmentId, config);
240
292
  }
241
293
 
242
294
  if (reset) {
243
- return resetEnvironment(environmentId, verified);
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
- const verified = await askMissing(config, ['accessToken', 'spaceId', 'environmentId']);
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
- ...verified,
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 inquirer = require('inquirer');
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 defaultOptions = {
20
- fieldId: 'migration',
21
- migrationContentTypeId: 'contentful-migrations',
22
- host: 'api.contentful.com',
23
- directory: path.resolve(process.cwd(), 'migrations'),
24
- };
25
-
26
- const environmentOptions = {
27
- spaceId: process.env.CONTENTFUL_SPACE_ID,
28
- environmentId: process.env.CONTENTFUL_ENVIRONMENT_ID,
29
- accessToken: process.env.CONTENTFUL_MANAGEMENT_TOKEN,
30
- host: process.env.CONTENTFUL_HOST,
31
- };
32
-
33
- let contentfulCliOptions = {};
34
- try {
35
- // get configuration from contentful rc file (created by the contentful cli command)
36
- const explorer = cosmiconfig('contentful');
37
- const explorerResult = await explorer.search();
38
- if (explorerResult !== null) {
39
- const { config } = explorerResult || {};
40
- const { managementToken, activeSpaceId, activeEnvironmentId, host } = config || {};
41
- contentfulCliOptions = {
42
- spaceId: activeSpaceId,
43
- accessToken: managementToken,
44
- host,
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
- if (configFileOptions.directory && !path.isAbsolute(configFileOptions.directory)) {
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 mergeOptions(defaultOptions, contentfulCliOptions, environmentOptions, configFileOptions, args || {});
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: 'input',
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
- default: function () {
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
- when(answers) {
135
- return answers.storage === STORAGE_TAG;
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
- when(answers) {
146
- return answers.storage === STORAGE_CONTENT;
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
- default: function () {
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
- return check;
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
- accessToken: managementToken,
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 && Object.keys(entryOverwrites).length ? '\n' : '';
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 { accessToken, host } = options || {};
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: string[]) => string[];
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
- const modifyValidations = (validations, method, key, values) =>
31
- validations.map((validation) => {
32
- if (validation?.[key]) {
33
- if (!Array.isArray(values)) {
34
- values = [values];
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
- if (!Array.isArray(validation[key])) {
37
- throw new Error(
38
- `modifying validation properties is only supported on arrays. validation.${key} is typeof ${typeof validation[
39
- key
40
- ]}`
41
- );
50
+ container = entry;
51
+ node = entry[key];
52
+ } else {
53
+ if (!node[key]) {
54
+ node[key] = isObjectNode ? {} : [];
42
55
  }
43
- validation[key] = method(validation[key], values);
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, validationKey, values),
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, validationKey, values));
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 { accessToken, requestBatchSize, spaceId } = config || {};
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": "5.6.2",
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": ">=14"
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",