@jungvonmatt/contentful-migrations 5.2.2 → 5.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -184,13 +184,25 @@ module.exports = withHelpers(async (migration, context, helpers) => {
184
184
  // Get default locale
185
185
  await helpers.locale.getDefaultLocale();
186
186
 
187
- // Add or remove values from "linkContentType" validations without affecting the other elements in the array
188
- await helpers.validation.addLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);
189
- await helpers.validation.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);
190
-
191
- // Add or remove values from "in" validations without affecting the other elements in the array
192
- await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value']);
187
+ // Add or remove values from "linkContentType" validations without knowing all the other elements in the array
188
+ await helpers.validation.addLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
189
+ await helpers.validation.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
190
+ await helpers.validation.modifyLinkContentTypeValues('contentTypeId', 'fieldId', (existing) => {
191
+ const result = existing.filter((contentType) => !contentType.startsWith('a-')); // remove some by prefix
192
+ result.push('t-test'); // and add one
193
+ return result; // possible duplicate values are removed afterwards
194
+ });
195
+
196
+ // Add or remove values from "in" validations without knowing all the other elements in the array
197
+ await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value']); // add at the end
198
+ await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value'], { mode: 'sorted'}); // add and sort
193
199
  await helpers.validation.removeInValues('contentTypeId', 'fieldId', ['value']);
200
+ await helpers.validation.modifyInValues('contentTypeId', 'fieldId', (existing) => {
201
+ const result = existing.filter((value) => value.startsWith('prefix')); // keep values with prefix
202
+ result.push('other'); // and add one
203
+ return result; // possible duplicate values are removed afterwards
204
+ });
205
+
194
206
  });
195
207
  ```
196
208
 
package/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type Migration, { MigrationContext, MigrationFunction } from "contentful-migration";
1
+ import type Migration from "contentful-migration";
2
+ import type { MigrationContext, MigrationFunction } from "contentful-migration";
2
3
  import type { LocaleHelpers } from "./lib/helpers/locale";
3
4
  import type { ValidationHelpers } from "./lib/helpers/validation";
4
5
 
package/index.js CHANGED
@@ -11,16 +11,16 @@
11
11
  * });
12
12
  *
13
13
  */
14
- const { getValidationHelpers } = require('./lib/helpers/validation');
15
- const { getLocaleHelpers } = require('./lib/helpers/locale');
14
+ const { getValidationHelpers } = require('./lib/helpers/validation');
15
+ const { getLocaleHelpers } = require('./lib/helpers/locale');
16
16
 
17
- // Export wrapper
18
- module.exports.withHelpers = (cb) => (migration, context) => {
19
- const locale = getLocaleHelpers(migration, context);
20
- const validation = getValidationHelpers(migration, context);
17
+ // Export wrapper
18
+ module.exports.withHelpers = (cb) => (migration, context) => {
19
+ const locale = getLocaleHelpers(migration, context);
20
+ const validation = getValidationHelpers(migration, context);
21
21
 
22
- return cb(migration, context, { locale, validation });
23
- };
22
+ return cb(migration, context, { locale, validation });
23
+ };
24
24
 
25
- module.exports.getValidationHelpers = getValidationHelpers;
26
- module.exports.getLocaleHelpers = getLocaleHelpers;
25
+ module.exports.getValidationHelpers = getValidationHelpers;
26
+ module.exports.getLocaleHelpers = getLocaleHelpers;
package/lib/backend.js CHANGED
@@ -2,7 +2,7 @@ const path = require('path');
2
2
  const fs = require('fs/promises');
3
3
  const chalk = require('chalk');
4
4
  const cliProgress = require('cli-progress');
5
- const { getEnvironment, getDefaultLocale } = require('./contentful');
5
+ const { getEnvironment, getDefaultLocale, getMigrationItems } = require('./contentful');
6
6
  const { STORAGE_TAG, STORAGE_CONTENT, STATE_SUCCESS, STATE_FAILURE } = require('./config');
7
7
 
8
8
  /**
@@ -193,8 +193,8 @@ const migrateToContentStorage = async (config) => {
193
193
  const client = await getEnvironment(config);
194
194
  const version = await getMigrationVersionFromTag(config);
195
195
 
196
- const {globby} = await import('globby');
197
- const migrations = await globby([`${directory}/*.js`,`${directory}/*.cjs`]);
196
+ const { globby } = await import('globby');
197
+ const migrations = await globby([`${directory}/*.js`, `${directory}/*.cjs`]);
198
198
  const environmentId = client.sys.id;
199
199
  const filtered = migrations.filter((file) => {
200
200
  const name = path.basename(file);
@@ -292,9 +292,7 @@ const migrateToTagStorage = async (config) => {
292
292
  * @returns {number[]}
293
293
  */
294
294
  const getMigrationVersions = async (config) => {
295
- const { migrationContentTypeId } = config;
296
- const client = await getEnvironment(config);
297
- const { items } = await client.getEntries({ content_type: migrationContentTypeId });
295
+ const items = await getMigrationItems(config);
298
296
 
299
297
  return (items || []).map((item) => parseInt(item.sys.id, 10));
300
298
  };
@@ -353,11 +351,11 @@ const getLatestVersion = async (config) => {
353
351
  return getMigrationVersionFromTag(config);
354
352
  };
355
353
 
356
- const getVersionFromFile = file => {
354
+ const getVersionFromFile = (file) => {
357
355
  const name = path.basename(file);
358
356
  const [, num] = /^(\d+)-/.exec(name);
359
- return parseInt(num,10);
360
- }
357
+ return parseInt(num, 10);
358
+ };
361
359
 
362
360
  /**
363
361
  * Get all unexecuted migration files
@@ -365,17 +363,16 @@ const getVersionFromFile = file => {
365
363
  */
366
364
  const getNewMigrations = async (config) => {
367
365
  const { directory, storage, migrationContentTypeId } = config || {};
368
- const {globby} = await import('globby');
369
- const migrations = (await globby([`${directory}/*.js`,`${directory}/*.cjs`])).sort((a,b) => {
366
+ const { globby } = await import('globby');
367
+ const migrations = (await globby([`${directory}/*.js`, `${directory}/*.cjs`])).sort((a, b) => {
370
368
  const numA = getVersionFromFile(a);
371
369
  const numB = getVersionFromFile(b);
372
- return numA-numB;
370
+ return numA - numB;
373
371
  });
374
372
 
375
373
  if (storage === STORAGE_CONTENT) {
376
374
  try {
377
375
  const versions = await getMigrationVersions(config);
378
-
379
376
  const result = migrations.filter((file) => {
380
377
  const num = getVersionFromFile(file);
381
378
  return !(versions || []).includes(num);
@@ -384,10 +381,14 @@ const getNewMigrations = async (config) => {
384
381
  return result;
385
382
  } catch (error) {
386
383
  // check if we have a migration scheduled which adds the initial content-type
387
- const regexp = new RegExp(`createContentType\\(['"]${migrationContentTypeId}['"]\\)`,'mg');
388
- const initial = (await Promise.all(migrations.map(async file => {
389
- return fs.readFile(file, 'utf8');
390
- }))).some(content => regexp.test(content));
384
+ const regexp = new RegExp(`createContentType\\(['"]${migrationContentTypeId}['"]\\)`, 'mg');
385
+ const initial = (
386
+ await Promise.all(
387
+ migrations.map(async (file) => {
388
+ return fs.readFile(file, 'utf8');
389
+ })
390
+ )
391
+ ).some((content) => regexp.test(content));
391
392
 
392
393
  if (initial) {
393
394
  return migrations;
package/lib/contentful.js CHANGED
@@ -215,11 +215,9 @@ const pagedGet = async (environment, { method, skip = 0, aggregatedResponse = nu
215
215
  } else {
216
216
  aggregatedResponse.items = aggregatedResponse.items.concat(response.items);
217
217
  }
218
- // const page = Math.ceil(skip / MAX_ALLOWED_LIMIT) + 1;
219
- // const pages = Math.ceil(response.total / MAX_ALLOWED_LIMIT);
220
218
 
221
- if (skip + MAX_ALLOWED_LIMIT <= response.total) {
222
- return pagedGet(environment, { method, skip: skip + MAX_ALLOWED_LIMIT, aggregatedResponse, query });
219
+ if (skip + fullQuery.limit <= response.total) {
220
+ return pagedGet(environment, { method, skip: skip + fullQuery.limit, aggregatedResponse, query });
223
221
  }
224
222
  return aggregatedResponse;
225
223
  };
@@ -316,6 +314,17 @@ const getContent = async (options) => {
316
314
  return { entries, assets, contentTypes };
317
315
  };
318
316
 
317
+ const getMigrationItems = async (options) => {
318
+ const { migrationContentTypeId } = options;
319
+ const environment = await getEnvironment(options);
320
+ const { items } = await pagedGet(environment, {
321
+ method: 'getEntries',
322
+ query: { content_type: migrationContentTypeId },
323
+ });
324
+
325
+ return items;
326
+ };
327
+
319
328
  const getContentTypes = async (options) => {
320
329
  const { contentType } = options;
321
330
  const environment = await getEnvironment(options);
@@ -354,6 +363,7 @@ exports.getNodeName = getNodeName;
354
363
  exports.getEnvironmentId = getEnvironmentId;
355
364
  exports.getEditorInterfaces = getEditorInterfaces;
356
365
  exports.getApiKeys = getApiKeys;
366
+ exports.getMigrationItems = getMigrationItems;
357
367
 
358
368
  // Constants
359
369
  exports.TYPE_SYMBOL = TYPE_SYMBOL;
@@ -1,8 +1,9 @@
1
1
  import type { Locale } from "contentful-management/dist/typings/export-types";
2
- import type Migration, { MigrationContext } from "contentful-migration";
2
+ import type Migration from "contentful-migration";
3
+ import type { MigrationContext } from "contentful-migration";
3
4
 
4
5
  export interface LocaleHelpers {
5
- getLocales(): Promise<[Locale]>;
6
+ getLocales(): Promise<Locale[]>;
6
7
  getDefaultLocale(): Promise<Locale>;
7
8
  }
8
9
 
@@ -1,10 +1,22 @@
1
- import type Migration, { MigrationContext } from "contentful-migration";
1
+ import type Migration from "contentful-migration";
2
+ import type { MigrationContext } from "contentful-migration";
3
+
4
+ export type ValueMappingFunction = (values: string[]) => string[];
5
+
6
+ export type AddValuesOptionMode = 'sorted' | 'start' | 'end' | 'before' | 'after';
7
+
8
+ export interface AddValuesOptions {
9
+ mode?: AddValuesOptionMode;
10
+ ref?: string;
11
+ }
2
12
 
3
13
  export interface ValidationHelpers {
4
- addLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
5
- addInValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
6
- removeLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
7
- removeInValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
14
+ addLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
15
+ addInValues(contentTypeId: string, fieldId: string, values: string | string[], options?: AddValuesOptions): Promise<void>;
16
+ removeLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
17
+ removeInValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
18
+ modifyLinkContentTypeValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction): Promise<void>;
19
+ modifyInValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction): Promise<void>;
8
20
  }
9
21
 
10
22
  export function getValidationHelpers(migration: Migration, context: MigrationContext): ValidationHelpers;
@@ -1,4 +1,5 @@
1
- const { TYPE_LINK, TYPE_ARRAY } = require('../contentful');
1
+ const { TYPE_ARRAY } = require('../contentful');
2
+ const { addValues, unique } = require('./validation.utils');
2
3
 
3
4
  /**
4
5
  * Adds utils for the migration
@@ -10,9 +11,9 @@ const { TYPE_LINK, TYPE_ARRAY } = require('../contentful');
10
11
  * const validationHelper = getValidationHelpers(migration, context);
11
12
  * ...
12
13
  *
13
- * await validationHelper.addLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);
14
+ * await validationHelper.addLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
14
15
  * await validationHelper.addInValues('contentTypeId', 'fieldId', ['value']);
15
- * await validationHelper.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);
16
+ * await validationHelper.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
16
17
  * await validationHelper.removeInValues('contentTypeId', 'fieldId', ['value']);
17
18
  *
18
19
  * };
@@ -21,24 +22,12 @@ const { TYPE_LINK, TYPE_ARRAY } = require('../contentful');
21
22
  const getValidationHelpers = (migration, context) => {
22
23
  const { makeRequest } = context;
23
24
 
24
- const addValidationValues = (validations, key, values = []) =>
25
- validations.map((validation) => {
26
- if (validation?.[key]) {
27
- if (!Array.isArray(values)) {
28
- values = [values];
29
- }
30
- if (!Array.isArray(validation[key])) {
31
- throw new Error(
32
- `addValidationValues can only be used on arrays. validation.${key} is typeof ${typeof validation[key]}`
33
- );
34
- }
35
- validation[key] = [...new Set([...validation[key], ...values])];
36
- }
25
+ const addValidationValues = (existingValues, newValues = []) => unique([...existingValues, ...newValues]);
37
26
 
38
- return validation;
39
- });
27
+ const removeValidationValues = (existingValues, newValues = []) =>
28
+ existingValues.filter((x) => !newValues.includes(x));
40
29
 
41
- const removeValidationValues = (validations, key, values = []) =>
30
+ const modifyValidations = (validations, method, key, values) =>
42
31
  validations.map((validation) => {
43
32
  if (validation?.[key]) {
44
33
  if (!Array.isArray(values)) {
@@ -46,16 +35,18 @@ const getValidationHelpers = (migration, context) => {
46
35
  }
47
36
  if (!Array.isArray(validation[key])) {
48
37
  throw new Error(
49
- `removeValidationValues can only be used on arrays. validation.${key} is typeof ${typeof validation[key]}`
38
+ `modifying validation properties is only supported on arrays. validation.${key} is typeof ${typeof validation[
39
+ key
40
+ ]}`
50
41
  );
51
42
  }
52
- validation[key] = validation[key].filter((x) => !values.includes(x));
43
+ validation[key] = method(validation[key], values);
53
44
  }
54
45
 
55
46
  return validation;
56
47
  });
57
48
 
58
- const modifyValidationValuesForType = async (validationKey, method, contentTypeId, fieldId, typeIds) => {
49
+ const modifyValidationValuesForType = async (validationKey, method, contentTypeId, fieldId, values) => {
59
50
  // Fetch content type
60
51
  const { fields } = await makeRequest({
61
52
  method: 'GET',
@@ -63,32 +54,71 @@ const getValidationHelpers = (migration, context) => {
63
54
  });
64
55
 
65
56
  const { type, items = {}, validations = [] } = fields?.find((field) => field.id === fieldId) ?? {};
57
+ if (type === undefined) {
58
+ throw new Error(`Content type ${contentTypeId} has no field ${fieldId}`);
59
+ }
66
60
 
67
61
  if (type === TYPE_ARRAY) {
68
62
  const ct = migration.editContentType(contentTypeId);
69
- ct.editField(fieldId).items({ ...items, validations: method(items?.validations ?? [], validationKey, typeIds) });
63
+ ct.editField(fieldId).items({
64
+ ...items,
65
+ validations: modifyValidations(items?.validations, method, validationKey, values),
66
+ });
70
67
  } else {
71
68
  const ct = migration.editContentType(contentTypeId);
72
- ct.editField(fieldId).validations(method(validations ?? [], validationKey, typeIds));
69
+ ct.editField(fieldId).validations(modifyValidations(validations, method, validationKey, values));
73
70
  }
74
71
  };
75
72
 
76
73
  return {
74
+ /**
75
+ * Add the specified values to the list of allowed content type values
76
+ */
77
77
  async addLinkContentTypeValues(contentTypeId, fieldId, values) {
78
78
  await modifyValidationValuesForType('linkContentType', addValidationValues, contentTypeId, fieldId, values);
79
79
  },
80
80
 
81
- async addInValues(contentTypeId, fieldId, values) {
82
- await modifyValidationValuesForType('in', addValidationValues, contentTypeId, fieldId, values);
81
+ /**
82
+ * Add the specified values to the list of allowed values
83
+ * @param {string} options.mode The mode how to add the values (sorted, start, end, before, after)
84
+ * @param {string|undefined} options.ref The reference value for mode "before" and "after"
85
+ */
86
+ async addInValues(contentTypeId, fieldId, values, options = {}) {
87
+ const addValuesWithOptions = (existingValues, newValues = []) => addValues(existingValues, newValues, options);
88
+ await modifyValidationValuesForType('in', addValuesWithOptions, contentTypeId, fieldId, values);
83
89
  },
84
90
 
91
+ /**
92
+ * Remove the specified values from the list of allowed content type values
93
+ */
85
94
  async removeLinkContentTypeValues(contentTypeId, fieldId, values) {
86
95
  await modifyValidationValuesForType('linkContentType', removeValidationValues, contentTypeId, fieldId, values);
87
96
  },
88
97
 
98
+ /**
99
+ * Remove the specified values from the list of allowed values
100
+ */
89
101
  async removeInValues(contentTypeId, fieldId, values) {
90
102
  await modifyValidationValuesForType('in', removeValidationValues, contentTypeId, fieldId, values);
91
103
  },
104
+
105
+ /**
106
+ * Modifies the list of allowed content types by calling the valueMappingFunction with the existing values and
107
+ * setting the result as the new value list.
108
+ */
109
+ async modifyLinkContentTypeValues(contentTypeId, fieldId, valueMappingFunction) {
110
+ const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
111
+ await modifyValidationValuesForType('linkContentType', uniqueMappingFunction, contentTypeId, fieldId, []);
112
+ },
113
+
114
+ /**
115
+ * Modifies the list of allowed values by calling the valueMappingFunction with the existing values and setting the
116
+ * result as the new value list.
117
+ */
118
+ async modifyInValues(contentTypeId, fieldId, valueMappingFunction) {
119
+ const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
120
+ await modifyValidationValuesForType('in', uniqueMappingFunction, contentTypeId, fieldId, []);
121
+ },
92
122
  };
93
123
  };
94
124
 
@@ -0,0 +1,171 @@
1
+ const { getValidationHelpers } = require('./validation');
2
+
3
+ describe('getValidationHelpers', () => {
4
+ const getTestContentType = () => ({
5
+ fields: [
6
+ {
7
+ id: 'selectWithOtherValidationProps',
8
+ type: 'Text',
9
+ items: [],
10
+ validations: [
11
+ {
12
+ foo: 'test',
13
+ },
14
+ {
15
+ in: ['foo', 'bar', 'baz'],
16
+ message: 'Some message',
17
+ },
18
+ ],
19
+ },
20
+ {
21
+ id: 'select',
22
+ type: 'Text',
23
+ items: [],
24
+ validations: [
25
+ {
26
+ in: ['foo', 'bar', 'baz'],
27
+ },
28
+ ],
29
+ },
30
+ {
31
+ id: 'reference',
32
+ type: 'Text',
33
+ validations: [
34
+ {
35
+ linkContentType: ['foo', 'bar', 'baz'],
36
+ },
37
+ ],
38
+ },
39
+ {
40
+ id: 'string-array',
41
+ type: 'Array',
42
+ items: [],
43
+ },
44
+ ],
45
+ });
46
+
47
+ const context = {
48
+ makeRequest: () => Promise.resolve(getTestContentType()),
49
+ };
50
+
51
+ let resultValidation;
52
+
53
+ const migration = {
54
+ editContentType: () => ({
55
+ editField: () => ({
56
+ validations: (validationObject) => {
57
+ resultValidation = validationObject;
58
+ },
59
+ }),
60
+ }),
61
+ };
62
+
63
+ beforeEach(() => {
64
+ resultValidation = undefined;
65
+ });
66
+
67
+ describe('addInValues', () => {
68
+ it('should add values at the end', async () => {
69
+ const validations = getValidationHelpers(migration, context);
70
+ await validations.addInValues('some-content-type', 'selectWithOtherValidationProps', 'bat');
71
+ expect(resultValidation).toEqual([
72
+ {
73
+ foo: 'test',
74
+ },
75
+ {
76
+ in: ['foo', 'bar', 'baz', 'bat'],
77
+ message: 'Some message',
78
+ },
79
+ ]);
80
+ });
81
+
82
+ it('should only add new values', async () => {
83
+ const validations = getValidationHelpers(migration, context);
84
+ await validations.addInValues('some-content-type', 'select', ['bat', 'foo', 'test']);
85
+ expect(resultValidation).toEqual([
86
+ {
87
+ in: ['foo', 'bar', 'baz', 'bat', 'test'],
88
+ },
89
+ ]);
90
+ });
91
+
92
+ it('should add new values sorted', async () => {
93
+ const validations = getValidationHelpers(migration, context);
94
+ await validations.addInValues('some-content-type', 'select', ['bat', 'foo', 'test'], { mode: 'sorted' });
95
+ expect(resultValidation).toEqual([
96
+ {
97
+ in: ['bar', 'bat', 'baz', 'foo', 'test'],
98
+ },
99
+ ]);
100
+ });
101
+ });
102
+
103
+ describe('removeInValues', () => {
104
+ it('should remove values', async () => {
105
+ const validations = getValidationHelpers(migration, context);
106
+ await validations.removeInValues('some-content-type', 'select', ['foo', 'baz']);
107
+ expect(resultValidation).toEqual([
108
+ {
109
+ in: ['bar'],
110
+ },
111
+ ]);
112
+ });
113
+ });
114
+
115
+ describe('modifyInValues', () => {
116
+ it('should modify values with custom function', async () => {
117
+ const validations = getValidationHelpers(migration, context);
118
+ await validations.modifyInValues('some-content-type', 'select', (values) => {
119
+ const result = values.slice(0, values.length - 1); // remove bar
120
+ result.push('test');
121
+ return result;
122
+ });
123
+ expect(resultValidation).toEqual([
124
+ {
125
+ in: ['foo', 'bar', 'test'],
126
+ },
127
+ ]);
128
+ });
129
+ });
130
+
131
+ describe('addLinkContentTypeValues', () => {
132
+ it('should add values at the end', async () => {
133
+ const validations = getValidationHelpers(migration, context);
134
+ await validations.addLinkContentTypeValues('some-content-type', 'reference', 'bat');
135
+ expect(resultValidation).toEqual([
136
+ {
137
+ linkContentType: ['foo', 'bar', 'baz', 'bat'],
138
+ },
139
+ ]);
140
+ });
141
+ });
142
+
143
+ describe('removeLinkContentTypeValues', () => {
144
+ it('should remove values', async () => {
145
+ const validations = getValidationHelpers(migration, context);
146
+ await validations.removeLinkContentTypeValues('some-content-type', 'reference', ['foo', 'baz']);
147
+ expect(resultValidation).toEqual([
148
+ {
149
+ linkContentType: ['bar'],
150
+ },
151
+ ]);
152
+ });
153
+ });
154
+
155
+ describe('modifyLinkContentTypeValues', () => {
156
+ it('should modify unique values with custom function', async () => {
157
+ const validations = getValidationHelpers(migration, context);
158
+ await validations.modifyLinkContentTypeValues('some-content-type', 'reference', (values) => {
159
+ const result = values.slice(0, values.length - 1); // remove bar
160
+ result.push('test');
161
+ result.push('foo'); // should be removed since it exists
162
+ return result;
163
+ });
164
+ expect(resultValidation).toEqual([
165
+ {
166
+ linkContentType: ['foo', 'bar', 'test'],
167
+ },
168
+ ]);
169
+ });
170
+ });
171
+ });
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Returns a new array containing only the unique values of the given array.
3
+ * The given order of the elements is kept.
4
+ * If duplicates exist the first occurrence is kept and the others are removed.
5
+ */
6
+ const unique = (array) => [...new Set(array)];
7
+
8
+ /**
9
+ * Returns an array with the unique items of both arrays sorted by natural ordering.
10
+ * @param {Array} array1
11
+ * @param {Array} array2
12
+ */
13
+ const addOrdered = (array1, array2) => {
14
+ const result = [...new Set([...array1, ...array2])];
15
+ result.sort();
16
+ return result;
17
+ };
18
+
19
+ const ADD_MODE_SORTED = 'sorted';
20
+ const ADD_MODE_START = 'start';
21
+ const ADD_MODE_END = 'end';
22
+ const ADD_MODE_BEFORE = 'before';
23
+ const ADD_MODE_AFTER = 'after'; // default
24
+
25
+ /**
26
+ * Get the insert position depending on the options.
27
+ * @param {string[]} array The array where values should be added.
28
+ * @param {string} options.mode The mode how to add the values (sorted, start, end, before, after)
29
+ * @param {string|undefined} options.ref The reference value for mode "before" and "after"
30
+ * @returns {number}
31
+ */
32
+ const getInsertPosition = (array, options = {}) => {
33
+ if (options.mode === ADD_MODE_START) {
34
+ return 0;
35
+ }
36
+ if (options.mode === ADD_MODE_END) {
37
+ return array.length;
38
+ }
39
+
40
+ let refIndex = array.indexOf(options.ref);
41
+ if (refIndex >= 0) {
42
+ if (options.mode === ADD_MODE_BEFORE) {
43
+ return refIndex;
44
+ }
45
+ if (options.mode === ADD_MODE_AFTER) {
46
+ return refIndex + 1;
47
+ }
48
+ }
49
+
50
+ // default at the end
51
+ return array.length;
52
+ };
53
+
54
+ /**
55
+ * Adds values to a string array depending on the specified mode.
56
+ * The result list always contains unique values by calling the unique function.
57
+ * @param {string[]} array The array where values should be added.
58
+ * @param {string[]} toAdd The list of values to add.
59
+ * @param {string} options.mode The mode how to add the values (sorted, start, end, before, after)
60
+ * @param {string|undefined} options.ref The reference value for mode "before" and "after"
61
+ * @returns {string[]}
62
+ */
63
+ const addValues = (array, toAdd, options = {}) => {
64
+ if (options.mode === ADD_MODE_SORTED) {
65
+ return addOrdered(array, toAdd);
66
+ }
67
+
68
+ let position = getInsertPosition(array, options);
69
+ return unique([...array.slice(0, position), ...toAdd, ...array.slice(position)]);
70
+ };
71
+
72
+ module.exports = {
73
+ addOrdered,
74
+ addValues,
75
+ unique,
76
+ };
@@ -0,0 +1,45 @@
1
+ const { addOrdered, addValues, unique } = require('./validation.utils');
2
+
3
+ describe('unique', () => {
4
+ it('should remove duplicates', async () => {
5
+ expect(unique(['foo', 'bar', 'foo', 'baz', 'bar'])).toEqual(['foo', 'bar', 'baz']);
6
+ });
7
+ });
8
+
9
+ describe('addOrdered', () => {
10
+ const a1 = ['foo', 'bar'];
11
+ const a2 = ['baz', 'bar', 'bat'];
12
+
13
+ it('should add unique values and order the list', async () => {
14
+ expect(addOrdered(a1, a2)).toEqual(['bar', 'bat', 'baz', 'foo']);
15
+ });
16
+ });
17
+
18
+ describe('addValues', () => {
19
+ const a1 = ['foo', 'bar', 'test'];
20
+ const a2 = ['baz', 'bar', 'bat'];
21
+
22
+ it('should add values sorted', async () => {
23
+ expect(addValues(a1, a2, { mode: 'sorted' })).toEqual(['bar', 'bat', 'baz', 'foo', 'test']);
24
+ });
25
+
26
+ it('should add values at start', async () => {
27
+ expect(addValues(a1, a2, { mode: 'start' })).toEqual(['baz', 'bar', 'bat', 'foo', 'test']);
28
+ });
29
+
30
+ it('should add values at end', async () => {
31
+ expect(addValues(a1, a2, { mode: 'end' })).toEqual(['foo', 'bar', 'test', 'baz', 'bat']);
32
+ });
33
+
34
+ it('should add values before', async () => {
35
+ expect(addValues(a1, a2, { mode: 'before', ref: 'bar' })).toEqual(['foo', 'baz', 'bar', 'bat', 'test']);
36
+ });
37
+
38
+ it('should add values before fallback', async () => {
39
+ expect(addValues(a1, a2, { mode: 'before', ref: 'unknown' })).toEqual(['foo', 'bar', 'test', 'baz', 'bat']);
40
+ });
41
+
42
+ it('should add values after', async () => {
43
+ expect(addValues(a1, a2, { mode: 'after', ref: 'bar' })).toEqual(['foo', 'bar', 'baz', 'bat', 'test']);
44
+ });
45
+ });
package/lib/migration.js CHANGED
@@ -93,10 +93,12 @@ const fetchMigration = async (config) => {
93
93
  .toString()
94
94
  // Add call to utils.getDefaultLocale() to the top
95
95
  .replace(
96
- 'module.exports = function (migration) {',`
96
+ 'module.exports = function (migration) {',
97
+ `
97
98
  ${migrationHeader}
98
99
  const defaultLocale = helpers.locale.getDefaultLocale();
99
- `)
100
+ `
101
+ )
100
102
  // Replace the default locale with defaultLocale.code so that the migration
101
103
  // still works as expected when the locale is changed in contentful
102
104
  .replace(testDefaultValueRegex, '$1[defaultLocale.code]$2')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungvonmatt/contentful-migrations",
3
- "version": "5.2.2",
3
+ "version": "5.3.0",
4
4
  "description": "Helper to handle migrations in contentful",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -15,8 +15,9 @@
15
15
  "migrations": "cli.js"
16
16
  },
17
17
  "scripts": {
18
- "test": "npm run lint",
19
- "lint": "eslint . --ext .js,.ts,.tsx --ignore-path .gitignore"
18
+ "test": "jest --coverage && npm run lint",
19
+ "lint": "eslint . --ext .js,.ts,.tsx --ignore-path .gitignore",
20
+ "fix-lint": "npm run lint -- --fix"
20
21
  },
21
22
  "repository": {
22
23
  "type": "git",
@@ -61,11 +62,13 @@
61
62
  "read-pkg-up": "^9.0.0"
62
63
  },
63
64
  "devDependencies": {
64
- "babel-eslint": "^10.1.0",
65
+ "@babel/eslint-parser": "^7.17.0",
65
66
  "eslint": "^8.3.0",
66
67
  "eslint-config-prettier": "^8.3.0",
67
68
  "eslint-plugin-prettier": "^4.0.0",
68
69
  "husky": "^7.0.4",
70
+ "jest": "^27.5.1",
71
+ "jest-sonar-reporter": "^2.0.0",
69
72
  "lint-staged": "^12.1.2"
70
73
  },
71
74
  "publishConfig": {
@@ -76,6 +79,9 @@
76
79
  "printWidth": 120
77
80
  },
78
81
  "eslintConfig": {
82
+ "ignorePatterns": [
83
+ "**/*.d.ts"
84
+ ],
79
85
  "plugins": [
80
86
  "prettier"
81
87
  ],
@@ -85,8 +91,9 @@
85
91
  "extends": [
86
92
  "prettier"
87
93
  ],
88
- "parser": "babel-eslint",
94
+ "parser": "@babel/eslint-parser",
89
95
  "parserOptions": {
96
+ "requireConfigFile": false,
90
97
  "allowImportExportEverywhere": true,
91
98
  "ecmaFeatures": {
92
99
  "ecmaVersion": 2017,
@@ -106,6 +113,29 @@
106
113
  "prettier --write"
107
114
  ]
108
115
  },
116
+ "jest": {
117
+ "testResultsProcessor": "jest-sonar-reporter",
118
+ "coverageDirectory": "<rootDir>/__coverage__/",
119
+ "collectCoverageFrom": [
120
+ "<rootDir>/lib/*/**"
121
+ ],
122
+ "roots": [
123
+ "<rootDir>/lib/"
124
+ ],
125
+ "moduleFileExtensions": [
126
+ "js"
127
+ ],
128
+ "testRegex": "/lib/.*\\.test.js$",
129
+ "moduleDirectories": [
130
+ "node_modules"
131
+ ],
132
+ "globals": {
133
+ "DEVELOPMENT": false
134
+ },
135
+ "reporters": [
136
+ "default"
137
+ ]
138
+ },
109
139
  "husky": {
110
140
  "hooks": {
111
141
  "pre-commit": "lint-staged"
@@ -115,5 +145,10 @@
115
145
  "storage": "content",
116
146
  "migrationContentTypeId": "contentful-migrations",
117
147
  "directory": "migrations"
148
+ },
149
+ "jestSonar": {
150
+ "reportPath": "__coverage__",
151
+ "reportFile": "test-report.xml",
152
+ "indent": 2
118
153
  }
119
154
  }