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,