implement cleanOrder function

This commit is contained in:
Pierre Noël 2022-09-26 16:22:22 +02:00
parent 9830910312
commit a56879e12b
4 changed files with 157 additions and 163 deletions

View File

@ -29,6 +29,7 @@ const {
deletePreviousOneToAnyRelations, deletePreviousOneToAnyRelations,
deletePreviousAnyToOneRelations, deletePreviousAnyToOneRelations,
deleteRelations, deleteRelations,
cleanOrderColumns,
} = require('./regular-relations'); } = require('./regular-relations');
const toId = (value) => value.id || value; const toId = (value) => value.id || value;
@ -731,7 +732,6 @@ const createEntityManager = (db) => {
if (isPartialUpdate) { if (isPartialUpdate) {
if (isAnyToOne(attribute)) { if (isAnyToOne(attribute)) {
cleanRelationData.connect = cleanRelationData.connect.slice(-1); cleanRelationData.connect = cleanRelationData.connect.slice(-1);
cleanRelationData.disconnect = [];
} }
relIdsToaddOrMove = toIds(cleanRelationData.connect); relIdsToaddOrMove = toIds(cleanRelationData.connect);
const relIdsToDelete = toIds( const relIdsToDelete = toIds(
@ -813,11 +813,7 @@ const createEntityManager = (db) => {
// insert rows // insert rows
const query = this.createQueryBuilder(joinTable.name) const query = this.createQueryBuilder(joinTable.name)
.insert(insert) .insert(insert)
.onConflict([ .onConflict(joinTable.pivotColumns);
joinColumn.name,
inverseJoinColumn.name,
...Object.keys(cleanRelationData.connect[0].__pivot || {}),
]);
if (isAnyToMany(attribute)) { if (isAnyToMany(attribute)) {
query.merge([orderColumnName]); query.merge([orderColumnName]);
@ -828,22 +824,11 @@ const createEntityManager = (db) => {
await query.execute(); await query.execute();
// remove gap between orders // remove gap between orders
if (isAnyToMany(attribute)) { await cleanOrderColumns({ joinTable, attribute, db, id });
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();
}
}
}
} else { } else {
if (isAnyToOne(attribute)) {
cleanRelationData.set = cleanRelationData.set.slice(-1);
}
// overwrite all relations // overwrite all relations
relIdsToaddOrMove = toIds(cleanRelationData.set); relIdsToaddOrMove = toIds(cleanRelationData.set);
await deleteRelations( await deleteRelations(
@ -907,11 +892,7 @@ const createEntityManager = (db) => {
// insert rows // insert rows
const query = this.createQueryBuilder(joinTable.name) const query = this.createQueryBuilder(joinTable.name)
.insert(insert) .insert(insert)
.onConflict([ .onConflict(joinTable.pivotColumns);
joinColumn.name,
inverseJoinColumn.name,
...Object.keys(cleanRelationData.set[0].__pivot || {}),
]);
if (isAnyToMany(attribute)) { if (isAnyToMany(attribute)) {
query.merge([orderColumnName]); query.merge([orderColumnName]);
@ -923,6 +904,7 @@ const createEntityManager = (db) => {
} }
// Delete the previous relations for oneToAny relations // Delete the previous relations for oneToAny relations
if (!isEmpty(relIdsToaddOrMove)) {
await deletePreviousOneToAnyRelations({ await deletePreviousOneToAnyRelations({
id, id,
attribute, attribute,
@ -936,12 +918,13 @@ const createEntityManager = (db) => {
id, id,
attribute, attribute,
joinTable, joinTable,
relIdsToadd: relIdsToaddOrMove, relIdToadd: relIdsToaddOrMove[0],
db, db,
}); });
} }
} }
} }
}
}, },
/** /**

View File

@ -1,5 +1,6 @@
'use strict'; 'use strict';
const { map, isEmpty } = require('lodash/fp');
const { const {
isBidirectional, isBidirectional,
isOneToAny, isOneToAny,
@ -9,51 +10,11 @@ const {
} = require('../metadata/relations'); } = require('../metadata/relations');
const { createQueryBuilder } = require('../query'); 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 deletePreviousOneToAnyRelations = async ({ id, attribute, joinTable, relIdsToadd, db }) => {
const { joinColumn, inverseJoinColumn, orderColumnName } = joinTable; const { joinColumn, inverseJoinColumn } = joinTable;
const select = getSelect(joinTable, attribute);
// need to delete the previous relations for oneToAny relations // need to delete the previous relations for oneToAny relations
if (isBidirectional(attribute) && isOneToAny(attribute)) { 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 // delete previous oneToAny relations
await createQueryBuilder(joinTable.name, db) await createQueryBuilder(joinTable.name, db)
.delete() .delete()
@ -63,49 +24,52 @@ const deletePreviousOneToAnyRelations = async ({ id, attribute, joinTable, relId
}) })
.where(joinTable.on || {}) .where(joinTable.on || {})
.execute(); .execute();
await cleanOrderColumns({ joinTable, attribute, db, inverseRelIds: relIdsToadd });
} }
}; };
const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relIdsToadd, db }) => { const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relIdToadd, db }) => {
const { joinColumn, inverseJoinColumn, inverseOrderColumnName } = joinTable; const { joinColumn, inverseJoinColumn } = joinTable;
const select = getSelect(joinTable, attribute);
// Delete the previous relations for anyToOne relations // Delete the previous relations for anyToOne relations
if (isBidirectional(attribute) && isAnyToOne(attribute)) { if (isBidirectional(attribute) && isAnyToOne(attribute)) {
// update orders for previous anyToOne relations that will be deleted if it has order (manyToOne) // update orders for previous anyToOne relations that will be deleted if it has order (manyToOne)
if (isManyToAny(attribute)) { if (isManyToAny(attribute)) {
const currentRelsToDelete = await createQueryBuilder(joinTable.name, db) // if the database integrity was not broken relsToDelete is supposed to be of length 1
.select(select) const relsToDelete = await createQueryBuilder(joinTable.name, db)
.select(inverseJoinColumn.name)
.where({ .where({
[joinColumn.name]: id, [joinColumn.name]: id,
[inverseJoinColumn.name]: { $notIn: relIdsToadd }, [inverseJoinColumn.name]: { $ne: relIdToadd },
}) })
.where(joinTable.on || {}) .where(joinTable.on || {})
.execute(); .execute();
for (const relToDelete of currentRelsToDelete) { const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete);
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();
}
}
}
// delete previous oneToAny relations // delete previous anyToOne relations
await createQueryBuilder(joinTable.name, db) await createQueryBuilder(joinTable.name, db)
.delete() .delete()
.where({ .where({
[joinColumn.name]: id, [joinColumn.name]: id,
[inverseJoinColumn.name]: { $notIn: relIdsToadd }, [inverseJoinColumn.name]: { $in: relIdsToDelete },
}) })
.where(joinTable.on || {}) .where(joinTable.on || {})
.execute(); .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 }, { id, attribute, joinTable, db },
{ relIdsToNotDelete = [], relIdsToDelete = [] } { relIdsToNotDelete = [], relIdsToDelete = [] }
) => { ) => {
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; const { joinColumn, inverseJoinColumn } = joinTable;
const select = getSelect(joinTable, attribute);
const all = relIdsToDelete === 'all'; const all = relIdsToDelete === 'all';
if (isAnyToMany(attribute) || (isBidirectional(attribute) && isManyToAny(attribute))) { if (isAnyToMany(attribute) || (isBidirectional(attribute) && isManyToAny(attribute))) {
@ -123,8 +86,8 @@ const deleteRelations = async (
let done = false; let done = false;
const batchSize = 100; const batchSize = 100;
while (!done) { while (!done) {
const relsToDelete = await createQueryBuilder(joinTable.name, db) const batchToDelete = await createQueryBuilder(joinTable.name, db)
.select(select) .select(inverseJoinColumn.name)
.where({ .where({
[joinColumn.name]: id, [joinColumn.name]: id,
id: { $gt: lastId }, id: { $gt: lastId },
@ -135,50 +98,23 @@ const deleteRelations = async (
.orderBy('id') .orderBy('id')
.limit(batchSize) .limit(batchSize)
.execute(); .execute();
done = relsToDelete.length < batchSize; done = batchToDelete.length < batchSize;
lastId = relsToDelete[relsToDelete.length - 1]?.id; lastId = batchToDelete[batchToDelete.length - 1]?.id;
// ORDER UPDATE const batchIds = map(inverseJoinColumn.name, batchToDelete);
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]);
for (const relToDelete of relsToDelete) {
if (relToDelete[orderColumnName] !== null) {
await createQueryBuilder(joinTable.name, db) await createQueryBuilder(joinTable.name, db)
.decrement(orderColumnName, 1) .delete()
.where({ .where({
[joinColumn.name]: id, [joinColumn.name]: id,
[orderColumnName]: { $gt: relToDelete[orderColumnName] }, [inverseJoinColumn.name]: { $in: batchIds },
})
.where(joinTable.on || {})
// manque le pivot ici
.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 || {}) .where(joinTable.on || {})
.execute(); .execute();
updateInverseOrderPromises.push(updatePromise);
}
}
await Promise.all(updateInverseOrderPromises);
}
}
}
await cleanOrderColumns({ joinTable, attribute, db, id, inverseRelIds: batchIds });
}
} else {
await createQueryBuilder(joinTable.name, db) await createQueryBuilder(joinTable.name, db)
.delete() .delete()
.where({ .where({
@ -188,10 +124,80 @@ const deleteRelations = async (
}) })
.where(joinTable.on || {}) .where(joinTable.on || {})
.execute(); .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;
}
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 = { module.exports = {
deletePreviousOneToAnyRelations, deletePreviousOneToAnyRelations,
deletePreviousAnyToOneRelations, deletePreviousAnyToOneRelations,
deleteRelations, deleteRelations,
cleanOrderColumns,
}; };

View File

@ -123,7 +123,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
type: 'integer', type: 'integer',
column: { column: {
unsigned: true, unsigned: true,
defaultTo: 0, defaultTo: null,
}, },
}, },
}, },
@ -188,6 +188,7 @@ const createDynamicZone = (attributeName, attribute, meta) => {
orderBy: { orderBy: {
order: 'asc', order: 'asc',
}, },
pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
}, },
}); });
}; };
@ -210,10 +211,11 @@ const createComponent = (attributeName, attribute, meta) => {
on: { on: {
field: attributeName, field: attributeName,
}, },
orderColumnName: 'order',
orderBy: { orderBy: {
order: 'asc', order: 'asc',
}, },
...(attribute.repeatable === true ? { orderColumnName: 'order' } : {}), pivotColumns: ['entity_id', 'component_id', 'field'],
}, },
}); });
}; };

View File

@ -272,6 +272,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
orderBy: { orderBy: {
order: 'asc', order: 'asc',
}, },
pivotColumns: [joinColumnName, typeColumnName, idColumnName],
}; };
attribute.joinTable = joinTable; attribute.joinTable = joinTable;
@ -478,6 +479,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
name: inverseJoinColumnName, name: inverseJoinColumnName,
referencedColumn: 'id', referencedColumn: 'id',
}, },
pivotColumns: [joinColumnName, inverseJoinColumnName],
}; };
// order // order
@ -532,6 +534,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
name: joinTableName, name: joinTableName,
joinColumn: joinTable.inverseJoinColumn, joinColumn: joinTable.inverseJoinColumn,
inverseJoinColumn: joinTable.joinColumn, inverseJoinColumn: joinTable.joinColumn,
pivotColumns: joinTable.pivotColumns,
}; };
if (isManyToAny(attribute)) { if (isManyToAny(attribute)) {