From c6922daf0a0a9aa0858328487b216751bfbda0ca Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Thu, 25 Nov 2021 12:31:13 +0100 Subject: [PATCH 01/11] add `unique` validator --- .../FormModal/attributes/advancedForm.js | 22 ++------ .../server/controllers/validation/types.js | 4 -- .../lib/services/entity-service/index.js | 1 + .../lib/services/entity-validator/index.js | 35 +++++++++--- .../services/entity-validator/validators.js | 53 +++++++++++++------ .../migration-draft-publish.test.e2e.js | 2 +- .../migration-unique-attribute.test.e2e.js | 2 +- 7 files changed, 73 insertions(+), 46 deletions(-) diff --git a/packages/core/content-type-builder/admin/src/components/FormModal/attributes/advancedForm.js b/packages/core/content-type-builder/admin/src/components/FormModal/attributes/advancedForm.js index a1144b448c..5c69f5e4b9 100644 --- a/packages/core/content-type-builder/admin/src/components/FormModal/attributes/advancedForm.js +++ b/packages/core/content-type-builder/admin/src/components/FormModal/attributes/advancedForm.js @@ -46,7 +46,7 @@ const advancedForm = { id: getTrad('form.attribute.item.settings.name'), defaultMessage: 'Settings', }, - items: [options.required, options.unique, options.private], + items: [options.required, options.private], }, ], }; @@ -209,7 +209,7 @@ const advancedForm = { id: getTrad('form.attribute.item.settings.name'), defaultMessage: 'Settings', }, - items: [options.required, options.unique, options.private], + items: [options.required, options.private], }, ], }; @@ -222,7 +222,7 @@ const advancedForm = { id: getTrad('form.attribute.item.settings.name'), defaultMessage: 'Settings', }, - items: [options.required, options.unique, options.private], + items: [options.required, options.private], }, ], }; @@ -297,13 +297,7 @@ const advancedForm = { id: getTrad('form.attribute.item.settings.name'), defaultMessage: 'Settings', }, - items: [ - options.required, - options.unique, - options.maxLength, - options.minLength, - options.private, - ], + items: [options.required, options.maxLength, options.minLength, options.private], }, ], }; @@ -330,13 +324,7 @@ const advancedForm = { id: getTrad('form.attribute.item.settings.name'), defaultMessage: 'Settings', }, - items: [ - options.required, - options.unique, - options.maxLength, - options.minLength, - options.private, - ], + items: [options.required, options.maxLength, options.minLength, options.private], }, ], }; diff --git a/packages/core/content-type-builder/server/controllers/validation/types.js b/packages/core/content-type-builder/server/controllers/validation/types.js index 0df1f33fda..038614fe91 100644 --- a/packages/core/content-type-builder/server/controllers/validation/types.js +++ b/packages/core/content-type-builder/server/controllers/validation/types.js @@ -51,7 +51,6 @@ const getTypeShape = (attribute, { modelType, attributes } = {}) => { return { multiple: yup.boolean(), required: validators.required, - unique: validators.unique, allowedTypes: yup .array() .of(yup.string().oneOf(['images', 'videos', 'files'])) @@ -129,7 +128,6 @@ const getTypeShape = (attribute, { modelType, attributes } = {}) => { return { default: yup.mixed().test(isValidDefaultJSON), required: validators.required, - unique: validators.unique, }; } case 'enumeration': { @@ -148,7 +146,6 @@ const getTypeShape = (attribute, { modelType, attributes } = {}) => { default: yup.string().when('enum', enumVal => yup.string().oneOf(enumVal)), enumName: yup.string().test(isValidName), required: validators.required, - unique: validators.unique, }; } case 'password': { @@ -225,7 +222,6 @@ const getTypeShape = (attribute, { modelType, attributes } = {}) => { return { default: yup.boolean(), required: validators.required, - unique: validators.unique, }; } diff --git a/packages/core/strapi/lib/services/entity-service/index.js b/packages/core/strapi/lib/services/entity-service/index.js index c86e733186..0ae56a5bb6 100644 --- a/packages/core/strapi/lib/services/entity-service/index.js +++ b/packages/core/strapi/lib/services/entity-service/index.js @@ -204,6 +204,7 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator }) const validData = await entityValidator.validateEntityUpdate(model, data, { isDraft, + entityId, }); const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams)); diff --git a/packages/core/strapi/lib/services/entity-validator/index.js b/packages/core/strapi/lib/services/entity-validator/index.js index 022efce405..9474dcba78 100644 --- a/packages/core/strapi/lib/services/entity-validator/index.js +++ b/packages/core/strapi/lib/services/entity-validator/index.js @@ -118,11 +118,14 @@ const createRelationValidator = createOrUpdate => (attr, data, { isDraft }) => { return validator; }; -const createScalarAttributeValidator = createOrUpdate => (attr, { isDraft }) => { +const createScalarAttributeValidator = createOrUpdate => ( + attr, + { isDraft, uid, attributeName, entityId } +) => { let validator; if (has(attr.type, validators)) { - validator = validators[attr.type](attr, { isDraft }); + validator = validators[attr.type](attr, { isDraft, uid, attributeName, entityId }); } else { // No validators specified - fall back to mixed validator = yup.mixed(); @@ -133,13 +136,22 @@ const createScalarAttributeValidator = createOrUpdate => (attr, { isDraft }) => return validator; }; -const createAttributeValidator = createOrUpdate => (attr, data, { isDraft }) => { +const createAttributeValidator = createOrUpdate => ( + attr, + data, + { isDraft, uid, attributeName, entityId } +) => { let validator; if (isMediaAttribute(attr)) { validator = yup.mixed(); } else if (isScalarAttribute(attr)) { - validator = createScalarAttributeValidator(createOrUpdate)(attr, { isDraft }); + validator = createScalarAttributeValidator(createOrUpdate)(attr, { + isDraft, + uid, + attributeName, + entityId, + }); } else { if (attr.type === 'component') { validator = createComponentValidator(createOrUpdate)(attr, data, { isDraft }); @@ -157,14 +169,14 @@ const createAttributeValidator = createOrUpdate => (attr, data, { isDraft }) => return validator; }; -const createModelValidator = createOrUpdate => (model, data, { isDraft }) => { +const createModelValidator = createOrUpdate => (model, data, { isDraft, entityId }) => { const writableAttributes = model ? getWritableAttributes(model) : []; const schema = writableAttributes.reduce((validators, attributeName) => { const validator = createAttributeValidator(createOrUpdate)( model.attributes[attributeName], prop(attributeName, data), - { isDraft } + { isDraft, uid: model.uid, attributeName, entityId } ); return assoc(attributeName, validator)(validators); @@ -173,7 +185,11 @@ const createModelValidator = createOrUpdate => (model, data, { isDraft }) => { return yup.object().shape(schema); }; -const createValidateEntity = createOrUpdate => async (model, data, { isDraft = false } = {}) => { +const createValidateEntity = createOrUpdate => async ( + model, + data, + { isDraft = false, entityId } = {} +) => { if (!isObject(data)) { const { displayName } = model.info; @@ -182,7 +198,10 @@ const createValidateEntity = createOrUpdate => async (model, data, { isDraft = f ); } - const validator = createModelValidator(createOrUpdate)(model, data, { isDraft }).required(); + const validator = createModelValidator(createOrUpdate)(model, data, { + isDraft, + entityId, + }).required(); return validateYupSchema(validator, { strict: false, abortEarly: false })(data); }; diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index b6503a9af0..d63d110bd3 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -7,9 +7,9 @@ const { yup } = require('@strapi/utils'); /** * Utility function to compose validators */ -const composeValidators = (...fns) => (attr, { isDraft }) => { +const composeValidators = (...fns) => (attr, { isDraft, uid, attributeName, entityId }) => { return fns.reduce((validator, fn) => { - return fn(attr, validator, { isDraft }); + return fn(attr, validator, { isDraft, uid, attributeName, entityId }); }, yup.mixed()); }; @@ -71,13 +71,33 @@ const addMaxFloatValidator = ({ max }, validator) => const addStringRegexValidator = ({ regex }, validator) => _.isUndefined(regex) ? validator : validator.matches(new RegExp(regex)); +const addUniqueValidator = (attr, validator, { uid, attributeName, entityId }) => { + if (attr.unique) { + return validator.test('unique', 'This attribute must be unique', async value => { + let whereParams = entityId + ? { $and: [{ [attributeName]: value }, { $not: { id: entityId } }] } + : { [attributeName]: value }; + + const record = await strapi.db.query(uid).findOne({ + select: ['id', attributeName], + where: whereParams, + }); + + return !!record; + }); + } + + return validator; +}; + /* Type validators */ const stringValidator = composeValidators( () => yup.string().transform((val, originalVal) => originalVal), addMinLengthValidator, addMaxLengthValidator, - addStringRegexValidator + addStringRegexValidator, + addUniqueValidator ); const emailValidator = composeValidators(stringValidator, (attr, validator) => validator.email()); @@ -86,20 +106,23 @@ const uidValidator = composeValidators(stringValidator, (attr, validator) => validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$')) ); -const enumerationValidator = attr => { - return yup.string().oneOf((Array.isArray(attr.enum) ? attr.enum : [attr.enum]).concat(null)); -}; +const enumerationValidator = composeValidators( + attr => yup.string().oneOf((Array.isArray(attr.enum) ? attr.enum : [attr.enum]).concat(null)), + addUniqueValidator +); const integerValidator = composeValidators( () => yup.number().integer(), addMinIntegerValidator, - addMaxIntegerValidator + addMaxIntegerValidator, + addUniqueValidator ); const floatValidator = composeValidators( () => yup.number(), addMinFloatValidator, - addMaxFloatValidator + addMaxFloatValidator, + addUniqueValidator ); module.exports = { @@ -109,15 +132,15 @@ module.exports = { password: stringValidator, email: emailValidator, enumeration: enumerationValidator, - boolean: () => yup.boolean(), + boolean: () => composeValidators(() => yup.mixed(), addUniqueValidator), uid: uidValidator, - json: () => yup.mixed(), + json: () => composeValidators(() => yup.mixed(), addUniqueValidator), integer: integerValidator, - biginteger: () => yup.mixed(), + biginteger: composeValidators(() => yup.mixed(), addUniqueValidator), float: floatValidator, decimal: floatValidator, - date: () => yup.mixed(), - time: () => yup.mixed(), - datetime: () => yup.mixed(), - timestamp: () => yup.mixed(), + date: composeValidators(() => yup.mixed(), addUniqueValidator), + time: composeValidators(() => yup.mixed(), addUniqueValidator), + datetime: composeValidators(() => yup.mixed(), addUniqueValidator), + timestamp: composeValidators(() => yup.mixed(), addUniqueValidator), }; diff --git a/packages/core/strapi/tests/migrations/migration-draft-publish.test.e2e.js b/packages/core/strapi/tests/migrations/migration-draft-publish.test.e2e.js index 45581d017d..224fe86a7e 100644 --- a/packages/core/strapi/tests/migrations/migration-draft-publish.test.e2e.js +++ b/packages/core/strapi/tests/migrations/migration-draft-publish.test.e2e.js @@ -159,7 +159,7 @@ describe('Migration - draft and publish', () => { expect(body.results[0].publishedAt).toBeUndefined(); }); - test.skip('Unique constraint is kept after disabling the feature', async () => { + test('Unique constraint is kept after disabling the feature', async () => { const dogToCreate = { code: 'sameCode' }; let res = await rq({ diff --git a/packages/core/strapi/tests/migrations/migration-unique-attribute.test.e2e.js b/packages/core/strapi/tests/migrations/migration-unique-attribute.test.e2e.js index cfdaf0174b..a9fd43c429 100644 --- a/packages/core/strapi/tests/migrations/migration-unique-attribute.test.e2e.js +++ b/packages/core/strapi/tests/migrations/migration-unique-attribute.test.e2e.js @@ -43,7 +43,7 @@ const restart = async () => { rq = await createAuthRequest({ strapi }); }; -describe.skip('Migration - unique attribute', () => { +describe('Migration - unique attribute', () => { beforeAll(async () => { await builder .addContentType(dogModel) From 282a688482c357ca378560fdc219d19b9dc277a9 Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Thu, 25 Nov 2021 16:40:51 +0100 Subject: [PATCH 02/11] remove validator from types that cannot be unique --- .../lib/services/entity-validator/validators.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index d63d110bd3..bb0148b8b1 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -106,10 +106,9 @@ const uidValidator = composeValidators(stringValidator, (attr, validator) => validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$')) ); -const enumerationValidator = composeValidators( - attr => yup.string().oneOf((Array.isArray(attr.enum) ? attr.enum : [attr.enum]).concat(null)), - addUniqueValidator -); +const enumerationValidator = attr => { + return yup.string().oneOf((Array.isArray(attr.enum) ? attr.enum : [attr.enum]).concat(null)); +}; const integerValidator = composeValidators( () => yup.number().integer(), @@ -132,9 +131,9 @@ module.exports = { password: stringValidator, email: emailValidator, enumeration: enumerationValidator, - boolean: () => composeValidators(() => yup.mixed(), addUniqueValidator), + boolean: () => () => yup.mixed(), uid: uidValidator, - json: () => composeValidators(() => yup.mixed(), addUniqueValidator), + json: () => yup.mixed(), integer: integerValidator, biginteger: composeValidators(() => yup.mixed(), addUniqueValidator), float: floatValidator, From d3de1ca7be6b7d3aa758f648d9a1d61d970d0f1c Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Thu, 25 Nov 2021 16:42:27 +0100 Subject: [PATCH 03/11] only select the `id` in the findOne query --- .../core/strapi/lib/services/entity-validator/validators.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index bb0148b8b1..01631c39fb 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -79,7 +79,7 @@ const addUniqueValidator = (attr, validator, { uid, attributeName, entityId }) = : { [attributeName]: value }; const record = await strapi.db.query(uid).findOne({ - select: ['id', attributeName], + select: ['id'], where: whereParams, }); From 2b625210d7eb06ed0d1083c805c7f026ceea091c Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Fri, 26 Nov 2021 09:30:28 +0100 Subject: [PATCH 04/11] skip unique validation if the attribute is unchanged --- .../lib/services/entity-service/index.js | 12 ++++--- .../lib/services/entity-validator/index.js | 32 +++++++++++-------- .../services/entity-validator/validators.js | 26 ++++++++++----- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-service/index.js b/packages/core/strapi/lib/services/entity-service/index.js index 0ae56a5bb6..20a40c7c89 100644 --- a/packages/core/strapi/lib/services/entity-service/index.js +++ b/packages/core/strapi/lib/services/entity-service/index.js @@ -202,10 +202,14 @@ const createDefaultImplementation = ({ strapi, db, eventHub, entityValidator }) const isDraft = contentTypesUtils.isDraft(entityToUpdate, model); - const validData = await entityValidator.validateEntityUpdate(model, data, { - isDraft, - entityId, - }); + const validData = await entityValidator.validateEntityUpdate( + model, + data, + { + isDraft, + }, + entityToUpdate + ); const query = transformParamsToQuery(uid, pickSelectionParams(wrappedParams)); diff --git a/packages/core/strapi/lib/services/entity-validator/index.js b/packages/core/strapi/lib/services/entity-validator/index.js index 9474dcba78..0eccd60464 100644 --- a/packages/core/strapi/lib/services/entity-validator/index.js +++ b/packages/core/strapi/lib/services/entity-validator/index.js @@ -120,12 +120,13 @@ const createRelationValidator = createOrUpdate => (attr, data, { isDraft }) => { const createScalarAttributeValidator = createOrUpdate => ( attr, - { isDraft, uid, attributeName, entityId } + data, + { isDraft, model, attributeName, entity } ) => { let validator; if (has(attr.type, validators)) { - validator = validators[attr.type](attr, { isDraft, uid, attributeName, entityId }); + validator = validators[attr.type](attr, { isDraft, model, attributeName, entity, data }); } else { // No validators specified - fall back to mixed validator = yup.mixed(); @@ -139,18 +140,18 @@ const createScalarAttributeValidator = createOrUpdate => ( const createAttributeValidator = createOrUpdate => ( attr, data, - { isDraft, uid, attributeName, entityId } + { isDraft, model, attributeName, entity } ) => { let validator; if (isMediaAttribute(attr)) { validator = yup.mixed(); } else if (isScalarAttribute(attr)) { - validator = createScalarAttributeValidator(createOrUpdate)(attr, { + validator = createScalarAttributeValidator(createOrUpdate)(attr, data, { isDraft, - uid, + model, attributeName, - entityId, + entity, }); } else { if (attr.type === 'component') { @@ -169,14 +170,14 @@ const createAttributeValidator = createOrUpdate => ( return validator; }; -const createModelValidator = createOrUpdate => (model, data, { isDraft, entityId }) => { +const createModelValidator = createOrUpdate => (model, data, { isDraft }, entity) => { const writableAttributes = model ? getWritableAttributes(model) : []; const schema = writableAttributes.reduce((validators, attributeName) => { const validator = createAttributeValidator(createOrUpdate)( model.attributes[attributeName], prop(attributeName, data), - { isDraft, uid: model.uid, attributeName, entityId } + { isDraft, model, attributeName, entity } ); return assoc(attributeName, validator)(validators); @@ -188,7 +189,8 @@ const createModelValidator = createOrUpdate => (model, data, { isDraft, entityId const createValidateEntity = createOrUpdate => async ( model, data, - { isDraft = false, entityId } = {} + { isDraft = false } = {}, + entity = null ) => { if (!isObject(data)) { const { displayName } = model.info; @@ -198,10 +200,14 @@ const createValidateEntity = createOrUpdate => async ( ); } - const validator = createModelValidator(createOrUpdate)(model, data, { - isDraft, - entityId, - }).required(); + const validator = createModelValidator(createOrUpdate)( + model, + data, + { + isDraft, + }, + entity + ).required(); return validateYupSchema(validator, { strict: false, abortEarly: false })(data); }; diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index 01631c39fb..59bd2bf7b8 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -7,9 +7,9 @@ const { yup } = require('@strapi/utils'); /** * Utility function to compose validators */ -const composeValidators = (...fns) => (attr, { isDraft, uid, attributeName, entityId }) => { +const composeValidators = (...fns) => (attr, { isDraft, model, attributeName, entity, data }) => { return fns.reduce((validator, fn) => { - return fn(attr, validator, { isDraft, uid, attributeName, entityId }); + return fn(attr, validator, { isDraft, model, attributeName, entity, data }); }, yup.mixed()); }; @@ -71,14 +71,24 @@ const addMaxFloatValidator = ({ max }, validator) => const addStringRegexValidator = ({ regex }, validator) => _.isUndefined(regex) ? validator : validator.matches(new RegExp(regex)); -const addUniqueValidator = (attr, validator, { uid, attributeName, entityId }) => { - if (attr.unique) { +const addUniqueValidator = (attr, validator, { model, attributeName, entity, data }) => { + /** + * If the attribute is unchanged we skip the unique verification. This will + * prevent the validator to be triggered in case the user activated the + * unique constraint after already creating multiple entries with + * the same attribute value for that field. + */ + if (entity && data === entity[attributeName]) { + return validator; + } + + if (attr.unique || attr.type === 'uid') { return validator.test('unique', 'This attribute must be unique', async value => { - let whereParams = entityId - ? { $and: [{ [attributeName]: value }, { $not: { id: entityId } }] } + let whereParams = entity + ? { $and: [{ [attributeName]: value }, { $not: { id: entity.id } }] } : { [attributeName]: value }; - const record = await strapi.db.query(uid).findOne({ + const record = await strapi.db.query(model.uid).findOne({ select: ['id'], where: whereParams, }); @@ -131,7 +141,7 @@ module.exports = { password: stringValidator, email: emailValidator, enumeration: enumerationValidator, - boolean: () => () => yup.mixed(), + boolean: () => yup.boolean(), uid: uidValidator, json: () => yup.mixed(), integer: integerValidator, From d3364e0ae843e3a239665f85b2ce0530fd81735d Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Fri, 26 Nov 2021 13:18:16 +0100 Subject: [PATCH 05/11] first batch of unit tests for the unique validator --- .../__tests__/validators.test.js | 744 ++++++++++++++++++ .../services/entity-validator/validators.js | 34 +- 2 files changed, 766 insertions(+), 12 deletions(-) create mode 100644 packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js new file mode 100644 index 0000000000..22010b7c17 --- /dev/null +++ b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js @@ -0,0 +1,744 @@ +'use strict'; + +const strapiUtils = require('@strapi/utils'); +const { YupValidationError } = require('../../../../../utils/lib/errors'); +const entityValidator = require('../validators'); + +describe('Entity validator', () => { + const fakeFindOne = jest.fn(); + + global.strapi = { + db: { + query: jest.fn(() => ({ + findOne: fakeFindOne, + })), + }, + }; + + afterEach(() => { + jest.clearAllMocks(); + fakeFindOne.mockReset(); + }); + + describe('String unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrStringUnique: { type: 'string', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.string( + { type: 'string' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrStringUnique', + entity: null, + data: 'non-unique-test-data', + } + ) + ); + + await validator('non-unique-test-data'); + + expect(fakeFindOne).not.toBeCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.string( + { type: 'string', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrStringUnique', + entity: null, + data: 'non-unique-test-data', + } + ) + ); + + expect(await validator('non-unique-test-data')).toBe('non-unique-test-data'); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrStringUnique: 'unique-test-data' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.string( + { type: 'string', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrStringUnique', + entity: null, + data: 'unique-test-data', + } + ) + ); + + try { + await validator('unique-test-data'); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrStringUnique: 'non-updated-unique-test-data' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.string( + { type: 'string', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrStringUnique', + entity: { id: 1, attrStringUnique: 'non-updated-unique-test-data' }, + data: 'non-updated-unique-test-data', + } + ) + ); + + expect(await validator('non-updated-unique-test-data')).toBe('non-updated-unique-test-data'); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.string( + { type: 'string', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrStringUnique', + entity: null, + data: 'test-data', + } + ) + ); + + await validator('test-data'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrStringUnique: 'test-data' }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.string( + { type: 'string', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrStringUnique', + entity: { id: 1 }, + data: 'test-data', + } + ) + ); + + await validator('test-data'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrStringUnique: 'test-data' }, { $not: { id: 1 } }] }, + }); + }); + }); + + describe('Integer unique validator', () => { + const fakeModel = { + kind: 'contentType', + uid: 'test-uid', + modelName: 'test-model', + privateAttributes: [], + options: {}, + attributes: { + attrIntegerUnique: { type: 'integer', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.integer( + { type: 'integer' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrIntegerUnique', + entity: null, + data: 1, + } + ) + ); + + await validator(1); + + expect(fakeFindOne).not.toBeCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.integer( + { type: 'integer', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrIntegerUnique', + entity: null, + data: 1, + } + ) + ); + + expect(await validator(1)).toBe(1); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrIntegerUnique: 2 }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.integer( + { type: 'integer', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrIntegerUnique', + entity: null, + data: 2, + } + ) + ); + + try { + await validator(2); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrIntegerUnique: 3 }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.integer( + { type: 'integer', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrIntegerUnique', + entity: { attrIntegerUnique: 3 }, + data: 3, + } + ) + ); + + expect(await validator(3)).toBe(3); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.integer( + { type: 'integer', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrIntegerUnique', + entity: null, + data: 4, + } + ) + ); + + await validator(4); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrIntegerUnique: 4 }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.integer( + { type: 'integer', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrIntegerUnique', + entity: { id: 1 }, + data: 5, + } + ) + ); + + await validator(5); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrIntegerUnique: 5 }, { $not: { id: 1 } }] }, + }); + }); + }); + + describe('BigInteger unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrBigIntegerUnique: { type: 'biginteger', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.biginteger( + { type: 'biginteger' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrBigIntegerUnique', + entity: null, + data: 1, + } + ) + ); + + await validator(1); + + expect(fakeFindOne).not.toBeCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.biginteger( + { type: 'biginteger', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrBigIntegerUnique', + entity: null, + data: 1, + } + ) + ); + + expect(await validator(1)).toBe(1); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrBigIntegerUnique: 2 }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.biginteger( + { type: 'biginteger', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrBigIntegerUnique', + entity: null, + data: 2, + } + ) + ); + + try { + await validator(2); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrBigIntegerUnique: 3 }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.biginteger( + { type: 'biginteger', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrBigIntegerUnique', + entity: { attrBigIntegerUnique: 3 }, + data: 3, + } + ) + ); + + expect(await validator(3)).toBe(3); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.biginteger( + { type: 'biginteger', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrBigIntegerUnique', + entity: null, + data: 4, + } + ) + ); + + await validator(4); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrBigIntegerUnique: 4 }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.biginteger( + { type: 'biginteger', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrBigIntegerUnique', + entity: { id: 1 }, + data: 5, + } + ) + ); + + await validator(5); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrBigIntegerUnique: 5 }, { $not: { id: 1 } }] }, + }); + }); + }); + + describe('Float unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrFloatUnique: { type: 'float', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.float( + { type: 'float' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrFloatUnique', + entity: null, + data: 1, + } + ) + ); + + await validator(1); + + expect(fakeFindOne).not.toBeCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.float( + { type: 'float', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrFloatUnique', + entity: null, + data: 1, + } + ) + ); + + expect(await validator(1)).toBe(1); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrFloatUnique: 2 }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.float( + { type: 'float', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrFloatUnique', + entity: null, + data: 2, + } + ) + ); + + try { + await validator(2); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrFloatUnique: 3 }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.float( + { type: 'float', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrFloatUnique', + entity: { attrFloatUnique: 3 }, + data: 3, + } + ) + ); + + expect(await validator(3)).toBe(3); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.float( + { type: 'float', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrFloatUnique', + entity: null, + data: 4, + } + ) + ); + + await validator(4); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrFloatUnique: 4 }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.float( + { type: 'float', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrFloatUnique', + entity: { id: 1 }, + data: 5, + } + ) + ); + + await validator(5); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrFloatUnique: 5 }, { $not: { id: 1 } }] }, + }); + }); + }); + + describe('UID unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrUidUnique: { type: 'uid' }, + }, + }; + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.uid( + { type: 'uid', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrUidUnique', + entity: null, + data: 'non-unique-uid', + } + ) + ); + + expect(await validator('non-unique-uid')).toBe('non-unique-uid'); + }); + + test('it always validates the unique constraint even if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.uid( + { type: 'uid' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrUidUnique', + entity: null, + data: 'non-unique-uid', + } + ) + ); + + expect(await validator('non-unique-uid')).toBe('non-unique-uid'); + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrUidUnique: 'non-unique-uid' }, + }); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrUidUnique: 'unique-uid' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.uid( + { type: 'uid', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrUidUnique', + entity: null, + data: 'unique-uid', + } + ) + ); + + try { + await validator(2); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrUidUnique: 'unchanged-unique-uid' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.uid( + { type: 'uid', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrUidUnique', + entity: { attrUidUnique: 'unchanged-unique-uid' }, + data: 'unchanged-unique-uid', + } + ) + ); + + expect(await validator('unchanged-unique-uid')).toBe('unchanged-unique-uid'); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.uid( + { type: 'uid', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrUidUnique', + entity: null, + data: 'unique-uid', + } + ) + ); + + await validator('unique-uid'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrUidUnique: 'unique-uid' }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.uid( + { type: 'uid', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrUidUnique', + entity: { id: 1 }, + data: 'unique-uid', + } + ) + ); + + await validator('unique-uid'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrUidUnique: 'unique-uid' }, { $not: { id: 1 } }] }, + }); + }); + }); +}); diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index 59bd2bf7b8..a8fd3b7e1a 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -9,7 +9,7 @@ const { yup } = require('@strapi/utils'); */ const composeValidators = (...fns) => (attr, { isDraft, model, attributeName, entity, data }) => { return fns.reduce((validator, fn) => { - return fn(attr, validator, { isDraft, model, attributeName, entity, data }); + return fn(attr, { isDraft, model, attributeName, entity, data }, validator); }, yup.mixed()); }; @@ -20,7 +20,7 @@ const composeValidators = (...fns) => (attr, { isDraft, model, attributeName, en * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMinLengthValidator = ({ minLength }, validator, { isDraft }) => +const addMinLengthValidator = ({ minLength }, { isDraft }, validator) => _.isInteger(minLength) && !isDraft ? validator.min(minLength) : validator; /** @@ -28,7 +28,7 @@ const addMinLengthValidator = ({ minLength }, validator, { isDraft }) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMaxLengthValidator = ({ maxLength }, validator) => +const addMaxLengthValidator = ({ maxLength }, __, validator) => _.isInteger(maxLength) ? validator.max(maxLength) : validator; /** @@ -36,7 +36,7 @@ const addMaxLengthValidator = ({ maxLength }, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMinIntegerValidator = ({ min }, validator) => +const addMinIntegerValidator = ({ min }, __, validator) => _.isNumber(min) ? validator.min(_.toInteger(min)) : validator; /** @@ -44,7 +44,7 @@ const addMinIntegerValidator = ({ min }, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMaxIntegerValidator = ({ max }, validator) => +const addMaxIntegerValidator = ({ max }, __, validator) => _.isNumber(max) ? validator.max(_.toInteger(max)) : validator; /** @@ -52,7 +52,7 @@ const addMaxIntegerValidator = ({ max }, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMinFloatValidator = ({ min }, validator) => +const addMinFloatValidator = ({ min }, __, validator) => _.isNumber(min) ? validator.min(min) : validator; /** @@ -60,7 +60,7 @@ const addMinFloatValidator = ({ min }, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMaxFloatValidator = ({ max }, validator) => +const addMaxFloatValidator = ({ max }, __, validator) => _.isNumber(max) ? validator.max(max) : validator; /** @@ -68,10 +68,18 @@ const addMaxFloatValidator = ({ max }, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addStringRegexValidator = ({ regex }, validator) => +const addStringRegexValidator = ({ regex }, __, validator) => _.isUndefined(regex) ? validator : validator.matches(new RegExp(regex)); -const addUniqueValidator = (attr, validator, { model, attributeName, entity, data }) => { +const addUniqueValidator = (attr, { model, attributeName, entity, data }, validator) => { + /** + * If the attribute value is `null` we want to skip the unique validation. + * Otherwise it'll only accept a single `null` entry in the database. + */ + if (data === null) { + return validator; + } + /** * If the attribute is unchanged we skip the unique verification. This will * prevent the validator to be triggered in case the user activated the @@ -93,7 +101,7 @@ const addUniqueValidator = (attr, validator, { model, attributeName, entity, dat where: whereParams, }); - return !!record; + return !record; }); } @@ -110,9 +118,11 @@ const stringValidator = composeValidators( addUniqueValidator ); -const emailValidator = composeValidators(stringValidator, (attr, validator) => validator.email()); +const emailValidator = composeValidators(stringValidator, (attr, __, validator) => + validator.email() +); -const uidValidator = composeValidators(stringValidator, (attr, validator) => +const uidValidator = composeValidators(stringValidator, (attr, __, validator) => validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$')) ); From 61950f114e2f1899ff2647b8ce8ef78966e38171 Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Fri, 26 Nov 2021 14:01:24 +0100 Subject: [PATCH 06/11] add test for the rest of the validators --- .../__tests__/validators.test.js | 786 +++++++++++++++++- 1 file changed, 782 insertions(+), 4 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js index 22010b7c17..f63f870418 100644 --- a/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js +++ b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js @@ -50,7 +50,30 @@ describe('Entity validator', () => { await validator('non-unique-test-data'); - expect(fakeFindOne).not.toBeCalled(); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .string( + { type: 'string', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrStringUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + + expect(fakeFindOne).not.toHaveBeenCalled(); }); test('it validates the unique constraint if there is no other record in the database', async () => { @@ -194,7 +217,30 @@ describe('Entity validator', () => { await validator(1); - expect(fakeFindOne).not.toBeCalled(); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .integer( + { type: 'integer', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrIntegerUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + + expect(fakeFindOne).not.toHaveBeenCalled(); }); test('it validates the unique constraint if there is no other record in the database', async () => { @@ -338,7 +384,30 @@ describe('Entity validator', () => { await validator(1); - expect(fakeFindOne).not.toBeCalled(); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .biginteger( + { type: 'biginteger', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrBigIntegerUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + + expect(fakeFindOne).not.toHaveBeenCalled(); }); test('it validates the unique constraint if there is no other record in the database', async () => { @@ -482,7 +551,29 @@ describe('Entity validator', () => { await validator(1); - expect(fakeFindOne).not.toBeCalled(); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .float( + { type: 'float', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrFloatUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + expect(fakeFindOne).not.toHaveBeenCalled(); }); test('it validates the unique constraint if there is no other record in the database', async () => { @@ -627,6 +718,29 @@ describe('Entity validator', () => { expect(await validator('non-unique-uid')).toBe('non-unique-uid'); }); + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .uid( + { type: 'uid', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrUidUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + test('it always validates the unique constraint even if the attribute is not set as unique', async () => { fakeFindOne.mockResolvedValueOnce(null); @@ -741,4 +855,668 @@ describe('Entity validator', () => { }); }); }); + + describe('Date unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrDateUnique: { type: 'date', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.date( + { type: 'date' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateUnique', + entity: null, + data: '2021-11-29', + } + ) + ); + + await validator('2021-11-29'); + + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .date( + { type: 'date', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.date( + { type: 'date', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateUnique', + entity: null, + data: '2021-11-29', + } + ) + ); + + expect(await validator('2021-11-29')).toBe('2021-11-29'); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrDateUnique: '2021-11-29' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.date( + { type: 'date', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateUnique', + entity: null, + data: '2021-11-29', + } + ) + ); + + try { + await validator('2021-11-29'); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrDateUnique: '2021-11-29' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.date( + { type: 'date', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateUnique', + entity: { attrDateUnique: '2021-11-29' }, + data: '2021-11-29', + } + ) + ); + + expect(await validator('2021-11-29')).toBe('2021-11-29'); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.date( + { type: 'date', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateUnique', + entity: null, + data: '2021-11-29', + } + ) + ); + + await validator('2021-11-29'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrDateUnique: '2021-11-29' }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.date( + { type: 'date', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateUnique', + entity: { id: 1 }, + data: '2021-11-29', + } + ) + ); + + await validator('2021-11-29'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrDateUnique: '2021-11-29' }, { $not: { id: 1 } }] }, + }); + }); + }); + + describe('DateTime unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrDateTimeUnique: { type: 'datetime', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.datetime( + { type: 'datetime' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateTimeUnique', + entity: null, + data: '2021-11-29T00:00:00.000Z', + } + ) + ); + + await validator('2021-11-29T00:00:00.000Z'); + + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .datetime( + { type: 'datetime', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateTimeUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.datetime( + { type: 'datetime', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateTimeUnique', + entity: null, + data: '2021-11-29T00:00:00.000Z', + } + ) + ); + + expect(await validator('2021-11-29T00:00:00.000Z')).toBe('2021-11-29T00:00:00.000Z'); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.datetime( + { type: 'datetime', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateTimeUnique', + entity: null, + data: '2021-11-29T00:00:00.000Z', + } + ) + ); + + try { + await validator('2021-11-29T00:00:00.000Z'); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.datetime( + { type: 'datetime', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateTimeUnique', + entity: { attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }, + data: '2021-11-29T00:00:00.000Z', + } + ) + ); + + expect(await validator('2021-11-29T00:00:00.000Z')).toBe('2021-11-29T00:00:00.000Z'); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.datetime( + { type: 'datetime', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateTimeUnique', + entity: null, + data: '2021-11-29T00:00:00.000Z', + } + ) + ); + + await validator('2021-11-29T00:00:00.000Z'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.datetime( + { type: 'datetime', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrDateTimeUnique', + entity: { id: 1 }, + data: '2021-11-29T00:00:00.000Z', + } + ) + ); + + await validator('2021-11-29T00:00:00.000Z'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }, { $not: { id: 1 } }] }, + }); + }); + }); + + describe('Time unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrTimeUnique: { type: 'time', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.time( + { type: 'time' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimeUnique', + entity: null, + data: '00:00:00.000Z', + } + ) + ); + + await validator('00:00:00.000Z'); + + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .time( + { type: 'time', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimeUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.time( + { type: 'time', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimeUnique', + entity: null, + data: '00:00:00.000Z', + } + ) + ); + + expect(await validator('00:00:00.000Z')).toBe('00:00:00.000Z'); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrTimeUnique: '00:00:00.000Z' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.time( + { type: 'time', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimeUnique', + entity: null, + data: '00:00:00.000Z', + } + ) + ); + + try { + await validator('00:00:00.000Z'); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrTimeUnique: '00:00:00.000Z' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.time( + { type: 'time', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimeUnique', + entity: { attrTimeUnique: '00:00:00.000Z' }, + data: '00:00:00.000Z', + } + ) + ); + + expect(await validator('00:00:00.000Z')).toBe('00:00:00.000Z'); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.time( + { type: 'time', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimeUnique', + entity: null, + data: '00:00:00.000Z', + } + ) + ); + + await validator('00:00:00.000Z'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrTimeUnique: '00:00:00.000Z' }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.time( + { type: 'time', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimeUnique', + entity: { id: 1 }, + data: '00:00:00.000Z', + } + ) + ); + + await validator('00:00:00.000Z'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrTimeUnique: '00:00:00.000Z' }, { $not: { id: 1 } }] }, + }); + }); + }); + + describe('Timestamp unique validator', () => { + const fakeModel = { + kind: 'contentType', + modelName: 'test-model', + uid: 'test-uid', + privateAttributes: [], + options: {}, + attributes: { + attrTimestampUnique: { type: 'timestamp', unique: true }, + }, + }; + + test('it does not validates the unique constraint if the attribute is not set as unique', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.timestamp( + { type: 'timestamp' }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimestampUnique', + entity: null, + data: '1638140400', + } + ) + ); + + await validator('1638140400'); + + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it does not validates the unique constraint if the attribute value is `null`', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator + .timestamp( + { type: 'timestamp', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimestampUnique', + entity: null, + data: null, + } + ) + .nullable() + ); + + await validator(null); + expect(fakeFindOne).not.toHaveBeenCalled(); + }); + + test('it validates the unique constraint if there is no other record in the database', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.timestamp( + { type: 'timestamp', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimestampUnique', + entity: null, + data: '1638140400', + } + ) + ); + + expect(await validator('1638140400')).toBe('1638140400'); + }); + + test('it fails the validation of the unique constraint if the database contains a record with the same attribute value', async () => { + expect.assertions(1); + fakeFindOne.mockResolvedValueOnce({ attrTimestampUnique: '1638140400' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.timestamp( + { type: 'timestamp', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimestampUnique', + entity: null, + data: '1638140400', + } + ) + ); + + try { + await validator('1638140400'); + } catch (err) { + expect(err).toBeInstanceOf(YupValidationError); + } + }); + + test('it validates the unique constraint if the attribute data has not changed even if there is a record in the database with the same attribute value', async () => { + fakeFindOne.mockResolvedValueOnce({ attrTimestampUnique: '1638140400' }); + + const validator = strapiUtils.validateYupSchema( + entityValidator.timestamp( + { type: 'timestamp', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimestampUnique', + entity: { attrTimestampUnique: '1638140400' }, + data: '1638140400', + } + ) + ); + + expect(await validator('1638140400')).toBe('1638140400'); + }); + + test('it checks the database for records with the same value for the checked attribute', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.timestamp( + { type: 'timestamp', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimestampUnique', + entity: null, + data: '1638140400', + } + ) + ); + + await validator('1638140400'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { attrTimestampUnique: '1638140400' }, + }); + }); + + test('it checks the database for records with the same value but not the same id for the checked attribute if an entity is passed', async () => { + fakeFindOne.mockResolvedValueOnce(null); + + const validator = strapiUtils.validateYupSchema( + entityValidator.timestamp( + { type: 'timestamp', unique: true }, + { + isDraft: false, + model: fakeModel, + attributeName: 'attrTimestampUnique', + entity: { id: 1 }, + data: '1638140400', + } + ) + ); + + await validator('1638140400'); + + expect(fakeFindOne).toHaveBeenCalledWith({ + select: ['id'], + where: { $and: [{ attrTimestampUnique: '1638140400' }, { $not: { id: 1 } }] }, + }); + }); + }); }); From eddaa42ba6cefe0285169e1e6fc1a9e83ac1428b Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Fri, 26 Nov 2021 16:34:31 +0100 Subject: [PATCH 07/11] refactor the composeValidators function --- .../__tests__/validators.test.js | 723 ++++++++---------- .../lib/services/entity-validator/index.js | 23 +- .../services/entity-validator/validators.js | 76 +- 3 files changed, 366 insertions(+), 456 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js index f63f870418..255787b7f7 100644 --- a/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js +++ b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js @@ -38,13 +38,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( { type: 'string' }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrStringUnique', - entity: null, - data: 'non-unique-test-data', - } + name: 'attrStringUnique', + value: 'non-unique-test-data', + }, + null ) ); @@ -60,13 +60,13 @@ describe('Entity validator', () => { entityValidator .string( { type: 'string', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrStringUnique', - entity: null, - data: null, - } + name: 'attrStringUnique', + value: null, + }, + null ) .nullable() ); @@ -82,13 +82,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( { type: 'string', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrStringUnique', - entity: null, - data: 'non-unique-test-data', - } + name: 'attrStringUnique', + value: 'non-unique-test-data', + }, + null ) ); @@ -102,13 +102,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( { type: 'string', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrStringUnique', - entity: null, - data: 'unique-test-data', - } + name: 'attrStringUnique', + value: 'unique-test-data', + }, + null ) ); @@ -125,13 +125,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( { type: 'string', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrStringUnique', - entity: { id: 1, attrStringUnique: 'non-updated-unique-test-data' }, - data: 'non-updated-unique-test-data', - } + name: 'attrStringUnique', + value: 'non-updated-unique-test-data', + }, + { id: 1, attrStringUnique: 'non-updated-unique-test-data' } ) ); @@ -144,13 +144,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( { type: 'string', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrStringUnique', - entity: null, - data: 'test-data', - } + name: 'attrStringUnique', + value: 'test-data', + }, + null ) ); @@ -168,13 +168,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( { type: 'string', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrStringUnique', - entity: { id: 1 }, - data: 'test-data', - } + name: 'attrStringUnique', + value: 'test-data', + }, + { id: 1, attrStringUnique: 'other-data' } ) ); @@ -205,13 +205,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( { type: 'integer' }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrIntegerUnique', - entity: null, - data: 1, - } + { isDraft: false }, + fakeModel, + { name: 'attrIntegerUnique', value: 1 }, + null ) ); @@ -227,13 +224,10 @@ describe('Entity validator', () => { entityValidator .integer( { type: 'integer', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrIntegerUnique', - entity: null, - data: null, - } + { isDraft: false }, + fakeModel, + { name: 'attrIntegerUnique', value: null }, + null ) .nullable() ); @@ -249,13 +243,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( { type: 'integer', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrIntegerUnique', - entity: null, - data: 1, - } + { isDraft: false }, + fakeModel, + { name: 'attrIntegerUnique', value: 2 }, + null ) ); @@ -269,13 +260,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( { type: 'integer', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrIntegerUnique', - entity: null, - data: 2, - } + { isDraft: false }, + fakeModel, + { name: 'attrIntegerUnique', value: 2 }, + null ) ); @@ -292,13 +280,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( { type: 'integer', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrIntegerUnique', - entity: { attrIntegerUnique: 3 }, - data: 3, - } + { isDraft: false }, + fakeModel, + { name: 'attrIntegerUnique', value: 3 }, + { id: 1, attrIntegerUnique: 3 } ) ); @@ -311,13 +296,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( { type: 'integer', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrIntegerUnique', - entity: null, - data: 4, - } + { isDraft: false }, + fakeModel, + { name: 'attrIntegerUnique', value: 4 }, + null ) ); @@ -335,13 +317,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( { type: 'integer', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrIntegerUnique', - entity: { id: 1 }, - data: 5, - } + { isDraft: false }, + fakeModel, + { name: 'attrIntegerUnique', value: 5 }, + { id: 1, attrIntegerUnique: 42 } ) ); @@ -372,13 +351,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( { type: 'biginteger' }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrBigIntegerUnique', - entity: null, - data: 1, - } + name: 'attrBigIntegerUnique', + value: 1, + }, + null ) ); @@ -394,13 +373,13 @@ describe('Entity validator', () => { entityValidator .biginteger( { type: 'biginteger', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrBigIntegerUnique', - entity: null, - data: null, - } + name: 'attrBigIntegerUnique', + value: null, + }, + null ) .nullable() ); @@ -416,13 +395,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( { type: 'biginteger', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrBigIntegerUnique', - entity: null, - data: 1, - } + name: 'attrBigIntegerUnique', + value: 1, + }, + null ) ); @@ -436,13 +415,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( { type: 'biginteger', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrBigIntegerUnique', - entity: null, - data: 2, - } + name: 'attrBigIntegerUnique', + value: 2, + }, + null ) ); @@ -459,13 +438,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( { type: 'biginteger', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrBigIntegerUnique', - entity: { attrBigIntegerUnique: 3 }, - data: 3, - } + name: 'attrBigIntegerUnique', + value: 3, + }, + { id: 1, attrBigIntegerUnique: 3 } ) ); @@ -478,13 +457,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( { type: 'biginteger', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrBigIntegerUnique', - entity: null, - data: 4, - } + name: 'attrBigIntegerUnique', + value: 4, + }, + null ) ); @@ -502,13 +481,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( { type: 'biginteger', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrBigIntegerUnique', - entity: { id: 1 }, - data: 5, - } + name: 'attrBigIntegerUnique', + value: 5, + }, + { id: 1, attrBigIntegerUnique: 42 } ) ); @@ -539,13 +518,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( { type: 'float' }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrFloatUnique', - entity: null, - data: 1, - } + name: 'attrFloatUnique', + value: 1, + }, + null ) ); @@ -561,13 +540,13 @@ describe('Entity validator', () => { entityValidator .float( { type: 'float', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrFloatUnique', - entity: null, - data: null, - } + name: 'attrFloatUnique', + value: null, + }, + null ) .nullable() ); @@ -582,13 +561,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( { type: 'float', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrFloatUnique', - entity: null, - data: 1, - } + name: 'attrFloatUnique', + value: 1, + }, + null ) ); @@ -602,13 +581,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( { type: 'float', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrFloatUnique', - entity: null, - data: 2, - } + name: 'attrFloatUnique', + value: 2, + }, + null ) ); @@ -625,13 +604,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( { type: 'float', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrFloatUnique', - entity: { attrFloatUnique: 3 }, - data: 3, - } + name: 'attrFloatUnique', + value: 3, + }, + { id: 1, attrFloatUnique: 3 } ) ); @@ -644,13 +623,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( { type: 'float', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrFloatUnique', - entity: null, - data: 4, - } + name: 'attrFloatUnique', + value: 4, + }, + null ) ); @@ -668,13 +647,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( { type: 'float', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrFloatUnique', - entity: { id: 1 }, - data: 5, - } + name: 'attrFloatUnique', + value: 5, + }, + { id: 1, attrFloatUnique: 42 } ) ); @@ -705,13 +684,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( { type: 'uid', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrUidUnique', - entity: null, - data: 'non-unique-uid', - } + { isDraft: false }, + fakeModel, + { name: 'attrUidUnique', value: 'non-unique-uid' }, + null ) ); @@ -725,13 +701,10 @@ describe('Entity validator', () => { entityValidator .uid( { type: 'uid', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrUidUnique', - entity: null, - data: null, - } + { isDraft: false }, + fakeModel, + { name: 'attrUidUnique', value: null }, + null ) .nullable() ); @@ -747,13 +720,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( { type: 'uid' }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrUidUnique', - entity: null, - data: 'non-unique-uid', - } + { isDraft: false }, + fakeModel, + { name: 'attrUidUnique', value: 'non-unique-uid' }, + null ) ); @@ -771,18 +741,15 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( { type: 'uid', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrUidUnique', - entity: null, - data: 'unique-uid', - } + { isDraft: false }, + fakeModel, + { name: 'attrUidUnique', value: 'unique-uid' }, + null ) ); try { - await validator(2); + await validator('unique-uid'); } catch (err) { expect(err).toBeInstanceOf(YupValidationError); } @@ -794,13 +761,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( { type: 'uid', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrUidUnique', - entity: { attrUidUnique: 'unchanged-unique-uid' }, - data: 'unchanged-unique-uid', - } + { isDraft: false }, + fakeModel, + { name: 'attrUidUnique', value: 'unchanged-unique-uid' }, + { id: 1, attrUidUnique: 'unchanged-unique-uid' } ) ); @@ -813,13 +777,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( { type: 'uid', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrUidUnique', - entity: null, - data: 'unique-uid', - } + { isDraft: false }, + fakeModel, + { name: 'attrUidUnique', value: 'unique-uid' }, + null ) ); @@ -837,13 +798,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( { type: 'uid', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrUidUnique', - entity: { id: 1 }, - data: 'unique-uid', - } + { isDraft: false }, + fakeModel, + { name: 'attrUidUnique', value: 'unique-uid' }, + { id: 1, attrUidUnique: 'other-uid' } ) ); @@ -874,13 +832,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( { type: 'date' }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateUnique', - entity: null, - data: '2021-11-29', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateUnique', value: '2021-11-29' }, + null ) ); @@ -896,13 +851,10 @@ describe('Entity validator', () => { entityValidator .date( { type: 'date', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateUnique', - entity: null, - data: null, - } + { isDraft: false }, + fakeModel, + { name: 'attrDateUnique', value: null }, + null ) .nullable() ); @@ -917,13 +869,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( { type: 'date', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateUnique', - entity: null, - data: '2021-11-29', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateUnique', value: '2021-11-29' }, + null ) ); @@ -937,13 +886,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( { type: 'date', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateUnique', - entity: null, - data: '2021-11-29', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateUnique', value: '2021-11-29' }, + null ) ); @@ -960,13 +906,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( { type: 'date', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateUnique', - entity: { attrDateUnique: '2021-11-29' }, - data: '2021-11-29', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateUnique', value: '2021-11-29' }, + { id: 1, attrDateUnique: '2021-11-29' } ) ); @@ -979,13 +922,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( { type: 'date', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateUnique', - entity: null, - data: '2021-11-29', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateUnique', value: '2021-11-29' }, + null ) ); @@ -1003,13 +943,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( { type: 'date', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateUnique', - entity: { id: 1 }, - data: '2021-11-29', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateUnique', value: '2021-11-29' }, + { id: 1, attrDateUnique: '2021-12-15' } ) ); @@ -1040,13 +977,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( { type: 'datetime' }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateTimeUnique', - entity: null, - data: '2021-11-29T00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + { id: 1, attrDateTimeUnique: '2021-11-29T00:00:00.000Z' } ) ); @@ -1062,13 +996,10 @@ describe('Entity validator', () => { entityValidator .datetime( { type: 'datetime', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateTimeUnique', - entity: null, - data: null, - } + { isDraft: false }, + fakeModel, + { name: 'attrDateTimeUnique', value: null }, + null ) .nullable() ); @@ -1083,13 +1014,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( { type: 'datetime', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateTimeUnique', - entity: null, - data: '2021-11-29T00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + null ) ); @@ -1103,13 +1031,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( { type: 'datetime', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateTimeUnique', - entity: null, - data: '2021-11-29T00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + null ) ); @@ -1126,13 +1051,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( { type: 'datetime', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateTimeUnique', - entity: { attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }, - data: '2021-11-29T00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + { id: 1, attrDateTimeUnique: '2021-11-29T00:00:00.000Z' } ) ); @@ -1145,13 +1067,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( { type: 'datetime', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateTimeUnique', - entity: null, - data: '2021-11-29T00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + null ) ); @@ -1169,13 +1088,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( { type: 'datetime', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrDateTimeUnique', - entity: { id: 1 }, - data: '2021-11-29T00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + { id: 1, attrDateTimeUnique: '2021-12-25T00:00:00.000Z' } ) ); @@ -1206,13 +1122,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( { type: 'time' }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimeUnique', - entity: null, - data: '00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + null ) ); @@ -1228,13 +1141,10 @@ describe('Entity validator', () => { entityValidator .time( { type: 'time', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimeUnique', - entity: null, - data: null, - } + { isDraft: false }, + fakeModel, + { name: 'attrTimeUnique', value: null }, + { id: 1, attrTimeUnique: '00:00:00.000Z' } ) .nullable() ); @@ -1249,13 +1159,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( { type: 'time', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimeUnique', - entity: null, - data: '00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + null ) ); @@ -1269,13 +1176,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( { type: 'time', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimeUnique', - entity: null, - data: '00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + null ) ); @@ -1292,13 +1196,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( { type: 'time', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimeUnique', - entity: { attrTimeUnique: '00:00:00.000Z' }, - data: '00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + { id: 1, attrTimeUnique: '00:00:00.000Z' } ) ); @@ -1311,13 +1212,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( { type: 'time', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimeUnique', - entity: null, - data: '00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + null ) ); @@ -1335,13 +1233,10 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( { type: 'time', unique: true }, - { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimeUnique', - entity: { id: 1 }, - data: '00:00:00.000Z', - } + { isDraft: false }, + fakeModel, + { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + { id: 1, attrTimeUnique: '01:00:00.000Z' } ) ); @@ -1372,13 +1267,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( { type: 'timestamp' }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimestampUnique', - entity: null, - data: '1638140400', - } + name: 'attrTimestampUnique', + value: '1638140400', + }, + null ) ); @@ -1394,13 +1289,13 @@ describe('Entity validator', () => { entityValidator .timestamp( { type: 'timestamp', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimestampUnique', - entity: null, - data: null, - } + name: 'attrTimestampUnique', + value: null, + }, + null ) .nullable() ); @@ -1415,13 +1310,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( { type: 'timestamp', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimestampUnique', - entity: null, - data: '1638140400', - } + name: 'attrTimestampUnique', + value: '1638140400', + }, + null ) ); @@ -1435,13 +1330,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( { type: 'timestamp', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimestampUnique', - entity: null, - data: '1638140400', - } + name: 'attrTimestampUnique', + value: '1638140400', + }, + null ) ); @@ -1458,13 +1353,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( { type: 'timestamp', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimestampUnique', - entity: { attrTimestampUnique: '1638140400' }, - data: '1638140400', - } + name: 'attrTimestampUnique', + value: '1638140400', + }, + { id: 1, attrTimestampUnique: '1638140400' } ) ); @@ -1477,13 +1372,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( { type: 'timestamp', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimestampUnique', - entity: null, - data: '1638140400', - } + name: 'attrTimestampUnique', + value: '1638140400', + }, + null ) ); @@ -1501,13 +1396,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( { type: 'timestamp', unique: true }, + { isDraft: false }, + fakeModel, { - isDraft: false, - model: fakeModel, - attributeName: 'attrTimestampUnique', - entity: { id: 1 }, - data: '1638140400', - } + name: 'attrTimestampUnique', + value: '1638140400', + }, + { id: 1, attrTimestampUnique: '1000000000' } ) ); diff --git a/packages/core/strapi/lib/services/entity-validator/index.js b/packages/core/strapi/lib/services/entity-validator/index.js index 0eccd60464..75e6a01574 100644 --- a/packages/core/strapi/lib/services/entity-validator/index.js +++ b/packages/core/strapi/lib/services/entity-validator/index.js @@ -121,12 +121,21 @@ const createRelationValidator = createOrUpdate => (attr, data, { isDraft }) => { const createScalarAttributeValidator = createOrUpdate => ( attr, data, - { isDraft, model, attributeName, entity } + { isDraft }, + model, + attributeName, + entity ) => { let validator; if (has(attr.type, validators)) { - validator = validators[attr.type](attr, { isDraft, model, attributeName, entity, data }); + validator = validators[attr.type]( + attr, + { isDraft }, + model, + { name: attributeName, value: data }, + entity + ); } else { // No validators specified - fall back to mixed validator = yup.mixed(); @@ -147,12 +156,14 @@ const createAttributeValidator = createOrUpdate => ( if (isMediaAttribute(attr)) { validator = yup.mixed(); } else if (isScalarAttribute(attr)) { - validator = createScalarAttributeValidator(createOrUpdate)(attr, data, { - isDraft, + validator = createScalarAttributeValidator(createOrUpdate)( + attr, + data, + { isDraft }, model, attributeName, - entity, - }); + entity + ); } else { if (attr.type === 'component') { validator = createComponentValidator(createOrUpdate)(attr, data, { isDraft }); diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index a8fd3b7e1a..54142ea791 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -7,10 +7,16 @@ const { yup } = require('@strapi/utils'); /** * Utility function to compose validators */ -const composeValidators = (...fns) => (attr, { isDraft, model, attributeName, entity, data }) => { - return fns.reduce((validator, fn) => { - return fn(attr, { isDraft, model, attributeName, entity, data }, validator); - }, yup.mixed()); +const composeValidators = (...fns) => (...args) => { + let validator = yup.mixed(); + + // if we receive a schema then use it as base schema for nested composition + if (yup.isSchema(args[0])) { + validator = args[0]; + args = args.slice(1); + } + + return fns.reduce((validator, fn) => fn(validator, ...args), validator); }; /* Validator utils */ @@ -20,7 +26,7 @@ const composeValidators = (...fns) => (attr, { isDraft, model, attributeName, en * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMinLengthValidator = ({ minLength }, { isDraft }, validator) => +const addMinLengthValidator = (validator, { minLength }, { isDraft }) => _.isInteger(minLength) && !isDraft ? validator.min(minLength) : validator; /** @@ -28,7 +34,7 @@ const addMinLengthValidator = ({ minLength }, { isDraft }, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMaxLengthValidator = ({ maxLength }, __, validator) => +const addMaxLengthValidator = (validator, { maxLength }) => _.isInteger(maxLength) ? validator.max(maxLength) : validator; /** @@ -36,7 +42,7 @@ const addMaxLengthValidator = ({ maxLength }, __, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMinIntegerValidator = ({ min }, __, validator) => +const addMinIntegerValidator = (validator, { min }) => _.isNumber(min) ? validator.min(_.toInteger(min)) : validator; /** @@ -44,7 +50,7 @@ const addMinIntegerValidator = ({ min }, __, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMaxIntegerValidator = ({ max }, __, validator) => +const addMaxIntegerValidator = (validator, { max }) => _.isNumber(max) ? validator.max(_.toInteger(max)) : validator; /** @@ -52,7 +58,7 @@ const addMaxIntegerValidator = ({ max }, __, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMinFloatValidator = ({ min }, __, validator) => +const addMinFloatValidator = (validator, { min }) => _.isNumber(min) ? validator.min(min) : validator; /** @@ -60,7 +66,7 @@ const addMinFloatValidator = ({ min }, __, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addMaxFloatValidator = ({ max }, __, validator) => +const addMaxFloatValidator = (validator, { max }) => _.isNumber(max) ? validator.max(max) : validator; /** @@ -68,15 +74,19 @@ const addMaxFloatValidator = ({ max }, __, validator) => * @param {Object} attribute model attribute * @param {Object} validator yup validator */ -const addStringRegexValidator = ({ regex }, __, validator) => +const addStringRegexValidator = (validator, { regex }) => _.isUndefined(regex) ? validator : validator.matches(new RegExp(regex)); -const addUniqueValidator = (attr, { model, attributeName, entity, data }, validator) => { +const addUniqueValidator = (validator, attr, __, model, updatedAttribute, entity) => { + if (!attr.unique && attr.type !== 'uid') { + return validator; + } + /** * If the attribute value is `null` we want to skip the unique validation. * Otherwise it'll only accept a single `null` entry in the database. */ - if (data === null) { + if (updatedAttribute.value === null) { return validator; } @@ -86,26 +96,22 @@ const addUniqueValidator = (attr, { model, attributeName, entity, data }, valida * unique constraint after already creating multiple entries with * the same attribute value for that field. */ - if (entity && data === entity[attributeName]) { + if (entity && updatedAttribute.value === entity[updatedAttribute.name]) { return validator; } - if (attr.unique || attr.type === 'uid') { - return validator.test('unique', 'This attribute must be unique', async value => { - let whereParams = entity - ? { $and: [{ [attributeName]: value }, { $not: { id: entity.id } }] } - : { [attributeName]: value }; + return validator.test('unique', 'This attribute must be unique', async value => { + let whereParams = entity + ? { $and: [{ [updatedAttribute.name]: value }, { $not: { id: entity.id } }] } + : { [updatedAttribute.name]: value }; - const record = await strapi.db.query(model.uid).findOne({ - select: ['id'], - where: whereParams, - }); - - return !record; + const record = await strapi.db.query(model.uid).findOne({ + select: ['id'], + where: whereParams, }); - } - return validator; + return !record; + }); }; /* Type validators */ @@ -118,11 +124,9 @@ const stringValidator = composeValidators( addUniqueValidator ); -const emailValidator = composeValidators(stringValidator, (attr, __, validator) => - validator.email() -); +const emailValidator = composeValidators(stringValidator, validator => validator.email()); -const uidValidator = composeValidators(stringValidator, (attr, __, validator) => +const uidValidator = composeValidators(stringValidator, validator => validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$')) ); @@ -155,11 +159,11 @@ module.exports = { uid: uidValidator, json: () => yup.mixed(), integer: integerValidator, - biginteger: composeValidators(() => yup.mixed(), addUniqueValidator), + biginteger: composeValidators(addUniqueValidator), float: floatValidator, decimal: floatValidator, - date: composeValidators(() => yup.mixed(), addUniqueValidator), - time: composeValidators(() => yup.mixed(), addUniqueValidator), - datetime: composeValidators(() => yup.mixed(), addUniqueValidator), - timestamp: composeValidators(() => yup.mixed(), addUniqueValidator), + date: composeValidators(addUniqueValidator), + time: composeValidators(addUniqueValidator), + datetime: composeValidators(addUniqueValidator), + timestamp: composeValidators(addUniqueValidator), }; From eca08fc6421317d405d0a2e65b250e387f33579c Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Fri, 26 Nov 2021 16:37:29 +0100 Subject: [PATCH 08/11] small refactor for consistency --- .../core/strapi/lib/services/entity-validator/index.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-validator/index.js b/packages/core/strapi/lib/services/entity-validator/index.js index 75e6a01574..613b9c0eac 100644 --- a/packages/core/strapi/lib/services/entity-validator/index.js +++ b/packages/core/strapi/lib/services/entity-validator/index.js @@ -149,7 +149,10 @@ const createScalarAttributeValidator = createOrUpdate => ( const createAttributeValidator = createOrUpdate => ( attr, data, - { isDraft, model, attributeName, entity } + { isDraft }, + model, + attributeName, + entity ) => { let validator; @@ -188,7 +191,10 @@ const createModelValidator = createOrUpdate => (model, data, { isDraft }, entity const validator = createAttributeValidator(createOrUpdate)( model.attributes[attributeName], prop(attributeName, data), - { isDraft, model, attributeName, entity } + { isDraft }, + model, + attributeName, + entity ); return assoc(attributeName, validator)(validators); From 3f10ec8f7a7575d09cad60767462ab297de7fb3e Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Mon, 29 Nov 2021 11:21:23 +0100 Subject: [PATCH 09/11] refactor the validation functions signature --- .../__tests__/validators.test.js | 812 ++++++++++-------- .../lib/services/entity-validator/index.js | 124 ++- .../services/entity-validator/validators.js | 130 +-- 3 files changed, 606 insertions(+), 460 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js index 255787b7f7..e772f4e19f 100644 --- a/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js +++ b/packages/core/strapi/lib/services/entity-validator/__tests__/validators.test.js @@ -37,14 +37,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( - { type: 'string' }, - { isDraft: false }, - fakeModel, { - name: 'attrStringUnique', - value: 'non-unique-test-data', + attr: { type: 'string' }, + model: fakeModel, + updatedAttribute: { + name: 'attrStringUnique', + value: 'non-unique-test-data', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -59,14 +61,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .string( - { type: 'string', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrStringUnique', - value: null, + attr: { type: 'string', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrStringUnique', + value: null, + }, + entity: null, }, - null + { isDraft: false } ) .nullable() ); @@ -81,14 +85,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( - { type: 'string', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrStringUnique', - value: 'non-unique-test-data', + attr: { type: 'string', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrStringUnique', + value: 'non-unique-test-data', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -101,14 +107,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( - { type: 'string', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrStringUnique', - value: 'unique-test-data', + attr: { type: 'string', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrStringUnique', + value: 'unique-test-data', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -124,14 +132,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( - { type: 'string', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrStringUnique', - value: 'non-updated-unique-test-data', + attr: { type: 'string', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrStringUnique', + value: 'non-updated-unique-test-data', + }, + entity: { id: 1, attrStringUnique: 'non-updated-unique-test-data' }, }, - { id: 1, attrStringUnique: 'non-updated-unique-test-data' } + { isDraft: false } ) ); @@ -143,14 +153,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( - { type: 'string', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrStringUnique', - value: 'test-data', + attr: { type: 'string', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrStringUnique', + value: 'test-data', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -167,14 +179,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.string( - { type: 'string', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrStringUnique', - value: 'test-data', + attr: { type: 'string', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrStringUnique', + value: 'test-data', + }, + entity: { id: 1, attrStringUnique: 'other-data' }, }, - { id: 1, attrStringUnique: 'other-data' } + { isDraft: false } ) ); @@ -204,11 +218,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( - { type: 'integer' }, - { isDraft: false }, - fakeModel, - { name: 'attrIntegerUnique', value: 1 }, - null + { + attr: { type: 'integer' }, + model: fakeModel, + updatedAttribute: { name: 'attrIntegerUnique', value: 1 }, + entity: null, + }, + { isDraft: false } ) ); @@ -223,11 +239,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .integer( - { type: 'integer', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrIntegerUnique', value: null }, - null + { + attr: { type: 'integer', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrIntegerUnique', value: null }, + entity: null, + }, + { isDraft: false } ) .nullable() ); @@ -242,11 +260,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( - { type: 'integer', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrIntegerUnique', value: 2 }, - null + { + attr: { type: 'integer', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrIntegerUnique', value: 2 }, + entity: null, + }, + { isDraft: false } ) ); @@ -259,11 +279,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( - { type: 'integer', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrIntegerUnique', value: 2 }, - null + { + attr: { type: 'integer', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrIntegerUnique', value: 2 }, + entity: null, + }, + { isDraft: false } ) ); @@ -279,11 +301,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( - { type: 'integer', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrIntegerUnique', value: 3 }, - { id: 1, attrIntegerUnique: 3 } + { + attr: { type: 'integer', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrIntegerUnique', value: 3 }, + entity: { id: 1, attrIntegerUnique: 3 }, + }, + { isDraft: false } ) ); @@ -295,11 +319,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( - { type: 'integer', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrIntegerUnique', value: 4 }, - null + { + attr: { type: 'integer', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrIntegerUnique', value: 4 }, + entity: null, + }, + { isDraft: false } ) ); @@ -316,11 +342,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.integer( - { type: 'integer', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrIntegerUnique', value: 5 }, - { id: 1, attrIntegerUnique: 42 } + { + attr: { type: 'integer', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrIntegerUnique', value: 5 }, + entity: { id: 1, attrIntegerUnique: 42 }, + }, + { isDraft: false } ) ); @@ -350,14 +378,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( - { type: 'biginteger' }, - { isDraft: false }, - fakeModel, { - name: 'attrBigIntegerUnique', - value: 1, + attr: { type: 'biginteger' }, + model: fakeModel, + updatedAttribute: { + name: 'attrBigIntegerUnique', + value: 1, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -372,14 +402,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .biginteger( - { type: 'biginteger', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrBigIntegerUnique', - value: null, + attr: { type: 'biginteger', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrBigIntegerUnique', + value: null, + }, + entity: null, }, - null + { isDraft: false } ) .nullable() ); @@ -394,14 +426,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( - { type: 'biginteger', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrBigIntegerUnique', - value: 1, + attr: { type: 'biginteger', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrBigIntegerUnique', + value: 1, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -414,14 +448,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( - { type: 'biginteger', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrBigIntegerUnique', - value: 2, + attr: { type: 'biginteger', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrBigIntegerUnique', + value: 2, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -437,14 +473,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( - { type: 'biginteger', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrBigIntegerUnique', - value: 3, + attr: { type: 'biginteger', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrBigIntegerUnique', + value: 3, + }, + entity: { id: 1, attrBigIntegerUnique: 3 }, }, - { id: 1, attrBigIntegerUnique: 3 } + { isDraft: false } ) ); @@ -456,14 +494,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( - { type: 'biginteger', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrBigIntegerUnique', - value: 4, + attr: { type: 'biginteger', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrBigIntegerUnique', + value: 4, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -480,14 +520,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.biginteger( - { type: 'biginteger', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrBigIntegerUnique', - value: 5, + attr: { type: 'biginteger', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrBigIntegerUnique', + value: 5, + }, + entity: { id: 1, attrBigIntegerUnique: 42 }, }, - { id: 1, attrBigIntegerUnique: 42 } + { isDraft: false } ) ); @@ -517,14 +559,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( - { type: 'float' }, - { isDraft: false }, - fakeModel, { - name: 'attrFloatUnique', - value: 1, + attr: { type: 'float' }, + model: fakeModel, + updatedAttribute: { + name: 'attrFloatUnique', + value: 1, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -539,14 +583,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .float( - { type: 'float', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrFloatUnique', - value: null, + attr: { type: 'float', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrFloatUnique', + value: null, + }, + entity: null, }, - null + { isDraft: false } ) .nullable() ); @@ -560,14 +606,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( - { type: 'float', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrFloatUnique', - value: 1, + attr: { type: 'float', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrFloatUnique', + value: 1, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -580,14 +628,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( - { type: 'float', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrFloatUnique', - value: 2, + attr: { type: 'float', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrFloatUnique', + value: 2, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -603,14 +653,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( - { type: 'float', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrFloatUnique', - value: 3, + attr: { type: 'float', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrFloatUnique', + value: 3, + }, + entity: { id: 1, attrFloatUnique: 3 }, }, - { id: 1, attrFloatUnique: 3 } + { isDraft: false } ) ); @@ -622,14 +674,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( - { type: 'float', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrFloatUnique', - value: 4, + attr: { type: 'float', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrFloatUnique', + value: 4, + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -646,14 +700,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.float( - { type: 'float', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrFloatUnique', - value: 5, + attr: { type: 'float', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrFloatUnique', + value: 5, + }, + entity: { id: 1, attrFloatUnique: 42 }, }, - { id: 1, attrFloatUnique: 42 } + { isDraft: false } ) ); @@ -683,11 +739,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( - { type: 'uid', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrUidUnique', value: 'non-unique-uid' }, - null + { + attr: { type: 'uid', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrUidUnique', value: 'non-unique-uid' }, + entity: null, + }, + { isDraft: false } ) ); @@ -700,11 +758,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .uid( - { type: 'uid', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrUidUnique', value: null }, - null + { + attr: { type: 'uid', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrUidUnique', value: null }, + entity: null, + }, + { isDraft: false } ) .nullable() ); @@ -719,11 +779,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( - { type: 'uid' }, - { isDraft: false }, - fakeModel, - { name: 'attrUidUnique', value: 'non-unique-uid' }, - null + { + attr: { type: 'uid' }, + model: fakeModel, + updatedAttribute: { name: 'attrUidUnique', value: 'non-unique-uid' }, + entity: null, + }, + { isDraft: false } ) ); @@ -740,11 +802,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( - { type: 'uid', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrUidUnique', value: 'unique-uid' }, - null + { + attr: { type: 'uid', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrUidUnique', value: 'unique-uid' }, + entity: null, + }, + { isDraft: false } ) ); @@ -760,11 +824,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( - { type: 'uid', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrUidUnique', value: 'unchanged-unique-uid' }, - { id: 1, attrUidUnique: 'unchanged-unique-uid' } + { + attr: { type: 'uid', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrUidUnique', value: 'unchanged-unique-uid' }, + entity: { id: 1, attrUidUnique: 'unchanged-unique-uid' }, + }, + { isDraft: false } ) ); @@ -776,11 +842,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( - { type: 'uid', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrUidUnique', value: 'unique-uid' }, - null + { + attr: { type: 'uid', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrUidUnique', value: 'unique-uid' }, + entity: null, + }, + { isDraft: false } ) ); @@ -797,11 +865,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.uid( - { type: 'uid', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrUidUnique', value: 'unique-uid' }, - { id: 1, attrUidUnique: 'other-uid' } + { + attr: { type: 'uid', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrUidUnique', value: 'unique-uid' }, + entity: { id: 1, attrUidUnique: 'other-uid' }, + }, + { isDraft: false } ) ); @@ -831,11 +901,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( - { type: 'date' }, - { isDraft: false }, - fakeModel, - { name: 'attrDateUnique', value: '2021-11-29' }, - null + { + attr: { type: 'date' }, + model: fakeModel, + updatedAttribute: { name: 'attrDateUnique', value: '2021-11-29' }, + entity: null, + }, + { isDraft: false } ) ); @@ -850,11 +922,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .date( - { type: 'date', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateUnique', value: null }, - null + { + attr: { type: 'date', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateUnique', value: null }, + entity: null, + }, + { isDraft: false } ) .nullable() ); @@ -868,11 +942,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( - { type: 'date', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateUnique', value: '2021-11-29' }, - null + { + attr: { type: 'date', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateUnique', value: '2021-11-29' }, + entity: null, + }, + { isDraft: false } ) ); @@ -885,11 +961,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( - { type: 'date', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateUnique', value: '2021-11-29' }, - null + { + attr: { type: 'date', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateUnique', value: '2021-11-29' }, + entity: null, + }, + { isDraft: false } ) ); @@ -905,11 +983,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( - { type: 'date', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateUnique', value: '2021-11-29' }, - { id: 1, attrDateUnique: '2021-11-29' } + { + attr: { type: 'date', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateUnique', value: '2021-11-29' }, + entity: { id: 1, attrDateUnique: '2021-11-29' }, + }, + { isDraft: false } ) ); @@ -921,11 +1001,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( - { type: 'date', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateUnique', value: '2021-11-29' }, - null + { + attr: { type: 'date', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateUnique', value: '2021-11-29' }, + entity: null, + }, + { isDraft: false } ) ); @@ -942,11 +1024,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.date( - { type: 'date', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateUnique', value: '2021-11-29' }, - { id: 1, attrDateUnique: '2021-12-15' } + { + attr: { type: 'date', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateUnique', value: '2021-11-29' }, + entity: { id: 1, attrDateUnique: '2021-12-15' }, + }, + { isDraft: false } ) ); @@ -976,11 +1060,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( - { type: 'datetime' }, - { isDraft: false }, - fakeModel, - { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, - { id: 1, attrDateTimeUnique: '2021-11-29T00:00:00.000Z' } + { + attr: { type: 'datetime' }, + model: fakeModel, + updatedAttribute: { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + entity: { id: 1, attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }, + }, + { isDraft: false } ) ); @@ -995,11 +1081,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .datetime( - { type: 'datetime', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateTimeUnique', value: null }, - null + { + attr: { type: 'datetime', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateTimeUnique', value: null }, + entity: null, + }, + { isDraft: false } ) .nullable() ); @@ -1013,11 +1101,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( - { type: 'datetime', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, - null + { + attr: { type: 'datetime', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + entity: null, + }, + { isDraft: false } ) ); @@ -1030,11 +1120,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( - { type: 'datetime', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, - null + { + attr: { type: 'datetime', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + entity: null, + }, + { isDraft: false } ) ); @@ -1050,11 +1142,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( - { type: 'datetime', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, - { id: 1, attrDateTimeUnique: '2021-11-29T00:00:00.000Z' } + { + attr: { type: 'datetime', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + entity: { id: 1, attrDateTimeUnique: '2021-11-29T00:00:00.000Z' }, + }, + { isDraft: false } ) ); @@ -1066,11 +1160,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( - { type: 'datetime', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, - null + { + attr: { type: 'datetime', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + entity: null, + }, + { isDraft: false } ) ); @@ -1087,11 +1183,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.datetime( - { type: 'datetime', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, - { id: 1, attrDateTimeUnique: '2021-12-25T00:00:00.000Z' } + { + attr: { type: 'datetime', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrDateTimeUnique', value: '2021-11-29T00:00:00.000Z' }, + entity: { id: 1, attrDateTimeUnique: '2021-12-25T00:00:00.000Z' }, + }, + { isDraft: false } ) ); @@ -1121,11 +1219,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( - { type: 'time' }, - { isDraft: false }, - fakeModel, - { name: 'attrTimeUnique', value: '00:00:00.000Z' }, - null + { + attr: { type: 'time' }, + model: fakeModel, + updatedAttribute: { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + entity: null, + }, + { isDraft: false } ) ); @@ -1140,11 +1240,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .time( - { type: 'time', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrTimeUnique', value: null }, - { id: 1, attrTimeUnique: '00:00:00.000Z' } + { + attr: { type: 'time', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrTimeUnique', value: null }, + entity: { id: 1, attrTimeUnique: '00:00:00.000Z' }, + }, + { isDraft: false } ) .nullable() ); @@ -1158,11 +1260,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( - { type: 'time', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrTimeUnique', value: '00:00:00.000Z' }, - null + { + attr: { type: 'time', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + entity: null, + }, + { isDraft: false } ) ); @@ -1175,11 +1279,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( - { type: 'time', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrTimeUnique', value: '00:00:00.000Z' }, - null + { + attr: { type: 'time', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + entity: null, + }, + { isDraft: false } ) ); @@ -1195,11 +1301,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( - { type: 'time', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrTimeUnique', value: '00:00:00.000Z' }, - { id: 1, attrTimeUnique: '00:00:00.000Z' } + { + attr: { type: 'time', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + entity: { id: 1, attrTimeUnique: '00:00:00.000Z' }, + }, + { isDraft: false } ) ); @@ -1211,11 +1319,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( - { type: 'time', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrTimeUnique', value: '00:00:00.000Z' }, - null + { + attr: { type: 'time', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + entity: null, + }, + { isDraft: false } ) ); @@ -1232,11 +1342,13 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.time( - { type: 'time', unique: true }, - { isDraft: false }, - fakeModel, - { name: 'attrTimeUnique', value: '00:00:00.000Z' }, - { id: 1, attrTimeUnique: '01:00:00.000Z' } + { + attr: { type: 'time', unique: true }, + model: fakeModel, + updatedAttribute: { name: 'attrTimeUnique', value: '00:00:00.000Z' }, + entity: { id: 1, attrTimeUnique: '01:00:00.000Z' }, + }, + { isDraft: false } ) ); @@ -1266,14 +1378,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( - { type: 'timestamp' }, - { isDraft: false }, - fakeModel, { - name: 'attrTimestampUnique', - value: '1638140400', + attr: { type: 'timestamp' }, + model: fakeModel, + updatedAttribute: { + name: 'attrTimestampUnique', + value: '1638140400', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -1288,14 +1402,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator .timestamp( - { type: 'timestamp', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrTimestampUnique', - value: null, + attr: { type: 'timestamp', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrTimestampUnique', + value: null, + }, + entity: null, }, - null + { isDraft: false } ) .nullable() ); @@ -1309,14 +1425,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( - { type: 'timestamp', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrTimestampUnique', - value: '1638140400', + attr: { type: 'timestamp', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrTimestampUnique', + value: '1638140400', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -1329,14 +1447,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( - { type: 'timestamp', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrTimestampUnique', - value: '1638140400', + attr: { type: 'timestamp', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrTimestampUnique', + value: '1638140400', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -1352,14 +1472,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( - { type: 'timestamp', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrTimestampUnique', - value: '1638140400', + attr: { type: 'timestamp', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrTimestampUnique', + value: '1638140400', + }, + entity: { id: 1, attrTimestampUnique: '1638140400' }, }, - { id: 1, attrTimestampUnique: '1638140400' } + { isDraft: false } ) ); @@ -1371,14 +1493,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( - { type: 'timestamp', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrTimestampUnique', - value: '1638140400', + attr: { type: 'timestamp', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrTimestampUnique', + value: '1638140400', + }, + entity: null, }, - null + { isDraft: false } ) ); @@ -1395,14 +1519,16 @@ describe('Entity validator', () => { const validator = strapiUtils.validateYupSchema( entityValidator.timestamp( - { type: 'timestamp', unique: true }, - { isDraft: false }, - fakeModel, { - name: 'attrTimestampUnique', - value: '1638140400', + attr: { type: 'timestamp', unique: true }, + model: fakeModel, + updatedAttribute: { + name: 'attrTimestampUnique', + value: '1638140400', + }, + entity: { id: 1, attrTimestampUnique: '1000000000' }, }, - { id: 1, attrTimestampUnique: '1000000000' } + { isDraft: false } ) ); diff --git a/packages/core/strapi/lib/services/entity-validator/index.js b/packages/core/strapi/lib/services/entity-validator/index.js index 613b9c0eac..448d19da03 100644 --- a/packages/core/strapi/lib/services/entity-validator/index.js +++ b/packages/core/strapi/lib/services/entity-validator/index.js @@ -12,8 +12,11 @@ const { yup, validateYupSchema } = strapiUtils; const { isMediaAttribute, isScalarAttribute, getWritableAttributes } = strapiUtils.contentTypes; const { ValidationError } = strapiUtils.errors; -const addMinMax = (attr, validator, data) => { - if (Number.isInteger(attr.min) && (attr.required || (Array.isArray(data) && data.length > 0))) { +const addMinMax = (validator, { attr, updatedAttribute }) => { + if ( + Number.isInteger(attr.min) && + (attr.required || (Array.isArray(updatedAttribute.value) && updatedAttribute.value.length > 0)) + ) { validator = validator.min(attr.min); } if (Number.isInteger(attr.max)) { @@ -22,7 +25,7 @@ const addMinMax = (attr, validator, data) => { return validator; }; -const addRequiredValidation = createOrUpdate => (required, validator) => { +const addRequiredValidation = createOrUpdate => (validator, { attr: { required } }) => { if (required) { if (createOrUpdate === 'creation') { validator = validator.notNil(); @@ -35,7 +38,7 @@ const addRequiredValidation = createOrUpdate => (required, validator) => { return validator; }; -const addDefault = createOrUpdate => (attr, validator) => { +const addDefault = createOrUpdate => (validator, { attr }) => { if (createOrUpdate === 'creation') { if ( ((attr.type === 'component' && attr.repeatable) || attr.type === 'dynamiczone') && @@ -54,7 +57,7 @@ const addDefault = createOrUpdate => (attr, validator) => { const preventCast = validator => validator.transform((val, originalVal) => originalVal); -const createComponentValidator = createOrUpdate => (attr, data, { isDraft }) => { +const createComponentValidator = createOrUpdate => ({ attr, updatedAttribute }, { isDraft }) => { let validator; const model = strapi.getModel(attr.component); @@ -66,19 +69,23 @@ const createComponentValidator = createOrUpdate => (attr, data, { isDraft }) => validator = yup .array() .of( - yup.lazy(item => createModelValidator(createOrUpdate)(model, item, { isDraft }).notNull()) + yup.lazy(item => + createModelValidator(createOrUpdate)({ model, data: item }, { isDraft }).notNull() + ) ); - validator = addRequiredValidation(createOrUpdate)(true, validator); - validator = addMinMax(attr, validator, data); + validator = addRequiredValidation(createOrUpdate)(validator, { attr: { required: true } }); + validator = addMinMax(validator, { attr, updatedAttribute }); } else { - validator = createModelValidator(createOrUpdate)(model, data, { isDraft }); - validator = addRequiredValidation(createOrUpdate)(!isDraft && attr.required, validator); + validator = createModelValidator(createOrUpdate)({ model, updatedAttribute }, { isDraft }); + validator = addRequiredValidation(createOrUpdate)(validator, { + attr: { required: !isDraft && attr.required }, + }); } return validator; }; -const createDzValidator = createOrUpdate => (attr, data, { isDraft }) => { +const createDzValidator = createOrUpdate => ({ attr, updatedAttribute }, { isDraft }) => { let validator; validator = yup.array().of( @@ -95,106 +102,85 @@ const createDzValidator = createOrUpdate => (attr, data, { isDraft }) => { .notNull(); return model - ? schema.concat(createModelValidator(createOrUpdate)(model, item, { isDraft })) + ? schema.concat(createModelValidator(createOrUpdate)({ model, data: item }, { isDraft })) : schema; }) ); - validator = addRequiredValidation(createOrUpdate)(true, validator); - validator = addMinMax(attr, validator, data); + validator = addRequiredValidation(createOrUpdate)(validator, { attr: { required: true } }); + validator = addMinMax(validator, { attr, updatedAttribute }); return validator; }; -const createRelationValidator = createOrUpdate => (attr, data, { isDraft }) => { +const createRelationValidator = createOrUpdate => ({ attr, updatedAttribute }, { isDraft }) => { let validator; - if (Array.isArray(data)) { + if (Array.isArray(updatedAttribute.value)) { validator = yup.array().of(yup.mixed()); } else { validator = yup.mixed(); } - validator = addRequiredValidation(createOrUpdate)(!isDraft && attr.required, validator); + + validator = addRequiredValidation(createOrUpdate)(validator, { + attr: { required: !isDraft && attr.required }, + }); return validator; }; -const createScalarAttributeValidator = createOrUpdate => ( - attr, - data, - { isDraft }, - model, - attributeName, - entity -) => { +const createScalarAttributeValidator = createOrUpdate => (metas, options) => { let validator; - if (has(attr.type, validators)) { - validator = validators[attr.type]( - attr, - { isDraft }, - model, - { name: attributeName, value: data }, - entity - ); + if (has(metas.attr.type, validators)) { + validator = validators[metas.attr.type](metas, options); } else { // No validators specified - fall back to mixed validator = yup.mixed(); } - validator = addRequiredValidation(createOrUpdate)(!isDraft && attr.required, validator); + validator = addRequiredValidation(createOrUpdate)(validator, { + attr: { required: !options.isDraft && metas.attr.required }, + }); return validator; }; -const createAttributeValidator = createOrUpdate => ( - attr, - data, - { isDraft }, - model, - attributeName, - entity -) => { +const createAttributeValidator = createOrUpdate => (metas, options) => { let validator; - if (isMediaAttribute(attr)) { + if (isMediaAttribute(metas.attr)) { validator = yup.mixed(); - } else if (isScalarAttribute(attr)) { - validator = createScalarAttributeValidator(createOrUpdate)( - attr, - data, - { isDraft }, - model, - attributeName, - entity - ); + } else if (isScalarAttribute(metas.attr)) { + validator = createScalarAttributeValidator(createOrUpdate)(metas, options); } else { - if (attr.type === 'component') { - validator = createComponentValidator(createOrUpdate)(attr, data, { isDraft }); - } else if (attr.type === 'dynamiczone') { - validator = createDzValidator(createOrUpdate)(attr, data, { isDraft }); + if (metas.attr.type === 'component') { + validator = createComponentValidator(createOrUpdate)(metas, options); + } else if (metas.attr.type === 'dynamiczone') { + validator = createDzValidator(createOrUpdate)(metas, options); } else { - validator = createRelationValidator(createOrUpdate)(attr, data, { isDraft }); + validator = createRelationValidator(createOrUpdate)(metas, options); } validator = preventCast(validator); } - validator = addDefault(createOrUpdate)(attr, validator); + validator = addDefault(createOrUpdate)(validator, metas); return validator; }; -const createModelValidator = createOrUpdate => (model, data, { isDraft }, entity) => { +const createModelValidator = createOrUpdate => ({ model, data, entity }, options) => { const writableAttributes = model ? getWritableAttributes(model) : []; const schema = writableAttributes.reduce((validators, attributeName) => { const validator = createAttributeValidator(createOrUpdate)( - model.attributes[attributeName], - prop(attributeName, data), - { isDraft }, - model, - attributeName, - entity + { + attr: model.attributes[attributeName], + updatedAttribute: { name: attributeName, value: prop(attributeName, data) }, + model, + entity, + }, + options ); return assoc(attributeName, validator)(validators); @@ -218,12 +204,12 @@ const createValidateEntity = createOrUpdate => async ( } const validator = createModelValidator(createOrUpdate)( - model, - data, { - isDraft, + model, + data, + entity, }, - entity + { isDraft } ).required(); return validateYupSchema(validator, { strict: false, abortEarly: false })(data); }; diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index 54142ea791..33e12d6c9f 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -23,84 +23,118 @@ const composeValidators = (...fns) => (...args) => { /** * Adds minLength validator - * @param {Object} attribute model attribute - * @param {Object} validator yup validator + * @param {import('yup').StringSchema} validator yup validator + * @param {Object} metas + * @param {{ minLength: Number }} metas.attr model attribute + * @param {Object} options + * @param {boolean} options.isDraft + * + * @returns {{import('yup').StringSchema}} */ -const addMinLengthValidator = (validator, { minLength }, { isDraft }) => - _.isInteger(minLength) && !isDraft ? validator.min(minLength) : validator; +const addMinLengthValidator = (validator, { attr }, { isDraft }) => + _.isInteger(attr.minLength) && !isDraft ? validator.min(attr.minLength) : validator; /** * Adds maxLength validator - * @param {Object} attribute model attribute - * @param {Object} validator yup validator + * @param {import('yup').StringSchema} validator yup validator + * @param {Object} metas + * @param {{ maxLength: Number }} metas.attr model attribute + * + * @returns {{import('yup').StringSchema}} */ -const addMaxLengthValidator = (validator, { maxLength }) => - _.isInteger(maxLength) ? validator.max(maxLength) : validator; +const addMaxLengthValidator = (validator, { attr }) => + _.isInteger(attr.maxLength) ? validator.max(attr.maxLength) : validator; /** * Adds min integer validator - * @param {Object} attribute model attribute - * @param {Object} validator yup validator + * @param {import('yup').Number} validator yup validator + * @param {Object} metas + * @param {{ min: Number }} metas.attr model attribute + * + * @returns {{import('yup').StringSchema}} */ -const addMinIntegerValidator = (validator, { min }) => - _.isNumber(min) ? validator.min(_.toInteger(min)) : validator; +const addMinIntegerValidator = (validator, { attr }) => + _.isNumber(attr.min) ? validator.min(_.toInteger(attr.min)) : validator; /** * Adds max integer validator - * @param {Object} attribute model attribute - * @param {Object} validator yup validator + * @param {import('yup').Number} validator yup validator + * @param {Object} metas + * @param {{ max: Number }} metas.attr model attribute + * + * @returns {{import('yup').StringSchema}} */ -const addMaxIntegerValidator = (validator, { max }) => - _.isNumber(max) ? validator.max(_.toInteger(max)) : validator; +const addMaxIntegerValidator = (validator, { attr }) => + _.isNumber(attr.max) ? validator.max(_.toInteger(attr.max)) : validator; /** * Adds min float/decimal validator - * @param {Object} attribute model attribute - * @param {Object} validator yup validator + * @param {import('yup').NumberSchema} validator yup validator + * @param {Object} metas + * @param {{ min: Number }} metas.attr model attribute + * + * @returns {{import('yup').StringSchema}} */ -const addMinFloatValidator = (validator, { min }) => - _.isNumber(min) ? validator.min(min) : validator; +const addMinFloatValidator = (validator, { attr }) => + _.isNumber(attr.min) ? validator.min(attr.min) : validator; /** * Adds max float/decimal validator - * @param {Object} attribute model attribute - * @param {Object} validator yup validator + * @param {import('yup').Number} validator yup validator + * @param {Object} metas model attribute + * @param {{ max: Number }} metas.attr + * + * @returns {{import('yup').StringSchema}} */ -const addMaxFloatValidator = (validator, { max }) => - _.isNumber(max) ? validator.max(max) : validator; +const addMaxFloatValidator = (validator, { attr }) => + _.isNumber(attr.max) ? validator.max(attr.max) : validator; /** * Adds regex validator - * @param {Object} attribute model attribute - * @param {Object} validator yup validator + * @param {import('yup').StringSchema} validator yup validator + * @param {Object} metas model attribute + * @param {{ regex: RegExp }} metas.attr + * + * @returns {{import('yup').StringSchema}} */ -const addStringRegexValidator = (validator, { regex }) => - _.isUndefined(regex) ? validator : validator.matches(new RegExp(regex)); +const addStringRegexValidator = (validator, { attr }) => + _.isUndefined(attr.regex) ? validator : validator.matches(new RegExp(attr.regex)); -const addUniqueValidator = (validator, attr, __, model, updatedAttribute, entity) => { +/** + * + * @param {import('yup').AnySchema} validator + * @param {Object} metas + * @param {{ unique: Boolean, type: String }} metas.attr + * @param {{ uid: String }} metas.model + * @param {{ name: String, value: any }} metas.updatedAttribute + * @param {Object} metas.entity + * + * @returns {{import('yup').StringSchema}} + */ +const addUniqueValidator = (validator, { attr, model, updatedAttribute, entity }) => { if (!attr.unique && attr.type !== 'uid') { return validator; } - /** - * If the attribute value is `null` we want to skip the unique validation. - * Otherwise it'll only accept a single `null` entry in the database. - */ - if (updatedAttribute.value === null) { - return validator; - } - - /** - * If the attribute is unchanged we skip the unique verification. This will - * prevent the validator to be triggered in case the user activated the - * unique constraint after already creating multiple entries with - * the same attribute value for that field. - */ - if (entity && updatedAttribute.value === entity[updatedAttribute.name]) { - return validator; - } - return validator.test('unique', 'This attribute must be unique', async value => { + /** + * If the attribute value is `null` we want to skip the unique validation. + * Otherwise it'll only accept a single `null` entry in the database. + */ + if (updatedAttribute.value === null) { + return true; + } + + /** + * If the attribute is unchanged we skip the unique verification. This will + * prevent the validator to be triggered in case the user activated the + * unique constraint after already creating multiple entries with + * the same attribute value for that field. + */ + if (entity && updatedAttribute.value === entity[updatedAttribute.name]) { + return true; + } + let whereParams = entity ? { $and: [{ [updatedAttribute.name]: value }, { $not: { id: entity.id } }] } : { [updatedAttribute.name]: value }; @@ -130,7 +164,7 @@ const uidValidator = composeValidators(stringValidator, validator => validator.matches(new RegExp('^[A-Za-z0-9-_.~]*$')) ); -const enumerationValidator = attr => { +const enumerationValidator = ({ attr }) => { return yup.string().oneOf((Array.isArray(attr.enum) ? attr.enum : [attr.enum]).concat(null)); }; From 77a1684577652f2c5843ce6f07e80a76fd6006c4 Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Mon, 29 Nov 2021 11:24:33 +0100 Subject: [PATCH 10/11] fix typedefs --- .../lib/services/entity-validator/validators.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index 33e12d6c9f..c0a339d90d 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -47,22 +47,22 @@ const addMaxLengthValidator = (validator, { attr }) => /** * Adds min integer validator - * @param {import('yup').Number} validator yup validator + * @param {import('yup').NumberSchema} validator yup validator * @param {Object} metas * @param {{ min: Number }} metas.attr model attribute * - * @returns {{import('yup').StringSchema}} + * @returns {{import('yup').NumberSchema}} */ const addMinIntegerValidator = (validator, { attr }) => _.isNumber(attr.min) ? validator.min(_.toInteger(attr.min)) : validator; /** * Adds max integer validator - * @param {import('yup').Number} validator yup validator + * @param {import('yup').NumberSchema} validator yup validator * @param {Object} metas * @param {{ max: Number }} metas.attr model attribute * - * @returns {{import('yup').StringSchema}} + * @returns {{import('yup').NumberSchema}} */ const addMaxIntegerValidator = (validator, { attr }) => _.isNumber(attr.max) ? validator.max(_.toInteger(attr.max)) : validator; @@ -73,18 +73,18 @@ const addMaxIntegerValidator = (validator, { attr }) => * @param {Object} metas * @param {{ min: Number }} metas.attr model attribute * - * @returns {{import('yup').StringSchema}} + * @returns {{import('yup').NumberSchema}} */ const addMinFloatValidator = (validator, { attr }) => _.isNumber(attr.min) ? validator.min(attr.min) : validator; /** * Adds max float/decimal validator - * @param {import('yup').Number} validator yup validator + * @param {import('yup').NumberSchema} validator yup validator * @param {Object} metas model attribute * @param {{ max: Number }} metas.attr * - * @returns {{import('yup').StringSchema}} + * @returns {{import('yup').NumberSchema}} */ const addMaxFloatValidator = (validator, { attr }) => _.isNumber(attr.max) ? validator.max(attr.max) : validator; From 652cac024301e9bf6a680f1babd12819d2ad5ac9 Mon Sep 17 00:00:00 2001 From: Dieter Stinglhamber Date: Mon, 29 Nov 2021 11:46:22 +0100 Subject: [PATCH 11/11] improve jsdocs --- .../services/entity-validator/validators.js | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/core/strapi/lib/services/entity-validator/validators.js b/packages/core/strapi/lib/services/entity-validator/validators.js index c0a339d90d..ef039eb676 100644 --- a/packages/core/strapi/lib/services/entity-validator/validators.js +++ b/packages/core/strapi/lib/services/entity-validator/validators.js @@ -4,6 +4,12 @@ const _ = require('lodash'); const { yup } = require('@strapi/utils'); +/** + * @type {import('yup').StringSchema} StringSchema + * @type {import('yup').NumberSchema} NumberSchema + * @type {import('yup').AnySchema} AnySchema + */ + /** * Utility function to compose validators */ @@ -23,93 +29,93 @@ const composeValidators = (...fns) => (...args) => { /** * Adds minLength validator - * @param {import('yup').StringSchema} validator yup validator + * @param {StringSchema} validator yup validator * @param {Object} metas * @param {{ minLength: Number }} metas.attr model attribute * @param {Object} options * @param {boolean} options.isDraft * - * @returns {{import('yup').StringSchema}} + * @returns {StringSchema} */ const addMinLengthValidator = (validator, { attr }, { isDraft }) => _.isInteger(attr.minLength) && !isDraft ? validator.min(attr.minLength) : validator; /** * Adds maxLength validator - * @param {import('yup').StringSchema} validator yup validator + * @param {StringSchema} validator yup validator * @param {Object} metas * @param {{ maxLength: Number }} metas.attr model attribute * - * @returns {{import('yup').StringSchema}} + * @returns {StringSchema} */ const addMaxLengthValidator = (validator, { attr }) => _.isInteger(attr.maxLength) ? validator.max(attr.maxLength) : validator; /** * Adds min integer validator - * @param {import('yup').NumberSchema} validator yup validator + * @param {NumberSchema} validator yup validator * @param {Object} metas * @param {{ min: Number }} metas.attr model attribute * - * @returns {{import('yup').NumberSchema}} + * @returns {NumberSchema} */ const addMinIntegerValidator = (validator, { attr }) => _.isNumber(attr.min) ? validator.min(_.toInteger(attr.min)) : validator; /** * Adds max integer validator - * @param {import('yup').NumberSchema} validator yup validator + * @param {NumberSchema} validator yup validator * @param {Object} metas * @param {{ max: Number }} metas.attr model attribute * - * @returns {{import('yup').NumberSchema}} + * @returns {NumberSchema} */ const addMaxIntegerValidator = (validator, { attr }) => _.isNumber(attr.max) ? validator.max(_.toInteger(attr.max)) : validator; /** * Adds min float/decimal validator - * @param {import('yup').NumberSchema} validator yup validator + * @param {NumberSchema} validator yup validator * @param {Object} metas * @param {{ min: Number }} metas.attr model attribute * - * @returns {{import('yup').NumberSchema}} + * @returns {NumberSchema} */ const addMinFloatValidator = (validator, { attr }) => _.isNumber(attr.min) ? validator.min(attr.min) : validator; /** * Adds max float/decimal validator - * @param {import('yup').NumberSchema} validator yup validator + * @param {NumberSchema} validator yup validator * @param {Object} metas model attribute * @param {{ max: Number }} metas.attr * - * @returns {{import('yup').NumberSchema}} + * @returns {NumberSchema} */ const addMaxFloatValidator = (validator, { attr }) => _.isNumber(attr.max) ? validator.max(attr.max) : validator; /** * Adds regex validator - * @param {import('yup').StringSchema} validator yup validator + * @param {StringSchema} validator yup validator * @param {Object} metas model attribute * @param {{ regex: RegExp }} metas.attr * - * @returns {{import('yup').StringSchema}} + * @returns {StringSchema} */ const addStringRegexValidator = (validator, { attr }) => _.isUndefined(attr.regex) ? validator : validator.matches(new RegExp(attr.regex)); /** * - * @param {import('yup').AnySchema} validator + * @param {AnySchema} validator * @param {Object} metas * @param {{ unique: Boolean, type: String }} metas.attr * @param {{ uid: String }} metas.model * @param {{ name: String, value: any }} metas.updatedAttribute * @param {Object} metas.entity * - * @returns {{import('yup').StringSchema}} + * @returns {AnySchema} */ const addUniqueValidator = (validator, { attr, model, updatedAttribute, entity }) => { if (!attr.unique && attr.type !== 'uid') {