diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 63ca0c3020..b798187878 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -29,6 +29,7 @@ const { deletePreviousOneToAnyRelations, deletePreviousAnyToOneRelations, deleteRelations, + cleanOrderColumns, } = require('./regular-relations'); const toId = (value) => value.id || value; @@ -731,7 +732,6 @@ const createEntityManager = (db) => { if (isPartialUpdate) { if (isAnyToOne(attribute)) { cleanRelationData.connect = cleanRelationData.connect.slice(-1); - cleanRelationData.disconnect = []; } relIdsToaddOrMove = toIds(cleanRelationData.connect); const relIdsToDelete = toIds( @@ -813,11 +813,7 @@ const createEntityManager = (db) => { // insert rows const query = this.createQueryBuilder(joinTable.name) .insert(insert) - .onConflict([ - joinColumn.name, - inverseJoinColumn.name, - ...Object.keys(cleanRelationData.connect[0].__pivot || {}), - ]); + .onConflict(joinTable.pivotColumns); if (isAnyToMany(attribute)) { query.merge([orderColumnName]); @@ -828,22 +824,11 @@ const createEntityManager = (db) => { await query.execute(); // remove gap between orders - if (isAnyToMany(attribute)) { - currentMovingRels.sort((a, b) => b[orderColumnName] - a[orderColumnName]); - for (const currentRel of currentMovingRels) { - if (currentRel[orderColumnName] !== null) { - await this.createQueryBuilder(joinTable.name) - .decrement(orderColumnName, 1) - .where({ - [joinColumn.name]: id, - [orderColumnName]: { $gt: currentRel[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - } - } - } + await cleanOrderColumns({ joinTable, attribute, db, id }); } else { + if (isAnyToOne(attribute)) { + cleanRelationData.set = cleanRelationData.set.slice(-1); + } // overwrite all relations relIdsToaddOrMove = toIds(cleanRelationData.set); await deleteRelations( @@ -907,11 +892,7 @@ const createEntityManager = (db) => { // insert rows const query = this.createQueryBuilder(joinTable.name) .insert(insert) - .onConflict([ - joinColumn.name, - inverseJoinColumn.name, - ...Object.keys(cleanRelationData.set[0].__pivot || {}), - ]); + .onConflict(joinTable.pivotColumns); if (isAnyToMany(attribute)) { query.merge([orderColumnName]); @@ -923,22 +904,24 @@ const createEntityManager = (db) => { } // Delete the previous relations for oneToAny relations - await deletePreviousOneToAnyRelations({ - id, - attribute, - joinTable, - relIdsToadd: relIdsToaddOrMove, - db, - }); + if (!isEmpty(relIdsToaddOrMove)) { + await deletePreviousOneToAnyRelations({ + id, + attribute, + joinTable, + relIdsToadd: relIdsToaddOrMove, + db, + }); - // Delete the previous relations for anyToOne relations - await deletePreviousAnyToOneRelations({ - id, - attribute, - joinTable, - relIdsToadd: relIdsToaddOrMove, - db, - }); + // Delete the previous relations for anyToOne relations + await deletePreviousAnyToOneRelations({ + id, + attribute, + joinTable, + relIdToadd: relIdsToaddOrMove[0], + db, + }); + } } } } diff --git a/packages/core/database/lib/entity-manager/regular-relations.js b/packages/core/database/lib/entity-manager/regular-relations.js index e6255bd6c7..ac11f10449 100644 --- a/packages/core/database/lib/entity-manager/regular-relations.js +++ b/packages/core/database/lib/entity-manager/regular-relations.js @@ -1,5 +1,6 @@ 'use strict'; +const { map, isEmpty } = require('lodash/fp'); const { isBidirectional, isOneToAny, @@ -9,51 +10,11 @@ const { } = require('../metadata/relations'); const { createQueryBuilder } = require('../query'); -const getSelect = (joinTable, attribute) => { - const { joinColumn, orderColumnName, inverseJoinColumn, inverseOrderColumnName } = joinTable; - const select = [joinColumn.name, inverseJoinColumn.name]; - if (isAnyToMany(attribute)) { - select.push(orderColumnName); - } - if (isBidirectional(attribute) && isManyToAny(attribute)) { - select.push(inverseOrderColumnName); - } - return select; -}; - const deletePreviousOneToAnyRelations = async ({ id, attribute, joinTable, relIdsToadd, db }) => { - const { joinColumn, inverseJoinColumn, orderColumnName } = joinTable; - const select = getSelect(joinTable, attribute); + const { joinColumn, inverseJoinColumn } = joinTable; // need to delete the previous relations for oneToAny relations if (isBidirectional(attribute) && isOneToAny(attribute)) { - // update orders for previous oneToAny relations that will be deleted if it has order (oneToMany) - if (isAnyToMany(attribute)) { - const currentRelsToDelete = await createQueryBuilder(joinTable.name, db) - .select(select) - .where({ - [inverseJoinColumn.name]: relIdsToadd, - [joinColumn.name]: { $ne: id }, - }) - .where(joinTable.on || {}) - .execute(); - - currentRelsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]); - - for (const relToDelete of currentRelsToDelete) { - if (relToDelete[orderColumnName] !== null) { - await createQueryBuilder(joinTable.name, db) - .decrement(orderColumnName, 1) - .where({ - [joinColumn.name]: relToDelete[joinColumn.name], - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - } - } - } - // delete previous oneToAny relations await createQueryBuilder(joinTable.name, db) .delete() @@ -63,49 +24,52 @@ const deletePreviousOneToAnyRelations = async ({ id, attribute, joinTable, relId }) .where(joinTable.on || {}) .execute(); + + await cleanOrderColumns({ joinTable, attribute, db, inverseRelIds: relIdsToadd }); } }; -const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relIdsToadd, db }) => { - const { joinColumn, inverseJoinColumn, inverseOrderColumnName } = joinTable; - const select = getSelect(joinTable, attribute); +const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relIdToadd, db }) => { + const { joinColumn, inverseJoinColumn } = joinTable; // Delete the previous relations for anyToOne relations if (isBidirectional(attribute) && isAnyToOne(attribute)) { // update orders for previous anyToOne relations that will be deleted if it has order (manyToOne) if (isManyToAny(attribute)) { - const currentRelsToDelete = await createQueryBuilder(joinTable.name, db) - .select(select) + // if the database integrity was not broken relsToDelete is supposed to be of length 1 + const relsToDelete = await createQueryBuilder(joinTable.name, db) + .select(inverseJoinColumn.name) .where({ [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relIdsToadd }, + [inverseJoinColumn.name]: { $ne: relIdToadd }, }) .where(joinTable.on || {}) .execute(); - for (const relToDelete of currentRelsToDelete) { - if (relToDelete[inverseOrderColumnName] !== null) { - await createQueryBuilder(joinTable.name, db) - .decrement(inverseOrderColumnName, 1) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - } - } - } + const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete); - // delete previous oneToAny relations - await createQueryBuilder(joinTable.name, db) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relIdsToadd }, - }) - .where(joinTable.on || {}) - .execute(); + // delete previous anyToOne relations + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: relIdsToDelete }, + }) + .where(joinTable.on || {}) + .execute(); + + await cleanOrderColumns({ joinTable, attribute, db, inverseRelIds: relIdsToDelete }); + } else { + // delete previous anyToOne relations + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $ne: relIdToadd }, + }) + .where(joinTable.on || {}) + .execute(); + } } }; @@ -114,8 +78,7 @@ const deleteRelations = async ( { id, attribute, joinTable, db }, { relIdsToNotDelete = [], relIdsToDelete = [] } ) => { - const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; - const select = getSelect(joinTable, attribute); + const { joinColumn, inverseJoinColumn } = joinTable; const all = relIdsToDelete === 'all'; if (isAnyToMany(attribute) || (isBidirectional(attribute) && isManyToAny(attribute))) { @@ -123,8 +86,8 @@ const deleteRelations = async ( let done = false; const batchSize = 100; while (!done) { - const relsToDelete = await createQueryBuilder(joinTable.name, db) - .select(select) + const batchToDelete = await createQueryBuilder(joinTable.name, db) + .select(inverseJoinColumn.name) .where({ [joinColumn.name]: id, id: { $gt: lastId }, @@ -135,63 +98,106 @@ const deleteRelations = async ( .orderBy('id') .limit(batchSize) .execute(); - done = relsToDelete.length < batchSize; - lastId = relsToDelete[relsToDelete.length - 1]?.id; + done = batchToDelete.length < batchSize; + lastId = batchToDelete[batchToDelete.length - 1]?.id; - // ORDER UPDATE - if (isAnyToMany(attribute)) { - // sort by order DESC so that the order updates are done in the correct order - // avoiding one to interfere with the others - relsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]); + const batchIds = map(inverseJoinColumn.name, batchToDelete); - for (const relToDelete of relsToDelete) { - if (relToDelete[orderColumnName] !== null) { - await createQueryBuilder(joinTable.name, db) - .decrement(orderColumnName, 1) - .where({ - [joinColumn.name]: id, - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - // manque le pivot ici - .execute(); - } - } - } + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: batchIds }, + }) + .where(joinTable.on || {}) + .execute(); - if (isBidirectional(attribute) && isManyToAny(attribute)) { - const updateInverseOrderPromises = []; - for (const relToDelete of relsToDelete) { - if (relToDelete[inverseOrderColumnName] !== null) { - const updatePromise = createQueryBuilder(joinTable.name, db) - .decrement(inverseOrderColumnName, 1) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - updateInverseOrderPromises.push(updatePromise); - } - } - await Promise.all(updateInverseOrderPromises); - } + await cleanOrderColumns({ joinTable, attribute, db, id, inverseRelIds: batchIds }); } + } else { + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete }, + ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }), + }) + .where(joinTable.on || {}) + .execute(); + } +}; + +/** + * Clean the order columns by ensuring the order value are continuous (ex: 1, 2, 3 and not 1, 5, 10) + * @param {Object} params + * @param {string} params.joinTable - joinTable of the relation where the clean will be done + * @param {string} params.attribute - attribute on which the clean will be done + * @param {string} params.db - Database instance + * @param {string} params.id - Entity ID for which the clean will be done + * @param {string} params.inverseRelIds - Entity ids of the inverse side for which the clean will be done + */ +const cleanOrderColumns = async ({ joinTable, attribute, db, id, inverseRelIds }) => { + if ( + !(isAnyToMany(attribute) && id) && + !(isBidirectional(attribute) && isManyToAny(attribute) && !isEmpty(inverseRelIds)) + ) { + return; } - await createQueryBuilder(joinTable.name, db) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete }, - ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }), - }) - .where(joinTable.on || {}) - .execute(); + const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; + const knex = db.getConnection(); + const update = {}; + const subQuery = knex(joinTable.name).select('id'); + + if (isAnyToMany(attribute) && id) { + update[orderColumnName] = knex.raw('t.src_order'); + const on = [joinColumn.name, ...Object.keys(joinTable.on)]; + subQuery + .select( + knex.raw(`ROW_NUMBER() OVER (PARTITION BY ${on.join(', ')} ORDER BY ??) AS src_order`, [ + ...on, + orderColumnName, + ]) + ) + .where(joinColumn.name, id); + } + + if (isBidirectional(attribute) && isManyToAny(attribute) && !isEmpty(inverseRelIds)) { + update[inverseOrderColumnName] = knex.raw('t.inv_order'); + const on = [inverseJoinColumn.name, ...Object.keys(joinTable.on)]; + subQuery + .select( + knex.raw(`ROW_NUMBER() OVER (PARTITION BY ${on.join(', ')} ORDER BY ??) AS inv_order`, [ + ...on, + inverseOrderColumnName, + ]) + ) + .orWhereIn(inverseJoinColumn.name, inverseRelIds); + } + + await knex(joinTable.name) + .update(update) + .from(subQuery) + .where('t.id', knex.raw('??.id', joinTable.name)); + + /* + `UPDATE :joinTable: + SET :orderColumn: = t.order, :inverseOrderColumn: = t.inv_order + FROM ( + SELECT + id, + ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS order, + ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order + FROM :joinTable: + WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds) + ) AS t + WHERE t.id = :joinTable:.id`, + */ }; module.exports = { deletePreviousOneToAnyRelations, deletePreviousAnyToOneRelations, deleteRelations, + cleanOrderColumns, }; diff --git a/packages/core/database/lib/metadata/index.js b/packages/core/database/lib/metadata/index.js index f19a361edd..96f6103c73 100644 --- a/packages/core/database/lib/metadata/index.js +++ b/packages/core/database/lib/metadata/index.js @@ -123,7 +123,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => { type: 'integer', column: { unsigned: true, - defaultTo: 0, + defaultTo: null, }, }, }, @@ -188,6 +188,7 @@ const createDynamicZone = (attributeName, attribute, meta) => { orderBy: { order: 'asc', }, + pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'], }, }); }; @@ -210,10 +211,11 @@ const createComponent = (attributeName, attribute, meta) => { on: { field: attributeName, }, + orderColumnName: 'order', orderBy: { order: 'asc', }, - ...(attribute.repeatable === true ? { orderColumnName: 'order' } : {}), + pivotColumns: ['entity_id', 'component_id', 'field'], }, }); }; diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index 07707e0935..3d2400f20b 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -272,6 +272,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => { orderBy: { order: 'asc', }, + pivotColumns: [joinColumnName, typeColumnName, idColumnName], }; attribute.joinTable = joinTable; @@ -478,6 +479,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: inverseJoinColumnName, referencedColumn: 'id', }, + pivotColumns: [joinColumnName, inverseJoinColumnName], }; // order @@ -532,6 +534,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: joinTableName, joinColumn: joinTable.inverseJoinColumn, inverseJoinColumn: joinTable.joinColumn, + pivotColumns: joinTable.pivotColumns, }; if (isManyToAny(attribute)) {