diff --git a/packages/core/database/lib/entity-manager/__tests__/relations-orderer.test.js b/packages/core/database/lib/entity-manager/__tests__/relations-orderer.test.js index ca892025b3..df915b1d62 100644 --- a/packages/core/database/lib/entity-manager/__tests__/relations-orderer.test.js +++ b/packages/core/database/lib/entity-manager/__tests__/relations-orderer.test.js @@ -1,6 +1,6 @@ 'use strict'; -const relationsOrderer = require('../relations-orderer'); +const { relationsOrderer } = require('../relations-orderer'); describe('relations orderer', () => { test('connect at the end', () => { @@ -82,4 +82,22 @@ describe('relations orderer', () => { { id: 3, order: 0.5 }, ]); }); + + test('connect with disordered relations', () => { + const orderer = relationsOrderer([], 'id', 'order'); + + orderer.connect([ + { id: 5, position: { before: 1 } }, + { id: 1, position: { before: 2 } }, + { id: 2, position: { end: true } }, + { id: 3, position: { after: 1 } }, + ]); + + expect(orderer.get()).toMatchObject([ + { id: 5, order: 0.5 }, + { id: 1, order: 0.5 }, + { id: 3, order: 0.5 }, + { id: 2, order: 0.5 }, + ]); + }); }); diff --git a/packages/core/database/lib/entity-manager/__tests__/sort-connect-array.test.js b/packages/core/database/lib/entity-manager/__tests__/sort-connect-array.test.js new file mode 100644 index 0000000000..8f041f9bdd --- /dev/null +++ b/packages/core/database/lib/entity-manager/__tests__/sort-connect-array.test.js @@ -0,0 +1,40 @@ +'use strict'; + +const { sortConnectArray } = require('../relations-orderer'); + +describe('sortConnectArray', () => { + test('sorts connect array', () => { + const sortConnect = sortConnectArray([ + { id: 5, position: { before: 1 } }, + { id: 1, position: { before: 2 } }, + { id: 2, position: { end: true } }, + { id: 3, position: { after: 1 } }, + ]); + + expect(sortConnect).toMatchObject([ + { id: 2, position: { end: true } }, + { id: 1, position: { before: 2 } }, + { id: 5, position: { before: 1 } }, + { id: 3, position: { after: 1 } }, + ]); + }); + + test('sorts connect array with initial relations', () => { + const sortConnect = sortConnectArray( + [ + { id: 5, position: { before: 1 } }, + { id: 1, position: { before: 2 } }, + { id: 2, position: { end: true } }, + { id: 3, position: { after: 1 } }, + ], + [{ id: 1 }] + ); + + expect(sortConnect).toMatchObject([ + { id: 5, position: { before: 1 } }, + { id: 2, position: { end: true } }, + { id: 1, position: { before: 2 } }, + { id: 3, position: { after: 1 } }, + ]); + }); +}); diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 419d24f82c..4d900f5985 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -39,7 +39,7 @@ const { deleteRelations, cleanOrderColumns, } = require('./regular-relations'); -const relationsOrderer = require('./relations-orderer'); +const { relationsOrderer } = require('./relations-orderer'); const toId = (value) => value.id || value; const toIds = (value) => castArray(value || []).map(toId); diff --git a/packages/core/database/lib/entity-manager/relations-orderer.js b/packages/core/database/lib/entity-manager/relations-orderer.js index 47507cc223..1f37ffe6f0 100644 --- a/packages/core/database/lib/entity-manager/relations-orderer.js +++ b/packages/core/database/lib/entity-manager/relations-orderer.js @@ -2,6 +2,73 @@ const _ = require('lodash/fp'); +/** + * When connecting relations, the order you connect them matters. + * + * Example, if you connect the following relations: + * { id: 5, position: { before: 1 } } + * { id: 1, position: { before: 2 } } + * { id: 2, position: { end: true } } + * + * Going through the connect array, id 5 has to be connected before id 1, + * so the order of id5 = id1 - 1. But the order value of id 1 is unknown. + * The only way to know the order of id 1 is to connect it first. + * + * This function makes sure the relations are connected in the right order: + * { id: 2, position: { end: true } } + * { id: 1, position: { before: 2 } } + * { id: 5, position: { before: 1 } } + * + */ +const sortConnectArray = (connectArr, initialArr = []) => { + const sortedConnect = []; + // Map to validate if relation is already in sortedConnect or DB. + const relInArray = initialArr.reduce((acc, rel) => ({ ...acc, [rel.id]: true }), {}); + // Map to validate if connect relation has already been computed + const computedIdx = {}; + // Map to store the first index where a relation id is connected + const firstSeen = {}; + + // Populate firstSeen + connectArr.forEach((rel, idx) => { + if (!(rel.id in firstSeen)) firstSeen[rel.id] = idx; + }); + + // Iterate over connectArr and populate sortedConnect + connectArr.forEach((rel, idx) => { + const pushRelation = (rel) => { + sortedConnect.push(rel); + relInArray[rel.id] = true; + }; + + const computeRelation = (rel) => { + const adjacentRelId = rel.position?.before || rel.position?.after; + + // This connect has already been computed + if (idx in computedIdx) return; + + if (!adjacentRelId || relInArray[adjacentRelId]) { + return pushRelation(rel); + } + + // Look if id is referenced elsewhere in the array + const adjacentRelIdx = firstSeen[adjacentRelId]; + if (adjacentRelIdx) { + const adjacentRel = connectArr[adjacentRelIdx]; + // Mark adjacent relation idx as computed, + // so it is not computed again later in the loop + computedIdx[adjacentRelIdx] = true; + computeRelation(adjacentRel); + pushRelation(rel); + } + }; + + computeRelation(rel); + }); + + return sortedConnect; +}; + /** * Responsible for calculating the relations order when connecting them. * @@ -41,7 +108,6 @@ const relationsOrderer = (initArr, idColumn, orderColumn) => { const maxOrder = _.maxBy('order', arr)?.order || 0; - // TODO: Improve performance by using a map const findRelation = (id) => { const idx = arr.findIndex((r) => r.id === id); return { idx, relation: arr[idx] }; @@ -87,7 +153,8 @@ const relationsOrderer = (initArr, idColumn, orderColumn) => { return this; }, connect(relations) { - _.castArray(relations).forEach((relation) => { + const sortedRelations = sortConnectArray(relations, arr); + sortedRelations.forEach((relation) => { this.disconnect(relation); try { @@ -123,4 +190,4 @@ const relationsOrderer = (initArr, idColumn, orderColumn) => { }; }; -module.exports = relationsOrderer; +module.exports = { relationsOrderer, sortConnectArray };