From 1b6ccc76b6a3b199b204146f75497223b587210a Mon Sep 17 00:00:00 2001 From: WalkingPizza Date: Thu, 1 Sep 2022 22:45:23 +0200 Subject: [PATCH 01/10] Trim admin's first name when validating --- packages/core/admin/admin/src/pages/AuthPage/utils/forms.js | 4 ++-- .../pages/Users/ListPage/ModalForm/utils/schema.js | 2 +- .../pages/Users/utils/validations/users/profile.js | 4 ++-- packages/core/admin/server/validation/common-validators.js | 2 +- packages/core/strapi/lib/commands/admin-create.js | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/admin/admin/src/pages/AuthPage/utils/forms.js b/packages/core/admin/admin/src/pages/AuthPage/utils/forms.js index 5349fc27cc..ec4239ee34 100644 --- a/packages/core/admin/admin/src/pages/AuthPage/utils/forms.js +++ b/packages/core/admin/admin/src/pages/AuthPage/utils/forms.js @@ -52,7 +52,7 @@ const forms = { fieldsToDisable: ['email'], fieldsToOmit: ['userInfo.confirmPassword', 'userInfo.news', 'userInfo.email'], schema: yup.object().shape({ - firstname: yup.string().required(translatedErrors.required), + firstname: yup.string().trim().required(translatedErrors.required), lastname: yup.string(), password: yup .string() @@ -76,7 +76,7 @@ const forms = { fieldsToDisable: [], fieldsToOmit: ['confirmPassword', 'news'], schema: yup.object().shape({ - firstname: yup.string().required(translatedErrors.required), + firstname: yup.string().trim().required(translatedErrors.required), lastname: yup.string(), password: yup .string() diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/utils/schema.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/utils/schema.js index 9ae27dd2c5..adf046691b 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/utils/schema.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/ListPage/ModalForm/utils/schema.js @@ -2,7 +2,7 @@ import * as yup from 'yup'; import { translatedErrors } from '@strapi/helper-plugin'; const schema = yup.object().shape({ - firstname: yup.string().required(translatedErrors.required), + firstname: yup.string().trim().required(translatedErrors.required), lastname: yup.string(), email: yup.string().email(translatedErrors.email).required(translatedErrors.required), roles: yup.array().min(1, translatedErrors.required).required(translatedErrors.required), diff --git a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js index 3b241c1447..69b5a14668 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/pages/Users/utils/validations/users/profile.js @@ -2,8 +2,8 @@ import * as yup from 'yup'; import { translatedErrors } from '@strapi/helper-plugin'; export const commonUserSchema = { - firstname: yup.mixed().required(translatedErrors.required), - lastname: yup.mixed(), + firstname: yup.string().trim().required(translatedErrors.required), + lastname: yup.string(), email: yup.string().email(translatedErrors.email).lowercase().required(translatedErrors.required), username: yup.string().nullable(), password: yup diff --git a/packages/core/admin/server/validation/common-validators.js b/packages/core/admin/server/validation/common-validators.js index 64716bd0df..42193d7ced 100644 --- a/packages/core/admin/server/validation/common-validators.js +++ b/packages/core/admin/server/validation/common-validators.js @@ -16,7 +16,7 @@ const getActionFromProvider = (actionId) => { const email = yup.string().email().lowercase(); -const firstname = yup.string().min(1); +const firstname = yup.string().trim().min(1); const lastname = yup.string(); diff --git a/packages/core/strapi/lib/commands/admin-create.js b/packages/core/strapi/lib/commands/admin-create.js index 967a58cc69..754c8b156a 100644 --- a/packages/core/strapi/lib/commands/admin-create.js +++ b/packages/core/strapi/lib/commands/admin-create.js @@ -17,7 +17,7 @@ const passwordValidator = yup const adminCreateSchema = yup.object().shape({ email: emailValidator, password: passwordValidator, - firstname: yup.string().required('First name is required'), + firstname: yup.string().trim().required('First name is required'), lastname: yup.string(), }); From f681c064c4213cfdd7629e64962eb986c6612857 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 23 Aug 2022 10:56:36 +0200 Subject: [PATCH 02/10] Fix relation not correctly set when uploadinga new file to an entity --- packages/core/database/lib/entity-manager.js | 32 ++++++++++++ .../tests/content-api/upload.test.e2e.js | 50 ++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager.js index 7d492e4b71..b4a8022861 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager.js @@ -402,6 +402,21 @@ const createEntityManager = (db) => { continue; } + // delete previous relations + const where = { $or: [] }; + + for (const { field, related_type: relatedType, related_id: relatedId } of rows) { + const isMorphOne = + db.metadata.get(relatedType).attributes[field].relation === 'morphOne'; + if (isMorphOne) { + where.$or.push({ related_type: relatedType, field, related_id: relatedId }); + } + } + + if (!_.isEmpty(where.$or)) { + await this.createQueryBuilder(joinTable.name).delete().where(where).execute(); + } + await this.createQueryBuilder(joinTable.name).insert(rows).execute(); continue; @@ -503,6 +518,8 @@ const createEntityManager = (db) => { // set columns const { idColumn, typeColumn } = targetAttribute.morphColumn; + // update instead of deleting because the relation is directly on the entity table + // and not in a join table await this.createQueryBuilder(target) .update({ [idColumn.name]: null, [typeColumn.name]: null }) .where({ [idColumn.name]: id, [typeColumn.name]: uid }) @@ -581,6 +598,21 @@ const createEntityManager = (db) => { continue; } + // delete previous relations + const where = { $or: [] }; + + for (const { field, related_type: relatedType, related_id: relatedId } of rows) { + const isMorphOne = + db.metadata.get(relatedType).attributes[field].relation === 'morphOne'; + if (isMorphOne) { + where.$or.push({ related_type: relatedType, field, related_id: relatedId }); + } + } + + if (!_.isEmpty(where.$or)) { + await this.createQueryBuilder(joinTable.name).delete().where(where).execute(); + } + await this.createQueryBuilder(joinTable.name).insert(rows).execute(); continue; diff --git a/packages/core/upload/tests/content-api/upload.test.e2e.js b/packages/core/upload/tests/content-api/upload.test.e2e.js index 0a1fedd7bc..5bbf81f5b9 100644 --- a/packages/core/upload/tests/content-api/upload.test.e2e.js +++ b/packages/core/upload/tests/content-api/upload.test.e2e.js @@ -9,6 +9,7 @@ const { createStrapiInstance } = require('../../../../../test/helpers/strapi'); const { createContentAPIRequest } = require('../../../../../test/helpers/request'); const builder = createTestBuilder(); +const data = { dogs: [] }; let strapi; let rq; @@ -24,7 +25,7 @@ const dogModel = { }, }; -describe('Upload plugin end to end tests', () => { +describe('Upload plugin', () => { beforeAll(async () => { await builder.addContentType(dogModel).build(); strapi = await createStrapiInstance(); @@ -155,6 +156,8 @@ describe('Upload plugin end to end tests', () => { id: expect.anything(), }, }); + + data.dogs.push(res.body); }); test('With a pdf', async () => { @@ -183,6 +186,51 @@ describe('Upload plugin end to end tests', () => { id: expect.anything(), }, }); + data.dogs.push(res.body); + }); + }); + + // see https://github.com/strapi/strapi/issues/14125 + describe('File relations are correctly removed', () => { + test('Update an entity with a file correctly removes the relation between the entity and its old file', async () => { + const res = await rq({ + method: 'PUT', + url: `/dogs/${data.dogs[0].data.id}?populate=*`, + formData: { + data: '{}', + 'files.profilePicture': fs.createReadStream(path.join(__dirname, '../utils/strapi.jpg')), + }, + }); + + expect(res.statusCode).toBe(200); + expect(res.body.data.attributes.profilePicture.data.id).not.toBe( + data.dogs[0].data.attributes.profilePicture.data.id + ); + + data.dogs[0] = res.body; + }); + + test('Update a file with an entity correctly removes the relation between the entity and its old file', async () => { + const fileId = data.dogs[1].data.attributes.profilePicture.data.id; + await strapi.entityService.update('plugin::upload.file', fileId, { + data: { + related: [ + { + id: data.dogs[0].data.id, + __type: 'api::dog.dog', + __pivot: { field: 'profilePicture' }, + }, + ], + }, + }); + + const res = await rq({ + method: 'GET', + url: `/dogs/${data.dogs[0].data.id}?populate=*`, + }); + expect(res.body.data.attributes.profilePicture.data.id).toBe(fileId); + + data.dogs[0] = res.body; }); }); }); From 8100cffb3d4727524802e01f2912e1b19e3fd64c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Thu, 25 Aug 2022 11:56:52 +0200 Subject: [PATCH 03/10] use a grouped approach for where --- packages/core/database/lib/entity-manager.js | 95 ++++++++++++-------- 1 file changed, 59 insertions(+), 36 deletions(-) diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager.js index b4a8022861..9b6d5b9478 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager.js @@ -1,6 +1,19 @@ 'use strict'; -const _ = require('lodash/fp'); +const _ = require('lodash'); +const { + isUndefined, + castArray, + isNil, + has, + isString, + isInteger, + pick, + isPlainObject, + isEmpty, + isArray, + isNull, +} = require('lodash/fp'); const types = require('./types'); const { createField } = require('./fields'); const { createQueryBuilder } = require('./query'); @@ -8,12 +21,12 @@ const { createRepository } = require('./entity-repository'); const { isBidirectional, isOneToAny } = require('./metadata/relations'); const toId = (value) => value.id || value; -const toIds = (value) => _.castArray(value || []).map(toId); +const toIds = (value) => castArray(value || []).map(toId); -const isValidId = (value) => _.isString(value) || _.isInteger(value); +const isValidId = (value) => isString(value) || isInteger(value); const toAssocs = (data) => { - return _.castArray(data) - .filter((datum) => !_.isNil(datum)) + return castArray(data) + .filter((datum) => !isNil(datum)) .map((datum) => { // if it is a string or an integer return an obj with id = to datum if (isValidId(datum)) { @@ -21,7 +34,7 @@ const toAssocs = (data) => { } // if it is an object check it has at least a valid id - if (!_.has('id', datum) || !isValidId(datum.id)) { + if (!has('id', datum) || !isValidId(datum.id)) { throw new Error(`Invalid id, expected a string or integer, got ${datum}`); } @@ -40,8 +53,8 @@ const processData = (metadata, data = {}, { withDefaults = false } = {}) => { if (types.isScalar(attribute.type)) { const field = createField(attribute); - if (_.isUndefined(data[attributeName])) { - if (!_.isUndefined(attribute.default) && withDefaults) { + if (isUndefined(data[attributeName])) { + if (!isUndefined(attribute.default) && withDefaults) { if (typeof attribute.default === 'function') { obj[attributeName] = attribute.default(); } else { @@ -66,11 +79,11 @@ const processData = (metadata, data = {}, { withDefaults = false } = {}) => { const joinColumnName = attribute.joinColumn.name; // allow setting to null - const attrValue = !_.isUndefined(data[attributeName]) + const attrValue = !isUndefined(data[attributeName]) ? data[attributeName] : data[joinColumnName]; - if (!_.isUndefined(attrValue)) { + if (!isUndefined(attrValue)) { obj[joinColumnName] = attrValue; } @@ -91,8 +104,8 @@ const processData = (metadata, data = {}, { withDefaults = false } = {}) => { continue; } - if (!_.isUndefined(value)) { - if (!_.has('id', value) || !_.has(typeField, value)) { + if (!isUndefined(value)) { + if (!has('id', value) || !has(typeField, value)) { throw new Error(`Expects properties ${typeField} an id to make a morph association`); } @@ -137,7 +150,7 @@ const createEntityManager = (db) => { const states = await db.lifecycles.run('beforeCount', uid, { params }); const res = await this.createQueryBuilder(uid) - .init(_.pick(['_q', 'where', 'filters'], params)) + .init(pick(['_q', 'where', 'filters'], params)) .count() .first() .execute(); @@ -155,7 +168,7 @@ const createEntityManager = (db) => { const metadata = db.metadata.get(uid); const { data } = params; - if (!_.isPlainObject(data)) { + if (!isPlainObject(data)) { throw new Error('Create expects a data object'); } @@ -187,7 +200,7 @@ const createEntityManager = (db) => { const metadata = db.metadata.get(uid); const { data } = params; - if (!_.isArray(data)) { + if (!isArray(data)) { throw new Error('CreateMany expects data to be an array'); } @@ -195,7 +208,7 @@ const createEntityManager = (db) => { processData(metadata, datum, { withDefaults: true }) ); - if (_.isEmpty(dataToInsert)) { + if (isEmpty(dataToInsert)) { throw new Error('Nothing to insert'); } @@ -214,11 +227,11 @@ const createEntityManager = (db) => { const metadata = db.metadata.get(uid); const { where, data } = params; - if (!_.isPlainObject(data)) { + if (!isPlainObject(data)) { throw new Error('Update requires a data object'); } - if (_.isEmpty(where)) { + if (isEmpty(where)) { throw new Error('Update requires a where parameter'); } @@ -232,7 +245,7 @@ const createEntityManager = (db) => { const dataToUpdate = processData(metadata, data); - if (!_.isEmpty(dataToUpdate)) { + if (!isEmpty(dataToUpdate)) { await this.createQueryBuilder(uid).where({ id }).update(dataToUpdate).execute(); } @@ -259,7 +272,7 @@ const createEntityManager = (db) => { const dataToUpdate = processData(metadata, data); - if (_.isEmpty(dataToUpdate)) { + if (isEmpty(dataToUpdate)) { throw new Error('Update requires data'); } @@ -280,7 +293,7 @@ const createEntityManager = (db) => { const { where, select, populate } = params; - if (_.isEmpty(where)) { + if (isEmpty(where)) { throw new Error('Delete requires a where parameter'); } @@ -336,7 +349,7 @@ const createEntityManager = (db) => { for (const attributeName in attributes) { const attribute = attributes[attributeName]; - const isValidLink = _.has(attributeName, data) && !_.isNil(data[attributeName]); + const isValidLink = has(attributeName, data) && !isNil(data[attributeName]); if (attribute.type !== 'relation' || !isValidLink) { continue; @@ -373,7 +386,7 @@ const createEntityManager = (db) => { }; }); - if (_.isEmpty(rows)) { + if (isEmpty(rows)) { continue; } @@ -398,23 +411,33 @@ const createEntityManager = (db) => { ...(data.__pivot || {}), })); - if (_.isEmpty(rows)) { + if (isEmpty(rows)) { continue; } // delete previous relations - const where = { $or: [] }; + const typeAndFieldIdsMap = {}; for (const { field, related_type: relatedType, related_id: relatedId } of rows) { const isMorphOne = db.metadata.get(relatedType).attributes[field].relation === 'morphOne'; if (isMorphOne) { - where.$or.push({ related_type: relatedType, field, related_id: relatedId }); + const key = `${relatedType}+${field}`; + typeAndFieldIdsMap[key] = typeAndFieldIdsMap[key] || []; + typeAndFieldIdsMap[key].push(relatedId); } } - if (!_.isEmpty(where.$or)) { - await this.createQueryBuilder(joinTable.name).delete().where(where).execute(); + const orWhere = _.map(typeAndFieldIdsMap, (ids, typeAndField) => { + const [type, field] = typeAndField.split('+'); + return { related_type: type, field, related_id: { $in: ids } }; + }); + + if (!isEmpty(orWhere)) { + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ $or: orWhere }) + .execute(); } await this.createQueryBuilder(joinTable.name).insert(rows).execute(); @@ -465,7 +488,7 @@ const createEntityManager = (db) => { if (isOneToAny(attribute) && isBidirectional(attribute)) { await this.createQueryBuilder(joinTable.name) .delete() - .where({ [inverseJoinColumn.name]: _.castArray(data[attributeName]) }) + .where({ [inverseJoinColumn.name]: castArray(data[attributeName]) }) .where(joinTable.on || {}) .execute(); } @@ -505,7 +528,7 @@ const createEntityManager = (db) => { for (const attributeName in attributes) { const attribute = attributes[attributeName]; - if (attribute.type !== 'relation' || !_.has(attributeName, data)) { + if (attribute.type !== 'relation' || !has(attributeName, data)) { continue; } @@ -525,7 +548,7 @@ const createEntityManager = (db) => { .where({ [idColumn.name]: id, [typeColumn.name]: uid }) .execute(); - if (!_.isNull(data[attributeName])) { + if (!isNull(data[attributeName])) { await this.createQueryBuilder(target) .update({ [idColumn.name]: id, [typeColumn.name]: uid }) .where({ id: toId(data[attributeName]) }) @@ -557,7 +580,7 @@ const createEntityManager = (db) => { field: attributeName, })); - if (_.isEmpty(rows)) { + if (isEmpty(rows)) { continue; } @@ -594,7 +617,7 @@ const createEntityManager = (db) => { ...(data.__pivot || {}), })); - if (_.isEmpty(rows)) { + if (isEmpty(rows)) { continue; } @@ -609,7 +632,7 @@ const createEntityManager = (db) => { } } - if (!_.isEmpty(where.$or)) { + if (!isEmpty(where.$or)) { await this.createQueryBuilder(joinTable.name).delete().where(where).execute(); } @@ -634,7 +657,7 @@ const createEntityManager = (db) => { .update({ [attribute.joinColumn.referencedColumn]: null }) .execute(); - if (!_.isNull(data[attributeName])) { + if (!isNull(data[attributeName])) { await this.createQueryBuilder(target) // NOTE: works if it is an array or a single id .where({ id: data[attributeName] }) @@ -665,7 +688,7 @@ const createEntityManager = (db) => { .execute(); } - if (!_.isNull(data[attributeName])) { + if (!isNull(data[attributeName])) { const insert = toAssocs(data[attributeName]).map((data) => { return { [joinColumn.name]: id, From a658c6cc0a83b4b3c4d25719551004fd0b4f3f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Fri, 26 Aug 2022 17:35:11 +0200 Subject: [PATCH 04/10] handle component use case --- packages/core/database/lib/entity-manager.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager.js index 9b6d5b9478..0bfaa1d67e 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager.js @@ -346,7 +346,7 @@ const createEntityManager = (db) => { async attachRelations(uid, id, data) { const { attributes } = db.metadata.get(uid); - for (const attributeName in attributes) { + for (const attributeName of Object.keys(attributes)) { const attribute = attributes[attributeName]; const isValidLink = has(attributeName, data) && !isNil(data[attributeName]); @@ -419,6 +419,8 @@ const createEntityManager = (db) => { const typeAndFieldIdsMap = {}; for (const { field, related_type: relatedType, related_id: relatedId } of rows) { + if (!relatedType) continue; // if !relatedType then it's a component. No need to handle that case + const isMorphOne = db.metadata.get(relatedType).attributes[field].relation === 'morphOne'; if (isMorphOne) { @@ -525,7 +527,7 @@ const createEntityManager = (db) => { async updateRelations(uid, id, data) { const { attributes } = db.metadata.get(uid); - for (const attributeName in attributes) { + for (const attributeName of Object.keys(attributes)) { const attribute = attributes[attributeName]; if (attribute.type !== 'relation' || !has(attributeName, data)) { @@ -625,6 +627,8 @@ const createEntityManager = (db) => { const where = { $or: [] }; for (const { field, related_type: relatedType, related_id: relatedId } of rows) { + if (!relatedType) continue; // if !relatedType then it's a component. No need to handle that case + const isMorphOne = db.metadata.get(relatedType).attributes[field].relation === 'morphOne'; if (isMorphOne) { From aa53790bd521afb8b1c874019d08a5a0678aa7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Fri, 2 Sep 2022 12:18:48 +0200 Subject: [PATCH 05/10] refactor with groupby --- packages/core/database/lib/entity-manager.js | 85 +++++++++++++------ .../database/lib/query/helpers/populate.js | 2 +- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager.js index 0bfaa1d67e..d2382fa2e8 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager.js @@ -1,6 +1,5 @@ 'use strict'; -const _ = require('lodash'); const { isUndefined, castArray, @@ -13,6 +12,10 @@ const { isEmpty, isArray, isNull, + groupBy, + pipe, + mapValues, + map, } = require('lodash/fp'); const types = require('./types'); const { createField } = require('./fields'); @@ -416,25 +419,37 @@ const createEntityManager = (db) => { } // delete previous relations - const typeAndFieldIdsMap = {}; + const morphOneRows = rows.filter((row) => { + const relatedType = row[typeColumn.name]; + const field = row.field; - for (const { field, related_type: relatedType, related_id: relatedId } of rows) { - if (!relatedType) continue; // if !relatedType then it's a component. No need to handle that case + const targetAttribute = db.metadata.get(relatedType).attributes[field]; - const isMorphOne = - db.metadata.get(relatedType).attributes[field].relation === 'morphOne'; - if (isMorphOne) { - const key = `${relatedType}+${field}`; - typeAndFieldIdsMap[key] = typeAndFieldIdsMap[key] || []; - typeAndFieldIdsMap[key].push(relatedId); + // ensure targeted field is the right one + check if it is a morphOne + return ( + targetAttribute?.target === uid && + targetAttribute?.morphBy === attributeName && + targetAttribute?.relation === 'morphOne' + ); + }); + + const groupByType = groupBy(typeColumn.name); + const groupByField = groupBy('field'); + + const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows); + + const orWhere = []; + + for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) { + for (const [field, arr] of Object.entries(v)) { + orWhere.push({ + [typeColumn.name]: type, + field, + [idColumn.name]: { $in: map(idColumn.name, arr) }, + }); } } - const orWhere = _.map(typeAndFieldIdsMap, (ids, typeAndField) => { - const [type, field] = typeAndField.split('+'); - return { related_type: type, field, related_id: { $in: ids } }; - }); - if (!isEmpty(orWhere)) { await this.createQueryBuilder(joinTable.name) .delete() @@ -624,20 +639,42 @@ const createEntityManager = (db) => { } // delete previous relations - const where = { $or: [] }; + const morphOneRows = rows.filter((row) => { + const relatedType = row[typeColumn.name]; + const field = row.field; - for (const { field, related_type: relatedType, related_id: relatedId } of rows) { - if (!relatedType) continue; // if !relatedType then it's a component. No need to handle that case + const targetAttribute = db.metadata.get(relatedType).attributes[field]; - const isMorphOne = - db.metadata.get(relatedType).attributes[field].relation === 'morphOne'; - if (isMorphOne) { - where.$or.push({ related_type: relatedType, field, related_id: relatedId }); + // ensure targeted field is the right one + check if it is a morphOne + return ( + targetAttribute?.target === uid && + targetAttribute?.morphBy === attributeName && + targetAttribute?.relation === 'morphOne' + ); + }); + + const groupByType = groupBy(typeColumn.name); + const groupByField = groupBy('field'); + + const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows); + + const orWhere = []; + + for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) { + for (const [field, arr] of Object.entries(v)) { + orWhere.push({ + [typeColumn.name]: type, + field, + [idColumn.name]: { $in: map(idColumn.name, arr) }, + }); } } - if (!isEmpty(where.$or)) { - await this.createQueryBuilder(joinTable.name).delete().where(where).execute(); + if (!isEmpty(orWhere)) { + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ $or: orWhere }) + .execute(); } await this.createQueryBuilder(joinTable.name).insert(rows).execute(); diff --git a/packages/core/database/lib/query/helpers/populate.js b/packages/core/database/lib/query/helpers/populate.js index c5f01fabe2..1349e2dbfc 100644 --- a/packages/core/database/lib/query/helpers/populate.js +++ b/packages/core/database/lib/query/helpers/populate.js @@ -119,7 +119,7 @@ const applyPopulate = async (results, populate, ctx) => { return results; } - for (const key in populate) { + for (const key of Object.keys(populate)) { const attribute = meta.attributes[key]; const targetMeta = db.metadata.get(attribute.target); From 23657b7b5a215443a92fb7eb4dde4577e3ec8346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Mon, 5 Sep 2022 16:18:00 +0200 Subject: [PATCH 06/10] factorize code --- .../{ => entity-manager}/entity-repository.js | 0 .../index.js} | 95 +++---------------- .../lib/entity-manager/morph-relations.js | 59 ++++++++++++ 3 files changed, 74 insertions(+), 80 deletions(-) rename packages/core/database/lib/{ => entity-manager}/entity-repository.js (100%) rename packages/core/database/lib/{entity-manager.js => entity-manager/index.js} (90%) create mode 100644 packages/core/database/lib/entity-manager/morph-relations.js diff --git a/packages/core/database/lib/entity-repository.js b/packages/core/database/lib/entity-manager/entity-repository.js similarity index 100% rename from packages/core/database/lib/entity-repository.js rename to packages/core/database/lib/entity-manager/entity-repository.js diff --git a/packages/core/database/lib/entity-manager.js b/packages/core/database/lib/entity-manager/index.js similarity index 90% rename from packages/core/database/lib/entity-manager.js rename to packages/core/database/lib/entity-manager/index.js index d2382fa2e8..fe0110999c 100644 --- a/packages/core/database/lib/entity-manager.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -12,16 +12,13 @@ const { isEmpty, isArray, isNull, - groupBy, - pipe, - mapValues, - map, } = require('lodash/fp'); -const types = require('./types'); -const { createField } = require('./fields'); -const { createQueryBuilder } = require('./query'); +const types = require('../types'); +const { createField } = require('../fields'); +const { createQueryBuilder } = require('../query'); const { createRepository } = require('./entity-repository'); -const { isBidirectional, isOneToAny } = require('./metadata/relations'); +const { isBidirectional, isOneToAny } = require('../metadata/relations'); +const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations'); const toId = (value) => value.id || value; const toIds = (value) => castArray(value || []).map(toId); @@ -419,44 +416,13 @@ const createEntityManager = (db) => { } // delete previous relations - const morphOneRows = rows.filter((row) => { - const relatedType = row[typeColumn.name]; - const field = row.field; - - const targetAttribute = db.metadata.get(relatedType).attributes[field]; - - // ensure targeted field is the right one + check if it is a morphOne - return ( - targetAttribute?.target === uid && - targetAttribute?.morphBy === attributeName && - targetAttribute?.relation === 'morphOne' - ); + await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { + uid, + attributeName, + joinTable, + db, }); - const groupByType = groupBy(typeColumn.name); - const groupByField = groupBy('field'); - - const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows); - - const orWhere = []; - - for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) { - for (const [field, arr] of Object.entries(v)) { - orWhere.push({ - [typeColumn.name]: type, - field, - [idColumn.name]: { $in: map(idColumn.name, arr) }, - }); - } - } - - if (!isEmpty(orWhere)) { - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ $or: orWhere }) - .execute(); - } - await this.createQueryBuilder(joinTable.name).insert(rows).execute(); continue; @@ -639,44 +605,13 @@ const createEntityManager = (db) => { } // delete previous relations - const morphOneRows = rows.filter((row) => { - const relatedType = row[typeColumn.name]; - const field = row.field; - - const targetAttribute = db.metadata.get(relatedType).attributes[field]; - - // ensure targeted field is the right one + check if it is a morphOne - return ( - targetAttribute?.target === uid && - targetAttribute?.morphBy === attributeName && - targetAttribute?.relation === 'morphOne' - ); + await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { + uid, + attributeName, + joinTable, + db, }); - const groupByType = groupBy(typeColumn.name); - const groupByField = groupBy('field'); - - const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows); - - const orWhere = []; - - for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) { - for (const [field, arr] of Object.entries(v)) { - orWhere.push({ - [typeColumn.name]: type, - field, - [idColumn.name]: { $in: map(idColumn.name, arr) }, - }); - } - } - - if (!isEmpty(orWhere)) { - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ $or: orWhere }) - .execute(); - } - await this.createQueryBuilder(joinTable.name).insert(rows).execute(); continue; diff --git a/packages/core/database/lib/entity-manager/morph-relations.js b/packages/core/database/lib/entity-manager/morph-relations.js new file mode 100644 index 0000000000..eb2d7c4136 --- /dev/null +++ b/packages/core/database/lib/entity-manager/morph-relations.js @@ -0,0 +1,59 @@ +'use strict'; + +const { groupBy, pipe, mapValues, map, isEmpty } = require('lodash/fp'); +const { createQueryBuilder } = require('../query'); + +const getMorphToManyRowsLinkedToMorphOne = (rows, { uid, attributeName, typeColumn, db }) => + rows.filter((row) => { + const relatedType = row[typeColumn.name]; + const field = row.field; + + const targetAttribute = db.metadata.get(relatedType).attributes[field]; + + // ensure targeted field is the right one + check if it is a morphOne + return ( + targetAttribute?.target === uid && + targetAttribute?.morphBy === attributeName && + targetAttribute?.relation === 'morphOne' + ); + }); + +const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async ( + rows, + { uid, attributeName, joinTable, db } +) => { + const { morphColumn } = joinTable; + const { idColumn, typeColumn } = morphColumn; + + const morphOneRows = getMorphToManyRowsLinkedToMorphOne(rows, { + uid, + attributeName, + typeColumn, + db, + }); + + const groupByType = groupBy(typeColumn.name); + const groupByField = groupBy('field'); + + const typeAndFieldIdsGrouped = pipe(groupByType, mapValues(groupByField))(morphOneRows); + + const orWhere = []; + + for (const [type, v] of Object.entries(typeAndFieldIdsGrouped)) { + for (const [field, arr] of Object.entries(v)) { + orWhere.push({ + [typeColumn.name]: type, + field, + [idColumn.name]: { $in: map(idColumn.name, arr) }, + }); + } + } + + if (!isEmpty(orWhere)) { + await createQueryBuilder(joinTable.name, db).delete().where({ $or: orWhere }).execute(); + } +}; + +module.exports = { + deleteRelatedMorphOneRelationsAfterMorphToManyUpdate, +}; From b68aa8fcc6a14a6cf87092141d6afe9e667e6ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Mon, 5 Sep 2022 16:31:05 +0200 Subject: [PATCH 07/10] fix conflict --- packages/core/database/lib/entity-manager/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index fe0110999c..c1fc7b0164 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -822,7 +822,7 @@ const createEntityManager = (db) => { async load(uid, entity, fields, params) { const { attributes } = db.metadata.get(uid); - const fieldsArr = _.castArray(fields); + const fieldsArr = castArray(fields); fieldsArr.forEach((field) => { const attribute = attributes[field]; @@ -845,7 +845,7 @@ const createEntityManager = (db) => { } if (Array.isArray(fields)) { - return _.pick(fields, entry); + return pick(fields, entry); } return entry[fields]; From 49ec8a1527969dddb1c8144517df7d3b2f9f8a62 Mon Sep 17 00:00:00 2001 From: benjamingeets Date: Sat, 10 Sep 2022 08:48:15 +0200 Subject: [PATCH 08/10] Remove outdated Docker-Repo from README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ceb5c86f35..8b9d5e6281 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Complete installation requirements can be found in the documentation under Date: Sat, 10 Sep 2022 15:02:11 +0200 Subject: [PATCH 09/10] update sharp in upload package --- packages/core/upload/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/upload/package.json b/packages/core/upload/package.json index f45625f887..a71ac8281a 100644 --- a/packages/core/upload/package.json +++ b/packages/core/upload/package.json @@ -42,7 +42,7 @@ "react-redux": "7.2.8", "react-router": "^5.2.0", "react-router-dom": "5.2.0", - "sharp": "0.30.7" + "sharp": "0.31.0" }, "devDependencies": { "@testing-library/dom": "8.17.1", From 150f697bc81a496ea586954636a4df964127c29c Mon Sep 17 00:00:00 2001 From: Julie Plantey Date: Tue, 13 Sep 2022 10:53:33 +0200 Subject: [PATCH 10/10] use custom label for CM LV headers --- .../src/content-manager/components/DynamicTable/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js b/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js index 8636a937d8..c71cb4b9d3 100644 --- a/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js +++ b/packages/core/admin/admin/src/content-manager/components/DynamicTable/index.js @@ -47,7 +47,7 @@ const DynamicTable = ({ ...metadatas, label: formatMessage({ id: getTrad(`containers.ListPage.table-headers.${header.name}`), - defaultMessage: header.name, + defaultMessage: metadatas.label, }), }, name: sortFieldValue, @@ -60,7 +60,7 @@ const DynamicTable = ({ ...metadatas, label: formatMessage({ id: getTrad(`containers.ListPage.table-headers.${header.name}`), - defaultMessage: header.name, + defaultMessage: metadatas.label, }), }, };