diff --git a/packages/core/strapi/lib/services/__tests__/entity-validator.test.js b/packages/core/strapi/lib/services/__tests__/entity-validator.test.js index 28ca9eb960..4264975fd6 100644 --- a/packages/core/strapi/lib/services/__tests__/entity-validator.test.js +++ b/packages/core/strapi/lib/services/__tests__/entity-validator.test.js @@ -5,15 +5,16 @@ const entityValidator = require('../entity-validator'); describe('Entity validator', () => { describe('Published input', () => { describe('General Errors', () => { - it('Throws a badRequest error on invalid input', async () => { - global.strapi = { - errors: { - badRequest: jest.fn(), - }, - getModel: () => ({}), - }; + let model; + global.strapi = { + errors: { + badRequest: jest.fn(), + }, + getModel: () => model, + }; - const model = { + it('Throws a badRequest error on invalid input', async () => { + model = { attributes: { title: { type: 'string', @@ -45,7 +46,7 @@ describe('Entity validator', () => { }); it('Returns data on valid input', async () => { - const model = { + model = { attributes: { title: { type: 'string', @@ -62,7 +63,7 @@ describe('Entity validator', () => { }); it('Returns casted data when possible', async () => { - const model = { + model = { attributes: { title: { type: 'string', @@ -85,14 +86,7 @@ describe('Entity validator', () => { }); test('Throws on required not respected', async () => { - global.strapi = { - errors: { - badRequest: jest.fn(), - }, - getModel: () => ({}), - }; - - const model = { + model = { attributes: { title: { type: 'string', @@ -141,7 +135,7 @@ describe('Entity validator', () => { }); it('Supports custom field types', async () => { - const model = { + model = { attributes: { uuid: { type: 'uuid', @@ -166,7 +160,7 @@ describe('Entity validator', () => { errors: { badRequest: jest.fn(), }, - getModel: () => ({}), + getModel: () => model, }; const model = { @@ -202,13 +196,6 @@ describe('Entity validator', () => { }); test('Throws on max length not respected', async () => { - global.strapi = { - errors: { - badRequest: jest.fn(), - }, - getModel: () => ({}), - }; - const model = { attributes: { title: { @@ -333,7 +320,7 @@ describe('Entity validator', () => { errors: { badRequest: jest.fn(), }, - getModel: () => ({}), + getModel: () => model, }; const model = { @@ -461,6 +448,13 @@ describe('Entity validator', () => { }, }; + global.strapi = { + errors: { + badRequest: jest.fn(), + }, + getModel: () => model, + }; + const input = { title: 'tooSmall' }; expect.hasAssertions(); @@ -470,13 +464,6 @@ describe('Entity validator', () => { }); test('Throws on max length not respected', async () => { - global.strapi = { - errors: { - badRequest: jest.fn(), - }, - getModel: () => ({}), - }; - const model = { attributes: { title: { diff --git a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js index dca0c8ffdd..ae7c4e8570 100644 --- a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js +++ b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service-events.test.js @@ -4,28 +4,22 @@ const createEntityService = require('..'); const entityValidator = require('../../entity-validator'); describe('Entity service triggers webhooks', () => { - global.strapi = { - getModel: () => ({}), - config: { - get: () => [], - }, - }; - let instance; const eventHub = { emit: jest.fn() }; let entity = { attr: 'value' }; beforeAll(() => { + const model = { + kind: 'singleType', + modelName: 'test-model', + privateAttributes: [], + attributes: { + attr: { type: 'string' }, + }, + }; instance = createEntityService({ strapi: { - getModel: () => ({ - kind: 'singleType', - modelName: 'test-model', - privateAttributes: [], - attributes: { - attr: { type: 'string' }, - }, - }), + getModel: () => model, }, db: { query: () => ({ @@ -41,6 +35,13 @@ describe('Entity service triggers webhooks', () => { eventHub, entityValidator, }); + + global.strapi = { + getModel: () => model, + config: { + get: () => [], + }, + }; }); test('Emit event: Create', async () => { diff --git a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js index 65a00a8b48..7c95ab8b61 100644 --- a/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js +++ b/packages/core/strapi/lib/services/entity-service/__tests__/entity-service.test.js @@ -157,16 +157,15 @@ describe('Entity service', () => { }, }; const fakeQuery = (uid) => ({ - count: jest.fn(() => 0), create: jest.fn(({ data }) => data), - findWithCount: jest.fn(({ where }) => { - const ret = []; + count: jest.fn(({ where }) => { + let ret = 0; where.id.$in.forEach((id) => { const entity = fakeEntities[uid][id]; if (!entity) return; - ret.push(entity); + ret += 1; }); - return [ret, ret.length]; + return ret; }), }); @@ -372,14 +371,14 @@ describe('Entity service', () => { beforeAll(() => { const fakeQuery = (key) => ({ findOne: jest.fn(({ where }) => fakeEntities[key][where.id]), - findWithCount: jest.fn(({ where }) => { - const ret = []; + count: jest.fn(({ where }) => { + let ret = 0; where.id.$in.forEach((id) => { const entity = fakeEntities[key][id]; if (!entity) return; - ret.push(entity); + ret += 1; }); - return [ret, ret.length]; + return ret; }), update: jest.fn(({ where }) => ({ ...fakeEntities[key][where.id], diff --git a/packages/core/strapi/lib/services/entity-validator/index.js b/packages/core/strapi/lib/services/entity-validator/index.js index 2a0f7608b6..ddc340049e 100644 --- a/packages/core/strapi/lib/services/entity-validator/index.js +++ b/packages/core/strapi/lib/services/entity-validator/index.js @@ -205,107 +205,6 @@ const createModelValidator = return yup.object().shape(schema); }; -/** - * Builds a map containing all the media and relations being associated with an entity - * @param {String} uid of the model - * @param {Object} data - * @param {Map} relationsMap to be updated and returned - * @returns - */ -const buildRelationsMap = (uid, data, relationsMap = new Map()) => { - const currentModel = strapi.getModel(uid); - if (isEmpty(currentModel)) return; - - Object.keys(currentModel.attributes).forEach((attributeName) => { - const attribute = currentModel.attributes[attributeName]; - const value = data[attributeName]; - if (isEmpty(value) || isNil(value)) { - return; - } - switch (attribute.type) { - case 'relation': { - if (!attribute.target) { - break; - } - - // If the attribute type is a relation keep track of all - // associations being made with relations. These will later be checked - // against the DB to confirm they exist - let directValue = []; - if (Array.isArray(value)) { - directValue = value.map((v) => ({ id: v })); - } - relationsMap.set( - attribute.target, - (relationsMap.get(attribute.target) || []).concat( - ...(value.connect || value.set || directValue) - ) - ); - break; - } - case 'media': { - // For media attribute types keep track of all media associated with - // this entity. These will later be checked against the DB to confirm - // they exist - const mediaUID = 'plugin::upload.file'; - castArray(value).forEach((v) => - relationsMap.set(mediaUID, [...(relationsMap.get(mediaUID) || []), { id: v.id || v }]) - ); - break; - } - case 'component': { - return castArray(value).forEach((componentValue) => - buildRelationsMap(attribute.component, componentValue, relationsMap) - ); - } - case 'dynamiczone': { - return value.forEach((dzValue) => - buildRelationsMap(dzValue.__component, dzValue, relationsMap) - ); - } - default: - break; - } - }); - - return relationsMap; -}; - -/** - * Iterate through the relations map and validate that every relation or media - * mentioned exists - */ -const checkRelationsExist = async (relationsMap = new Map()) => { - const promises = []; - for (const [key, value] of relationsMap) { - const evaluate = async () => { - const uniqueValues = uniqBy(value, `id`); - // eslint-disable-next-line no-unused-vars - const [entities, count] = await strapi.db.query(key).findWithCount({ - where: { - id: { - $in: uniqueValues.map((v) => Number(v.id)), - }, - }, - }); - - if (count !== uniqueValues.length) { - const missingEntities = uniqueValues.filter( - (value) => !entities.find((entity) => entity.id === value.id) - ); - throw new ValidationError( - `Relations of type ${key} associated with this entity do not exist. IDs: ${missingEntities - .map((entity) => entity.id) - .join(',')}` - ); - } - }; - promises.push(evaluate()); - } - - return Promise.all(promises); -}; - const createValidateEntity = (createOrUpdate) => async (model, data, { isDraft = false } = {}, entity = null) => { @@ -327,7 +226,7 @@ const createValidateEntity = ) .test('relations-test', 'check that all relations exist', async function (data) { try { - await checkRelationsExist(buildRelationsMap(model.uid, data) || new Map()); + await checkRelationsExist(buildRelationsStore(model.uid, data) || {}); } catch (e) { return this.createError({ path: this.path, @@ -341,6 +240,100 @@ const createValidateEntity = return validateYupSchema(validator, { strict: false, abortEarly: false })(data); }; +/** + * Builds an object containing all the media and relations being associated with an entity + * @param {String} uid of the model + * @param {Object} data + * @param {Object} relationsStore to be updated and returned + * @returns + */ +const buildRelationsStore = (uid, data, relationsStore = {}) => { + const currentModel = strapi.getModel(uid); + + Object.keys(currentModel?.attributes || {}).forEach((attributeName) => { + const attribute = currentModel.attributes[attributeName]; + + const value = data[attributeName]; + if (isEmpty(value) || isNil(value)) { + return; + } + + switch (attribute.type) { + case 'relation': { + if (!attribute.target) { + return; + } + // If the attribute type is a relation keep track of all + // associations being made with relations. + let directValue = []; + if (Array.isArray(value)) { + directValue = value.map((v) => ({ id: v })); + } + relationsStore[attribute.target] = relationsStore[attribute.target] || []; + relationsStore[attribute.target].push(...(value.connect || value.set || directValue)); + break; + } + case 'media': { + // For media attribute types keep track of all media associated with + // this entity. + const mediaUID = 'plugin::upload.file'; + castArray(value).forEach((v) => { + relationsStore[mediaUID] = relationsStore[mediaUID] || []; + relationsStore[mediaUID].push({ id: v.id || v }); + }); + break; + } + case 'component': { + return castArray(value).forEach((componentValue) => + buildRelationsStore(attribute.component, componentValue, relationsStore) + ); + } + case 'dynamiczone': { + return value.forEach((dzValue) => + buildRelationsStore(dzValue.__component, dzValue, relationsStore) + ); + } + default: + break; + } + }); + + return relationsStore; +}; + +/** + * Iterate through the relations store and validates that every relation or media + * mentioned exists + */ +const checkRelationsExist = async (relationsStore = {}) => { + const promises = []; + + for (const [key, value] of Object.entries(relationsStore)) { + const evaluate = async () => { + const uniqueValues = uniqBy(value, `id`); + // eslint-disable-next-line no-unused-vars + const count = await strapi.db.query(key).count({ + where: { + id: { + $in: uniqueValues.map((v) => Number(v.id)), + }, + }, + }); + + if (count !== uniqueValues.length) { + throw new ValidationError( + `${ + uniqueValues.length - count + } relation(s) of type ${key} associated with this entity do not exist` + ); + } + }; + promises.push(evaluate()); + } + + return Promise.all(promises); +}; + module.exports = { validateEntityCreation: createValidateEntity('creation'), validateEntityUpdate: createValidateEntity('update'), diff --git a/packages/core/strapi/tests/endpoint.test.e2e.js b/packages/core/strapi/tests/endpoint.test.e2e.js index 05e41bc0c0..d7b19a3bec 100644 --- a/packages/core/strapi/tests/endpoint.test.e2e.js +++ b/packages/core/strapi/tests/endpoint.test.e2e.js @@ -163,9 +163,7 @@ describe('Create Strapi API End to End', () => { expect(res.statusCode).toBe(400); expect(JSON.parse(res.error.text).error.message).toContain( - `Relations of type api::tag.tag associated with this entity do not exist. IDs: ${entry.tags.join( - ',' - )}` + `1 relation(s) of type api::tag.tag associated with this entity do not exist` ); }); @@ -243,9 +241,7 @@ describe('Create Strapi API End to End', () => { expect(res.statusCode).toBe(400); expect(JSON.parse(res.error.text).error.message).toContain( - `Relations of type api::tag.tag associated with this entity do not exist. IDs: ${entry.tags.join( - ',' - )}` + `3 relation(s) of type api::tag.tag associated with this entity do not exist` ); });