Skip to content

Commit

Permalink
add validation helper modify funcs and options for addInValues (#30)
Browse files Browse the repository at this point in the history
Also add the unit test setup
  • Loading branch information
tharders authored Apr 26, 2022
1 parent adde319 commit d29aa0a
Show file tree
Hide file tree
Showing 10 changed files with 6,278 additions and 596 deletions.
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,25 @@ module.exports = withHelpers(async (migration, context, helpers) => {
// Get default locale
await helpers.locale.getDefaultLocale();

// Add or remove values from "linkContentType" validations without affecting the other elements in the array
await helpers.validation.addLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);
await helpers.validation.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);

// Add or remove values from "in" validations without affecting the other elements in the array
await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value']);
// Add or remove values from "linkContentType" validations without knowing all the other elements in the array
await helpers.validation.addLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
await helpers.validation.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
await helpers.validation.modifyLinkContentTypeValues('contentTypeId', 'fieldId', (existing) => {
const result = existing.filter((contentType) => !contentType.startsWith('a-')); // remove some by prefix
result.push('t-test'); // and add one
return result; // possible duplicate values are removed afterwards
});

// Add or remove values from "in" validations without knowing all the other elements in the array
await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value']); // add at the end
await helpers.validation.addInValues('contentTypeId', 'fieldId', ['value'], { mode: 'sorted'}); // add and sort
await helpers.validation.removeInValues('contentTypeId', 'fieldId', ['value']);
await helpers.validation.modifyInValues('contentTypeId', 'fieldId', (existing) => {
const result = existing.filter((value) => value.startsWith('prefix')); // keep values with prefix
result.push('other'); // and add one
return result; // possible duplicate values are removed afterwards
});

});
```

Expand Down
3 changes: 2 additions & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type Migration, { MigrationContext, MigrationFunction } from "contentful-migration";
import type Migration from "contentful-migration";
import type { MigrationContext, MigrationFunction } from "contentful-migration";
import type { LocaleHelpers } from "./lib/helpers/locale";
import type { ValidationHelpers } from "./lib/helpers/validation";

Expand Down
5 changes: 3 additions & 2 deletions lib/helpers/locale.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Locale } from "contentful-management/dist/typings/export-types";
import type Migration, { MigrationContext } from "contentful-migration";
import type Migration from "contentful-migration";
import type { MigrationContext } from "contentful-migration";

export interface LocaleHelpers {
getLocales(): Promise<[Locale]>;
getLocales(): Promise<Locale[]>;
getDefaultLocale(): Promise<Locale>;
}

Expand Down
22 changes: 17 additions & 5 deletions lib/helpers/validation.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
import type Migration, { MigrationContext } from "contentful-migration";
import type Migration from "contentful-migration";
import type { MigrationContext } from "contentful-migration";

export type ValueMappingFunction = (values: string[]) => string[];

export type AddValuesOptionMode = 'sorted' | 'start' | 'end' | 'before' | 'after';

export interface AddValuesOptions {
mode?: AddValuesOptionMode;
ref?: string;
}

export interface ValidationHelpers {
addLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
addInValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
removeLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
removeInValues(contentTypeId: string, fieldId: string, values: string | [string]): Promise<void>;
addLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
addInValues(contentTypeId: string, fieldId: string, values: string | string[], options?: AddValuesOptions): Promise<void>;
removeLinkContentTypeValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
removeInValues(contentTypeId: string, fieldId: string, values: string | string[]): Promise<void>;
modifyLinkContentTypeValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction): Promise<void>;
modifyInValues(contentTypeId: string, fieldId: string, valueMappingFunction: ValueMappingFunction): Promise<void>;
}

export function getValidationHelpers(migration: Migration, context: MigrationContext): ValidationHelpers;
82 changes: 56 additions & 26 deletions lib/helpers/validation.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { TYPE_LINK, TYPE_ARRAY } = require('../contentful');
const { TYPE_ARRAY } = require('../contentful');
const { addValues, unique } = require('./validation.utils');

/**
* Adds utils for the migration
Expand All @@ -10,9 +11,9 @@ const { TYPE_LINK, TYPE_ARRAY } = require('../contentful');
* const validationHelper = getValidationHelpers(migration, context);
* ...
*
* await validationHelper.addLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);
* await validationHelper.addLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
* await validationHelper.addInValues('contentTypeId', 'fieldId', ['value']);
* await validationHelper.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['value']);
* await validationHelper.removeLinkContentTypeValues('contentTypeId', 'fieldId', ['some-content-type']);
* await validationHelper.removeInValues('contentTypeId', 'fieldId', ['value']);
*
* };
Expand All @@ -21,74 +22,103 @@ const { TYPE_LINK, TYPE_ARRAY } = require('../contentful');
const getValidationHelpers = (migration, context) => {
const { makeRequest } = context;

const addValidationValues = (validations, key, values = []) =>
validations.map((validation) => {
if (validation?.[key]) {
if (!Array.isArray(values)) {
values = [values];
}
if (!Array.isArray(validation[key])) {
throw new Error(
`addValidationValues can only be used on arrays. validation.${key} is typeof ${typeof validation[key]}`
);
}
validation[key] = [...new Set([...validation[key], ...values])];
}
const addValidationValues = (existingValues, newValues = []) => unique([...existingValues, ...newValues]);

return validation;
});
const removeValidationValues = (existingValues, newValues = []) =>
existingValues.filter((x) => !newValues.includes(x));

const removeValidationValues = (validations, key, values = []) =>
const modifyValidations = (validations, method, key, values) =>
validations.map((validation) => {
if (validation?.[key]) {
if (!Array.isArray(values)) {
values = [values];
}
if (!Array.isArray(validation[key])) {
throw new Error(
`removeValidationValues can only be used on arrays. validation.${key} is typeof ${typeof validation[key]}`
`modifying validation properties is only supported on arrays. validation.${key} is typeof ${typeof validation[
key
]}`
);
}
validation[key] = validation[key].filter((x) => !values.includes(x));
validation[key] = method(validation[key], values);
}

return validation;
});

const modifyValidationValuesForType = async (validationKey, method, contentTypeId, fieldId, typeIds) => {
const modifyValidationValuesForType = async (validationKey, method, contentTypeId, fieldId, values) => {
// Fetch content type
const { fields } = await makeRequest({
method: 'GET',
url: `/content_types/${contentTypeId}`,
});

const { type, items = {}, validations = [] } = fields?.find((field) => field.id === fieldId) ?? {};
if (type === undefined) {
throw new Error(`Content type ${contentTypeId} has no field ${fieldId}`);
}

if (type === TYPE_ARRAY) {
const ct = migration.editContentType(contentTypeId);
ct.editField(fieldId).items({ ...items, validations: method(items?.validations ?? [], validationKey, typeIds) });
ct.editField(fieldId).items({
...items,
validations: modifyValidations(items?.validations, method, validationKey, values),
});
} else {
const ct = migration.editContentType(contentTypeId);
ct.editField(fieldId).validations(method(validations ?? [], validationKey, typeIds));
ct.editField(fieldId).validations(modifyValidations(validations, method, validationKey, values));
}
};

return {
/**
* Add the specified values to the list of allowed content type values
*/
async addLinkContentTypeValues(contentTypeId, fieldId, values) {
await modifyValidationValuesForType('linkContentType', addValidationValues, contentTypeId, fieldId, values);
},

async addInValues(contentTypeId, fieldId, values) {
await modifyValidationValuesForType('in', addValidationValues, contentTypeId, fieldId, values);
/**
* Add the specified values to the list of allowed values
* @param {string} options.mode The mode how to add the values (sorted, start, end, before, after)
* @param {string|undefined} options.ref The reference value for mode "before" and "after"
*/
async addInValues(contentTypeId, fieldId, values, options = {}) {
const addValuesWithOptions = (existingValues, newValues = []) => addValues(existingValues, newValues, options);
await modifyValidationValuesForType('in', addValuesWithOptions, contentTypeId, fieldId, values);
},

/**
* Remove the specified values from the list of allowed content type values
*/
async removeLinkContentTypeValues(contentTypeId, fieldId, values) {
await modifyValidationValuesForType('linkContentType', removeValidationValues, contentTypeId, fieldId, values);
},

/**
* Remove the specified values from the list of allowed values
*/
async removeInValues(contentTypeId, fieldId, values) {
await modifyValidationValuesForType('in', removeValidationValues, contentTypeId, fieldId, values);
},

/**
* Modifies the list of allowed content types by calling the valueMappingFunction with the existing values and
* setting the result as the new value list.
*/
async modifyLinkContentTypeValues(contentTypeId, fieldId, valueMappingFunction) {
const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
await modifyValidationValuesForType('linkContentType', uniqueMappingFunction, contentTypeId, fieldId, []);
},

/**
* Modifies the list of allowed values by calling the valueMappingFunction with the existing values and setting the
* result as the new value list.
*/
async modifyInValues(contentTypeId, fieldId, valueMappingFunction) {
const uniqueMappingFunction = (values) => unique(valueMappingFunction(values));
await modifyValidationValuesForType('in', uniqueMappingFunction, contentTypeId, fieldId, []);
},
};
};

Expand Down
171 changes: 171 additions & 0 deletions lib/helpers/validation.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
const { getValidationHelpers } = require('./validation');

describe('getValidationHelpers', () => {
const getTestContentType = () => ({
fields: [
{
id: 'selectWithOtherValidationProps',
type: 'Text',
items: [],
validations: [
{
foo: 'test',
},
{
in: ['foo', 'bar', 'baz'],
message: 'Some message',
},
],
},
{
id: 'select',
type: 'Text',
items: [],
validations: [
{
in: ['foo', 'bar', 'baz'],
},
],
},
{
id: 'reference',
type: 'Text',
validations: [
{
linkContentType: ['foo', 'bar', 'baz'],
},
],
},
{
id: 'string-array',
type: 'Array',
items: [],
},
],
});

const context = {
makeRequest: () => Promise.resolve(getTestContentType()),
};

let resultValidation;

const migration = {
editContentType: () => ({
editField: () => ({
validations: (validationObject) => {
resultValidation = validationObject;
},
}),
}),
};

beforeEach(() => {
resultValidation = undefined;
});

describe('addInValues', () => {
it('should add values at the end', async () => {
const validations = getValidationHelpers(migration, context);
await validations.addInValues('some-content-type', 'selectWithOtherValidationProps', 'bat');
expect(resultValidation).toEqual([
{
foo: 'test',
},
{
in: ['foo', 'bar', 'baz', 'bat'],
message: 'Some message',
},
]);
});

it('should only add new values', async () => {
const validations = getValidationHelpers(migration, context);
await validations.addInValues('some-content-type', 'select', ['bat', 'foo', 'test']);
expect(resultValidation).toEqual([
{
in: ['foo', 'bar', 'baz', 'bat', 'test'],
},
]);
});

it('should add new values sorted', async () => {
const validations = getValidationHelpers(migration, context);
await validations.addInValues('some-content-type', 'select', ['bat', 'foo', 'test'], { mode: 'sorted' });
expect(resultValidation).toEqual([
{
in: ['bar', 'bat', 'baz', 'foo', 'test'],
},
]);
});
});

describe('removeInValues', () => {
it('should remove values', async () => {
const validations = getValidationHelpers(migration, context);
await validations.removeInValues('some-content-type', 'select', ['foo', 'baz']);
expect(resultValidation).toEqual([
{
in: ['bar'],
},
]);
});
});

describe('modifyInValues', () => {
it('should modify values with custom function', async () => {
const validations = getValidationHelpers(migration, context);
await validations.modifyInValues('some-content-type', 'select', (values) => {
const result = values.slice(0, values.length - 1); // remove bar
result.push('test');
return result;
});
expect(resultValidation).toEqual([
{
in: ['foo', 'bar', 'test'],
},
]);
});
});

describe('addLinkContentTypeValues', () => {
it('should add values at the end', async () => {
const validations = getValidationHelpers(migration, context);
await validations.addLinkContentTypeValues('some-content-type', 'reference', 'bat');
expect(resultValidation).toEqual([
{
linkContentType: ['foo', 'bar', 'baz', 'bat'],
},
]);
});
});

describe('removeLinkContentTypeValues', () => {
it('should remove values', async () => {
const validations = getValidationHelpers(migration, context);
await validations.removeLinkContentTypeValues('some-content-type', 'reference', ['foo', 'baz']);
expect(resultValidation).toEqual([
{
linkContentType: ['bar'],
},
]);
});
});

describe('modifyLinkContentTypeValues', () => {
it('should modify unique values with custom function', async () => {
const validations = getValidationHelpers(migration, context);
await validations.modifyLinkContentTypeValues('some-content-type', 'reference', (values) => {
const result = values.slice(0, values.length - 1); // remove bar
result.push('test');
result.push('foo'); // should be removed since it exists
return result;
});
expect(resultValidation).toEqual([
{
linkContentType: ['foo', 'bar', 'test'],
},
]);
});
});
});
Loading

0 comments on commit d29aa0a

Please sign in to comment.