From e29c069e2a0ae1c4c7a700086951e3f9e4c450aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 6 Sep 2022 15:16:58 +0200 Subject: [PATCH 01/28] add first base for reordering --- .../core/database/lib/entity-manager/index.js | 121 ++++++++++++++---- .../core/database/lib/metadata/relations.js | 7 + 2 files changed, 101 insertions(+), 27 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index bd11129b26..fff41759e9 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -12,6 +12,7 @@ const { isEmpty, isArray, isNull, + map, } = require('lodash/fp'); const types = require('../types'); const { createField } = require('../fields'); @@ -476,12 +477,13 @@ const createEntityManager = (db) => { .execute(); } - const insert = toAssocs(data[attributeName]).map((data) => { + const insert = toAssocs(data[attributeName]).map((data, idx) => { return { [joinColumn.name]: id, [inverseJoinColumn.name]: data.id, ...(joinTable.on || {}), ...(data.__pivot || {}), + order: idx + 1, }; }); @@ -646,6 +648,94 @@ const createEntityManager = (db) => { const { joinTable } = attribute; const { joinColumn, inverseJoinColumn } = joinTable; + const onlyDeleteRelation = isNull(data[attributeName]); + let insert; + + if (!onlyDeleteRelation) { + const cleanRelationData = toAssocs(data[attributeName]); + const isPartialUpdate = cleanRelationData.connect || cleanRelationData.disconnect; + + if (isPartialUpdate) { + // partial update case + insert = await this.createQueryBuilder(joinTable.name).select().orderBy('order'); + + if (isArray(cleanRelationData.connect)) { + for (const connectItem of cleanRelationData.connect) { + const { start, end, after, before } = connectItem; + let wasInserted = false; + const relationToInsert = { + [joinColumn.name]: id, + [inverseJoinColumn.name]: connectItem.id, + }; + + insert = insert.reduce((acc, rel, i, arr) => { + const currRelId = String(rel[inverseJoinColumn.name]); + const nextRelId = String(arr[i + 1]?.[inverseJoinColumn.name]); + if (currRelId === String(connectItem.id)) { + return acc; + } + + if (wasInserted) { + return acc.push(rel); + } + + if (i === 0 && start) { + wasInserted = true; + return acc.push(relationToInsert); + } + + if (i === arr.length - 1 && (end || !wasInserted)) { + wasInserted = true; + return acc.push(relationToInsert); + } + + if (after && currRelId === String(after)) { + wasInserted = true; + return acc.push(relationToInsert); + } + + if (before && nextRelId === String(before)) { + wasInserted = true; + return acc.push(relationToInsert); + } + + return acc.push(rel); + }, []); + } + } + + if (isArray(cleanRelationData.disconnect)) { + const idsToRemove = map('id', cleanRelationData.disconnect); + insert = insert.filter((rel) => !idsToRemove.includes(rel[inverseJoinColumn.name])); + } + + insert.forEach((rel, i) => { + rel.order = i; + }); + } else { + insert = toAssocs(data[attributeName]).map((data, idx) => { + return { + [joinColumn.name]: id, + [inverseJoinColumn.name]: data.id, + ...(joinTable.on || {}), + ...(data.__pivot || {}), + order: idx + 1, + }; + }); + } + + if ( + isBidirectional(attribute) && + ['oneToOne', 'oneToMany'].includes(attribute.relation) + ) { + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) }) + .where(joinTable.on || {}) + .execute(); + } + } + // clear previous associations in the joinTable await this.createQueryBuilder(joinTable.name) .delete() @@ -653,33 +743,10 @@ const createEntityManager = (db) => { .where(joinTable.on || {}) .execute(); - if ( - isBidirectional(attribute) && - ['oneToOne', 'oneToMany'].includes(attribute.relation) - ) { - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) }) - .where(joinTable.on || {}) - .execute(); - } - - if (!isNull(data[attributeName])) { - const insert = toAssocs(data[attributeName]).map((data) => { - return { - [joinColumn.name]: id, - [inverseJoinColumn.name]: data.id, - ...(joinTable.on || {}), - ...(data.__pivot || {}), - }; - }); - - // if there is nothing to insert - if (insert.length === 0) { - continue; + if (!onlyDeleteRelation) { + if (!isEmpty(insert)) { + await this.createQueryBuilder(joinTable.name).insert(insert).execute(); } - - await this.createQueryBuilder(joinTable.name).insert(insert).execute(); } } } diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index bdef527461..2dfcb0e5ff 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -422,6 +422,13 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { unsigned: true, }, }, + order: { + type: 'integer', + column: { + unsigned: true, + defaultTo: 0, + }, + }, // TODO: add extra pivot attributes -> user should use an intermediate entity }, indexes: [ From b7a1e312b4016043be0e05e26357e13edfa4d467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 6 Sep 2022 17:54:42 +0200 Subject: [PATCH 02/28] implement simple partial update --- .../core/database/lib/entity-manager/index.js | 160 ++++++++++-------- 1 file changed, 90 insertions(+), 70 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index fff41759e9..ec457fd05f 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -13,6 +13,7 @@ const { isArray, isNull, map, + uniqBy, } = require('lodash/fp'); const types = require('../types'); const { createField } = require('../fields'); @@ -26,6 +27,13 @@ const toIds = (value) => castArray(value || []).map(toId); const isValidId = (value) => isString(value) || isInteger(value); const toAssocs = (data) => { + if (data?.connect || data?.disconnect) { + return { + connect: toAssocs(data.connect), + disconnect: toAssocs(data.disconnect), + }; + } + return castArray(data) .filter((datum) => !isNil(datum)) .map((datum) => { @@ -651,68 +659,82 @@ const createEntityManager = (db) => { const onlyDeleteRelation = isNull(data[attributeName]); let insert; - if (!onlyDeleteRelation) { + if (onlyDeleteRelation) { + // clear previous associations in the joinTable + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ [joinColumn.name]: id }) + .where(joinTable.on || {}) + .execute(); + } else { const cleanRelationData = toAssocs(data[attributeName]); - const isPartialUpdate = cleanRelationData.connect || cleanRelationData.disconnect; + const connect = cleanRelationData.connect || []; + const disconnect = cleanRelationData.disconnect || []; + const isPartialUpdate = connect.length || disconnect.length; if (isPartialUpdate) { - // partial update case - insert = await this.createQueryBuilder(joinTable.name).select().orderBy('order'); + const idsToRemove = map('id', connect.concat(disconnect)); + const existingRelsToDelete = await this.createQueryBuilder(joinTable.name) + .select([inverseJoinColumn.name, 'order']) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: idsToRemove }, + }) + .where(joinTable.on || {}) + .execute(); - if (isArray(cleanRelationData.connect)) { - for (const connectItem of cleanRelationData.connect) { - const { start, end, after, before } = connectItem; - let wasInserted = false; - const relationToInsert = { + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: idsToRemove }, + }) + .where(joinTable.on || {}) + .execute(); + + for (const relToDelete of existingRelsToDelete) { + await this.createQueryBuilder(joinTable.name) + .update({ order: db.getConnection().raw('?? - 1', 'order') }) + .where({ [joinColumn.name]: id, - [inverseJoinColumn.name]: connectItem.id, - }; - - insert = insert.reduce((acc, rel, i, arr) => { - const currRelId = String(rel[inverseJoinColumn.name]); - const nextRelId = String(arr[i + 1]?.[inverseJoinColumn.name]); - if (currRelId === String(connectItem.id)) { - return acc; - } - - if (wasInserted) { - return acc.push(rel); - } - - if (i === 0 && start) { - wasInserted = true; - return acc.push(relationToInsert); - } - - if (i === arr.length - 1 && (end || !wasInserted)) { - wasInserted = true; - return acc.push(relationToInsert); - } - - if (after && currRelId === String(after)) { - wasInserted = true; - return acc.push(relationToInsert); - } - - if (before && nextRelId === String(before)) { - wasInserted = true; - return acc.push(relationToInsert); - } - - return acc.push(rel); - }, []); - } + order: { $gt: relToDelete.order }, + }) + .where(joinTable.on || {}) + .execute(); } - if (isArray(cleanRelationData.disconnect)) { - const idsToRemove = map('id', cleanRelationData.disconnect); - insert = insert.filter((rel) => !idsToRemove.includes(rel[inverseJoinColumn.name])); + if (connect.length) { + const { max } = await this.createQueryBuilder(joinTable.name) + .max('order') + .where({ [joinColumn.name]: id }) + .where(joinTable.on || {}) + .first() + .execute(); + + insert = uniqBy('id', connect).map((rel, idx) => ({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: rel.id, + order: max + idx + 1, + })); } - insert.forEach((rel, i) => { - rel.order = i; - }); + if ( + isBidirectional(attribute) && + ['oneToOne', 'oneToMany'].includes(attribute.relation) + ) { + // TODO: reordering the relations when oneToMany + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) }) + .where(joinTable.on || {}) + .execute(); + } + + if (!isEmpty(insert)) { + await this.createQueryBuilder(joinTable.name).insert(insert).execute(); + } } else { + // overwrite insert = toAssocs(data[attributeName]).map((data, idx) => { return { [joinColumn.name]: id, @@ -722,30 +744,28 @@ const createEntityManager = (db) => { order: idx + 1, }; }); - } - if ( - isBidirectional(attribute) && - ['oneToOne', 'oneToMany'].includes(attribute.relation) - ) { + if ( + isBidirectional(attribute) && + ['oneToOne', 'oneToMany'].includes(attribute.relation) + ) { + // TODO: reordering the relations when oneToMany + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) }) + .where(joinTable.on || {}) + .execute(); + } + await this.createQueryBuilder(joinTable.name) .delete() - .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) }) + .where({ [joinColumn.name]: id }) .where(joinTable.on || {}) .execute(); - } - } - // clear previous associations in the joinTable - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ [joinColumn.name]: id }) - .where(joinTable.on || {}) - .execute(); - - if (!onlyDeleteRelation) { - if (!isEmpty(insert)) { - await this.createQueryBuilder(joinTable.name).insert(insert).execute(); + if (!isEmpty(insert)) { + await this.createQueryBuilder(joinTable.name).insert(insert).execute(); + } } } } From a1d1879ffb24b26229707c74459f2152cc824dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Wed, 7 Sep 2022 17:29:47 +0200 Subject: [PATCH 03/28] fix create entity with relations in connect --- packages/core/database/lib/entity-manager/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index ec457fd05f..79fa8af92d 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -485,7 +485,10 @@ const createEntityManager = (db) => { .execute(); } - const insert = toAssocs(data[attributeName]).map((data, idx) => { + const assocs = toAssocs(data[attributeName]); + + const relationsToAdd = assocs.connect || assocs; + const insert = relationsToAdd.map((data, idx) => { return { [joinColumn.name]: id, [inverseJoinColumn.name]: data.id, From c4031449fb38bed27b43152e5d9e048c4588b6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Wed, 7 Sep 2022 18:21:54 +0200 Subject: [PATCH 04/28] implement 2 columns + adapt attachRelations --- .../core/database/lib/entity-manager/index.js | 27 ++++++++++-- .../core/database/lib/metadata/relations.js | 41 +++++++++++++++++-- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 79fa8af92d..15618b2126 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -475,26 +475,45 @@ const createEntityManager = (db) => { // need to set the column on the target const { joinTable } = attribute; - const { joinColumn, inverseJoinColumn } = joinTable; + const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = + joinTable; if (isOneToAny(attribute) && isBidirectional(attribute)) { await this.createQueryBuilder(joinTable.name) .delete() - .where({ [inverseJoinColumn.name]: castArray(data[attributeName]) }) + .where({ [inverseJoinColumn.name]: castArray(data[attributeName]) }) // TODO: would break with connect .where(joinTable.on || {}) .execute(); } const assocs = toAssocs(data[attributeName]); - const relationsToAdd = assocs.connect || assocs; + const relationsToAdd = uniqBy('id', assocs.connect || assocs); + + const maxMap = {}; + if (inverseOrderColumnName) { + await Promise.all( + relationsToAdd.map(async (rel) => { + const { max } = await this.createQueryBuilder(joinTable.name) + .max(inverseOrderColumnName) + .where({ [inverseJoinColumn.name]: rel.id }) + .where(joinTable.on || {}) + .first() + .execute(); + + maxMap[rel.id] = max; + }) + ); + } + const insert = relationsToAdd.map((data, idx) => { return { [joinColumn.name]: id, [inverseJoinColumn.name]: data.id, ...(joinTable.on || {}), ...(data.__pivot || {}), - order: idx + 1, + [orderColumnName]: idx + 1, + ...(inverseOrderColumnName ? { [inverseOrderColumnName]: maxMap[data.id] + 1 } : {}), }; }); diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index 2dfcb0e5ff..b3b9896949 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -398,12 +398,14 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { const joinColumnName = _.snakeCase(`${meta.singularName}_id`); let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`); + const orderColumnName = _.snakeCase(`${meta.singularName}_order`); + // if relation is slef referencing if (joinColumnName === inverseJoinColumnName) { inverseJoinColumnName = `inv_${inverseJoinColumnName}`; } - metadata.add({ + const metadataSchema = { uid: joinTableName, tableName: joinTableName, attributes: { @@ -422,7 +424,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { unsigned: true, }, }, - order: { + [orderColumnName]: { type: 'integer', column: { unsigned: true, @@ -440,6 +442,10 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: `${joinTableName}_inv_fk`, columns: [inverseJoinColumnName], }, + { + name: `${joinTableName}_order_fk`, + columns: [orderColumnName], + }, ], foreignKeys: [ { @@ -457,7 +463,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { onDelete: 'CASCADE', }, ], - }); + }; const joinTable = { name: joinTableName, @@ -469,8 +475,35 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: inverseJoinColumnName, referencedColumn: 'id', }, + orderColumnName, }; + if (isBidirectional(attribute)) { + let inverseOrderColumnName = _.snakeCase(`${targetMeta.singularName}_order`); + + // if relation is slef referencing + if (joinColumnName === inverseJoinColumnName) { + inverseOrderColumnName = `inv_${inverseOrderColumnName}`; + } + + metadataSchema.attributes[inverseOrderColumnName] = { + type: 'integer', + column: { + unsigned: true, + defaultTo: 0, + }, + }; + + metadataSchema.indexes.push({ + name: `${joinTableName}_order_inv_fk`, + columns: [inverseOrderColumnName], + }); + + joinTable.inverseOrderColumnName = inverseOrderColumnName; + } + + metadata.add(metadataSchema); + attribute.joinTable = joinTable; if (isBidirectional(attribute)) { @@ -486,6 +519,8 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: joinTableName, joinColumn: joinTable.inverseJoinColumn, inverseJoinColumn: joinTable.joinColumn, + orderColumnName: joinTable.inverseOrderColumnName, + inverseOrderColumnName: joinTable.orderColumnName, }; } }; From 83c29436ca4cabbfe8f96d37d2ad24ca8691197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Wed, 14 Sep 2022 11:30:00 +0200 Subject: [PATCH 05/28] adapt all entity-manager with 2 order columns --- .../server/tests/index.test.e2e.js | 88 --- .../core/database/lib/entity-manager/index.js | 616 ++++++++++++++---- packages/core/database/lib/metadata/index.js | 1 + .../core/database/lib/metadata/relations.js | 65 +- .../lib/services/entity-service/components.js | 14 +- 5 files changed, 545 insertions(+), 239 deletions(-) diff --git a/packages/core/content-manager/server/tests/index.test.e2e.js b/packages/core/content-manager/server/tests/index.test.e2e.js index e832ff7eb4..c74efc87ea 100644 --- a/packages/core/content-manager/server/tests/index.test.e2e.js +++ b/packages/core/content-manager/server/tests/index.test.e2e.js @@ -91,15 +91,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe('tag1'); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -119,15 +115,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe('tag2'); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -147,15 +139,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe('tag3'); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -180,15 +168,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -216,15 +200,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -251,15 +231,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -282,15 +258,11 @@ describe('Relations', () => { expect(body.title).toBe(data.articles[0].title); expect(body.content).toBe(data.articles[0].content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -312,15 +284,11 @@ describe('Relations', () => { expect(body.title).toBe(data.articles[0].title); expect(body.content).toBe(data.articles[0].content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -346,15 +314,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -458,15 +422,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); @@ -502,15 +462,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe('cat1'); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -533,15 +489,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe('cat2'); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -568,15 +520,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -603,15 +551,11 @@ describe('Relations', () => { expect(body.title).toBe(data.articles[0].title); expect(body.content).toBe(data.articles[0].content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); @@ -640,15 +584,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); @@ -671,15 +611,11 @@ describe('Relations', () => { expect(body.title).toBe(data.articles[1].title); expect(body.content).toBe(data.articles[1].content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); @@ -704,15 +640,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe(data.categories[0].name); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); @@ -737,15 +669,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe(entry.name); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); @@ -845,15 +773,11 @@ describe('Relations', () => { expect(body.id).toBeDefined(); expect(body.name).toBe('ref1'); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); }); @@ -876,15 +800,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.publishedAt).toBeUndefined(); @@ -905,15 +825,11 @@ describe('Relations', () => { expect(body.title).toBe(data.articles[0].title); expect(body.content).toBe(data.articles[0].content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); @@ -940,15 +856,11 @@ describe('Relations', () => { expect(body.title).toBe(entry.title); expect(body.content).toBe(entry.content); expect(body.createdBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); expect(body.updatedBy).toMatchObject({ - firstname: 'admin', id: 1, - lastname: 'admin', username: null, }); const reference = await getRelations('article', 'reference', body.id); diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 15618b2126..60b9dc4960 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -12,22 +12,29 @@ const { isEmpty, isArray, isNull, - map, uniqBy, + differenceBy, + groupBy, } = require('lodash/fp'); const types = require('../types'); const { createField } = require('../fields'); const { createQueryBuilder } = require('../query'); const { createRepository } = require('./entity-repository'); -const { isBidirectional, isOneToAny } = require('../metadata/relations'); const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations'); +const { + isBidirectional, + isOneToAny, + isManyToAny, + isAnyToOne, + isAnyToMany, +} = require('../metadata/relations'); const toId = (value) => value.id || value; const toIds = (value) => castArray(value || []).map(toId); const isValidId = (value) => isString(value) || isInteger(value); const toAssocs = (data) => { - if (data?.connect || data?.disconnect) { + if (data?.connect || data?.disconnect || (isPlainObject(data) && !data.id)) { return { connect: toAssocs(data.connect), disconnect: toAssocs(data.disconnect), @@ -477,51 +484,96 @@ const createEntityManager = (db) => { const { joinTable } = attribute; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; + const select = [joinColumn.name]; + if (isAnyToMany(attribute)) { + select.push(orderColumnName); + } - if (isOneToAny(attribute) && isBidirectional(attribute)) { + const cleanRelationData = toAssocs(data[attributeName]); + const relsToAdd = uniqBy('id', cleanRelationData.connect || cleanRelationData); + const relIdsToadd = toIds(relsToAdd); + + // 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 this.createQueryBuilder(joinTable.name) + .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) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: relToDelete[joinColumn.name], + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + } + } + + // delete previous oneToAny relations await this.createQueryBuilder(joinTable.name) .delete() - .where({ [inverseJoinColumn.name]: castArray(data[attributeName]) }) // TODO: would break with connect + .where({ [inverseJoinColumn.name]: relIdsToadd }) .where(joinTable.on || {}) .execute(); } - const assocs = toAssocs(data[attributeName]); - - const relationsToAdd = uniqBy('id', assocs.connect || assocs); - - const maxMap = {}; - if (inverseOrderColumnName) { - await Promise.all( - relationsToAdd.map(async (rel) => { - const { max } = await this.createQueryBuilder(joinTable.name) - .max(inverseOrderColumnName) - .where({ [inverseJoinColumn.name]: rel.id }) - .where(joinTable.on || {}) - .first() - .execute(); - - maxMap[rel.id] = max; - }) - ); - } - - const insert = relationsToAdd.map((data, idx) => { + // prepare new relations to insert + const insert = relsToAdd.map((data) => { return { [joinColumn.name]: id, [inverseJoinColumn.name]: data.id, ...(joinTable.on || {}), ...(data.__pivot || {}), - [orderColumnName]: idx + 1, - ...(inverseOrderColumnName ? { [inverseOrderColumnName]: maxMap[data.id] + 1 } : {}), }; }); - // if there is nothing to insert + // add order value when relevant + if (isAnyToMany(attribute)) { + insert.forEach((rel, idx) => { + rel[orderColumnName] = idx + 1; + }); + } + // add inv_order value when relevant + if (isBidirectional(attribute) && isManyToAny(attribute)) { + const maxMap = {}; + await Promise.all( + relIdsToadd.map(async (relId) => { + const { max } = await this.createQueryBuilder(joinTable.name) + .max(inverseOrderColumnName) + .where({ [inverseJoinColumn.name]: relId }) + .where(joinTable.on || {}) + .first() + .execute(); + + maxMap[relId] = max; + }) + ); + + insert.forEach((rel) => { + rel[inverseOrderColumnName] = maxMap[rel[inverseJoinColumn.name]] + 1; + }); + } + if (insert.length === 0) { continue; } + // insert new relations await this.createQueryBuilder(joinTable.name).insert(insert).execute(); } } @@ -676,13 +728,59 @@ const createEntityManager = (db) => { if (attribute.joinTable) { const { joinTable } = attribute; - const { joinColumn, inverseJoinColumn } = joinTable; + const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = + joinTable; + const select = [joinColumn.name, inverseJoinColumn.name]; + if (isAnyToMany(attribute)) { + select.push(orderColumnName); + } + if (isBidirectional(attribute) && isManyToAny(attribute)) { + select.push(inverseOrderColumnName); + } - const onlyDeleteRelation = isNull(data[attributeName]); - let insert; + // only delete relations + if (isNull(data[attributeName])) { + // INVERSE ORDER UPDATE + if (isBidirectional(attribute) && isManyToAny(attribute)) { + let lastId = 0; + let done = false; + const batchSize = 100; + while (!done) { + const relsToDelete = await this.createQueryBuilder(joinTable.name) + .select(select) + .where({ + [joinColumn.name]: id, + id: { $gt: lastId }, + }) + .where(joinTable.on || {}) + .orderBy('id') + .limit(batchSize) + .execute(); + // TODO: cannot put pivot here... + done = relsToDelete.length < batchSize; + lastId = relsToDelete[relsToDelete.length - 1]?.id; + + const updateInverseOrderPromises = []; + for (const relToDelete of relsToDelete) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } + + await Promise.all(updateInverseOrderPromises); + } + } - if (onlyDeleteRelation) { - // clear previous associations in the joinTable await this.createQueryBuilder(joinTable.name) .delete() .where({ [joinColumn.name]: id }) @@ -690,104 +788,348 @@ const createEntityManager = (db) => { .execute(); } else { const cleanRelationData = toAssocs(data[attributeName]); - const connect = cleanRelationData.connect || []; - const disconnect = cleanRelationData.disconnect || []; - const isPartialUpdate = connect.length || disconnect.length; + const isPartialUpdate = + has('connect', cleanRelationData) || has('disconnect', cleanRelationData); + let relIdsToaddOrMove; if (isPartialUpdate) { - const idsToRemove = map('id', connect.concat(disconnect)); - const existingRelsToDelete = await this.createQueryBuilder(joinTable.name) - .select([inverseJoinColumn.name, 'order']) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $in: idsToRemove }, - }) - .where(joinTable.on || {}) - .execute(); + // does not support pivot + let connect = uniqBy('id', cleanRelationData.connect || []); + let disconnect = uniqBy('id', cleanRelationData.disconnect || []); + if (isAnyToOne(attribute)) { + connect = connect.slice(-1); + disconnect = []; + } + relIdsToaddOrMove = toIds(connect); + // DELETE relations in disconnect + const relIdsToDelete = toIds(differenceBy('id', disconnect, connect)); - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $in: idsToRemove }, - }) - .where(joinTable.on || {}) - .execute(); - - for (const relToDelete of existingRelsToDelete) { - await this.createQueryBuilder(joinTable.name) - .update({ order: db.getConnection().raw('?? - 1', 'order') }) + // UPDATE RELEVANT ORDERS + if ( + isAnyToMany(attribute) || + (isBidirectional(attribute) && isManyToAny(attribute)) + ) { + const relsToDelete = await this.createQueryBuilder(joinTable.name) + .select(select) .where({ [joinColumn.name]: id, - order: { $gt: relToDelete.order }, + [inverseJoinColumn.name]: { $in: relIdsToDelete }, }) .where(joinTable.on || {}) .execute(); + + // 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]); + + for (const relToDelete of relsToDelete) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: id, + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + } + } + + // INVERSE ORDER UPDATE + if (isBidirectional(attribute) && isManyToAny(attribute)) { + const updateInverseOrderPromises = []; + for (const relToDelete of relsToDelete) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } + + await Promise.all(updateInverseOrderPromises); + } } - if (connect.length) { - const { max } = await this.createQueryBuilder(joinTable.name) - .max('order') - .where({ [joinColumn.name]: id }) - .where(joinTable.on || {}) - .first() - .execute(); - - insert = uniqBy('id', connect).map((rel, idx) => ({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: rel.id, - order: max + idx + 1, - })); - } - - if ( - isBidirectional(attribute) && - ['oneToOne', 'oneToMany'].includes(attribute.relation) - ) { - // TODO: reordering the relations when oneToMany - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) }) - .where(joinTable.on || {}) - .execute(); - } - - if (!isEmpty(insert)) { - await this.createQueryBuilder(joinTable.name).insert(insert).execute(); - } - } else { - // overwrite - insert = toAssocs(data[attributeName]).map((data, idx) => { - return { - [joinColumn.name]: id, - [inverseJoinColumn.name]: data.id, - ...(joinTable.on || {}), - ...(data.__pivot || {}), - order: idx + 1, - }; - }); - - if ( - isBidirectional(attribute) && - ['oneToOne', 'oneToMany'].includes(attribute.relation) - ) { - // TODO: reordering the relations when oneToMany - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) }) - .where(joinTable.on || {}) - .execute(); - } - + // DELETE await this.createQueryBuilder(joinTable.name) .delete() - .where({ [joinColumn.name]: id }) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: relIdsToDelete }, + }) .where(joinTable.on || {}) .execute(); - if (!isEmpty(insert)) { - await this.createQueryBuilder(joinTable.name).insert(insert).execute(); + // add/move + let max; + const currentMovingRels = await this.createQueryBuilder(joinTable.name) + .select(select) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, + }) + .where(joinTable.on || {}) + .execute(); + const currentMovingRelsMap = groupBy(inverseJoinColumn.name, currentMovingRels); + + if (isAnyToMany(attribute)) { + max = ( + await this.createQueryBuilder(joinTable.name) + .max(orderColumnName) + .where({ [joinColumn.name]: id }) + .where(joinTable.on || {}) + .first() + .execute() + ).max; } + + for (const relToAddOrMove of connect) { + // const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0]; + const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0]; + if (currentRel && isAnyToMany(attribute)) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', currentRel[orderColumnName]), + }) + .where({ + [joinColumn.name]: id, + [orderColumnName]: { $gt: currentRel[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + + currentMovingRels.forEach((rel) => { + if (rel[orderColumnName] > currentRel[orderColumnName]) { + rel[orderColumnName] -= 1; + } + }); + + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: max, + }) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: relToAddOrMove.id, + }) + .where(joinTable.on || {}) + .execute(); + } else if (!currentRel) { + const insert = { + [joinColumn.name]: id, + [inverseJoinColumn.name]: relToAddOrMove.id, + ...(relToAddOrMove.__pivot || {}), + }; + + if (isAnyToMany(attribute)) { + insert[orderColumnName] = max + 1; + } + + if (isBidirectional(attribute) && isManyToAny(attribute)) { + const { max: reverseMax } = await this.createQueryBuilder(joinTable.name) + .max(inverseOrderColumnName) + .where({ [inverseJoinColumn.name]: relToAddOrMove.id }) + .where(joinTable.on || {}) + .first() + .execute(); + + insert[inverseOrderColumnName] = reverseMax + 1; + } + + await this.createQueryBuilder(joinTable.name).insert(insert).execute(); + max += 1; + } + } + } else { + // overwrite all relations + const relsToAdd = uniqBy('id', cleanRelationData); + relIdsToaddOrMove = toIds(relsToAdd); + + // UPDATE RELEVANT ORDERS BEFORE DELETE + if (isAnyToMany(attribute) || isManyToAny(attribute)) { + let lastId = 0; + let done = false; + const batchSize = 100; + while (!done) { + const relsToDelete = await this.createQueryBuilder(joinTable.name) + .select(select) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, + id: { $gt: lastId }, + }) + .where(joinTable.on || {}) + .orderBy('id') + .limit(batchSize) + .execute(); + + done = relsToDelete.length < batchSize; + lastId = relsToDelete[relsToDelete.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]); + + for (const relToDelete of relsToDelete) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: id, + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + // manque le pivot ici + .execute(); + } + } + + // INVERSE ORDER UPDATE + if (isBidirectional(attribute) && isManyToAny(attribute)) { + const updateInverseOrderPromises = []; + for (const relToDelete of relsToDelete) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } + + await Promise.all(updateInverseOrderPromises); + } + } + } + + // DELETE + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, + }) + .where(joinTable.on || {}) + .execute(); + + const currentMovingRels = await this.createQueryBuilder(joinTable.name) + .select(select) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, + }) + .where(joinTable.on || {}) + .execute(); + const currentMovingRelsMap = groupBy(inverseJoinColumn.name, currentMovingRels); + + let max = 0; + for (const relToAdd of relsToAdd) { + const currentRel = currentMovingRelsMap[relToAdd.id]?.[0]; + + if (currentRel && isAnyToMany(attribute)) { + const update = { [orderColumnName]: max + 1 }; + await this.createQueryBuilder(joinTable.name) + .update(update) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: relToAdd.id, + }) + .where(joinTable.on || {}) + .execute(); + } else if (!currentRel) { + const insert = { + [joinColumn.name]: id, + [inverseJoinColumn.name]: relToAdd.id, + ...(relToAdd.__pivot || {}), + }; + + if (isAnyToMany(attribute)) { + insert[orderColumnName] = max + 1; + } + // can be optimized in one query + if (isBidirectional(attribute) && isManyToAny(attribute)) { + const { max: reverseMax } = await this.createQueryBuilder(joinTable.name) + .max(inverseOrderColumnName) + .where({ [inverseJoinColumn.name]: id }) + .where(joinTable.on || {}) + .first() + .execute(); + + insert[inverseOrderColumnName] = reverseMax + 1; + } + + await this.createQueryBuilder(joinTable.name).insert(insert).execute(); + } + max += 1; + } + } + + // 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 this.createQueryBuilder(joinTable.name) + .select(select) + .where({ + [inverseJoinColumn.name]: relIdsToaddOrMove, + [joinColumn.name]: { $ne: id }, + }) + .where(joinTable.on || {}) + .execute(); + + currentRelsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]); + + for (const relToDelete of currentRelsToDelete) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: relToDelete[joinColumn.name], + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + } + } + + // delete previous oneToAny relations + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ + [inverseJoinColumn.name]: relIdsToaddOrMove, + [joinColumn.name]: { $ne: id }, + }) + .where(joinTable.on || {}) + .execute(); } } } @@ -905,7 +1247,47 @@ const createEntityManager = (db) => { if (attribute.joinTable) { const { joinTable } = attribute; - const { joinColumn } = joinTable; + const { joinColumn, inverseJoinColumn, inverseOrderColumnName } = joinTable; + + // INVERSE ORDER UPDATE + if (isBidirectional(attribute) && isManyToAny(attribute)) { + let lastId = 0; + let done = false; + const batchSize = 100; + while (!done) { + const relsToDelete = await this.createQueryBuilder(joinTable.name) + .select(inverseJoinColumn.name, inverseOrderColumnName) + .where({ + [joinColumn.name]: id, + id: { $gt: lastId }, + }) + .where(joinTable.on || {}) + .orderBy('id') + .limit(batchSize) + .execute(); + done = relsToDelete.length < batchSize; + lastId = relsToDelete[relsToDelete.length - 1]?.id; + + const updateInverseOrderPromises = []; + for (const relToDelete of relsToDelete) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } + + await Promise.all(updateInverseOrderPromises); + } + } await this.createQueryBuilder(joinTable.name) .delete() diff --git a/packages/core/database/lib/metadata/index.js b/packages/core/database/lib/metadata/index.js index c2309fa073..ee67afd09f 100644 --- a/packages/core/database/lib/metadata/index.js +++ b/packages/core/database/lib/metadata/index.js @@ -208,6 +208,7 @@ const createComponent = (attributeName, attribute, meta) => { orderBy: { order: 'asc', }, + ...(attribute.repeatable === true ? { orderColumnName: 'order' } : {}), }, }); }; diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index b3b9896949..988e581c81 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -10,6 +10,9 @@ const hasInversedBy = _.has('inversedBy'); const hasMappedBy = _.has('mappedBy'); const isOneToAny = (attribute) => ['oneToOne', 'oneToMany'].includes(attribute.relation); +const isManyToAny = (attribute) => ['manyToMany', 'manyToOne'].includes(attribute.relation); +const isAnyToOne = (attribute) => ['oneToOne', 'manyToOne'].includes(attribute.relation); +const isAnyToMany = (attribute) => ['oneToMany', 'manyToMany'].includes(attribute.relation); const isBidirectional = (attribute) => hasInversedBy(attribute) || hasMappedBy(attribute); const isOwner = (attribute) => !isBidirectional(attribute) || hasInversedBy(attribute); const shouldUseJoinTable = (attribute) => attribute.useJoinTable !== false; @@ -398,13 +401,19 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { const joinColumnName = _.snakeCase(`${meta.singularName}_id`); let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`); - const orderColumnName = _.snakeCase(`${meta.singularName}_order`); - - // if relation is slef referencing + // if relation is self referencing if (joinColumnName === inverseJoinColumnName) { inverseJoinColumnName = `inv_${inverseJoinColumnName}`; } + const orderColumnName = _.snakeCase(`${meta.singularName}_order`); + let inverseOrderColumnName = _.snakeCase(`${targetMeta.singularName}_order`); + + // if relation is self referencing + if (attribute.relation === 'manyToMany' && joinColumnName === inverseJoinColumnName) { + inverseOrderColumnName = `inv_${inverseOrderColumnName}`; + } + const metadataSchema = { uid: joinTableName, tableName: joinTableName, @@ -424,13 +433,6 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { unsigned: true, }, }, - [orderColumnName]: { - type: 'integer', - column: { - unsigned: true, - defaultTo: 0, - }, - }, // TODO: add extra pivot attributes -> user should use an intermediate entity }, indexes: [ @@ -442,10 +444,6 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: `${joinTableName}_inv_fk`, columns: [inverseJoinColumnName], }, - { - name: `${joinTableName}_order_fk`, - columns: [orderColumnName], - }, ], foreignKeys: [ { @@ -475,17 +473,27 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: inverseJoinColumnName, referencedColumn: 'id', }, - orderColumnName, }; - if (isBidirectional(attribute)) { - let inverseOrderColumnName = _.snakeCase(`${targetMeta.singularName}_order`); - - // if relation is slef referencing - if (joinColumnName === inverseJoinColumnName) { - inverseOrderColumnName = `inv_${inverseOrderColumnName}`; - } + // order + if (isAnyToMany(attribute)) { + metadataSchema.attributes[orderColumnName] = { + type: 'integer', + column: { + unsigned: true, + defaultTo: 0, + }, + }; + metadataSchema.indexes.push({ + name: `${joinTableName}_order_fk`, + columns: [orderColumnName], + }); + joinTable.orderColumnName = orderColumnName; + joinTable.orderBy = { [orderColumnName]: 'asc' }; + } + // inv order + if (isBidirectional(attribute) && isManyToAny(attribute)) { metadataSchema.attributes[inverseOrderColumnName] = { type: 'integer', column: { @@ -519,9 +527,15 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: joinTableName, joinColumn: joinTable.inverseJoinColumn, inverseJoinColumn: joinTable.joinColumn, - orderColumnName: joinTable.inverseOrderColumnName, - inverseOrderColumnName: joinTable.orderColumnName, }; + + if (isManyToAny(attribute)) { + inverseAttribute.joinTable.orderColumnName = inverseOrderColumnName; + inverseAttribute.joinTable.orderBy = { [inverseOrderColumnName]: 'asc' }; + } + if (isAnyToMany(attribute)) { + inverseAttribute.joinTable.inverseOrderColumnName = orderColumnName; + } } }; @@ -530,4 +544,7 @@ module.exports = { isBidirectional, isOneToAny, + isManyToAny, + isAnyToOne, + isAnyToMany, }; diff --git a/packages/core/strapi/lib/services/entity-service/components.js b/packages/core/strapi/lib/services/entity-service/components.js index 4244feaa95..c64c6dd31d 100644 --- a/packages/core/strapi/lib/services/entity-service/components.js +++ b/packages/core/strapi/lib/services/entity-service/components.js @@ -48,11 +48,10 @@ const createComponents = async (uid, data) => { ); // TODO: add order - componentBody[attributeName] = components.map(({ id }, idx) => { + componentBody[attributeName] = components.map(({ id }) => { return { id, __pivot: { - order: idx + 1, field: attributeName, component_type: componentUID, }, @@ -63,7 +62,6 @@ const createComponents = async (uid, data) => { componentBody[attributeName] = { id: component.id, __pivot: { - order: 1, field: attributeName, component_type: componentUID, }, @@ -81,13 +79,12 @@ const createComponents = async (uid, data) => { } componentBody[attributeName] = await Promise.all( - dynamiczoneValues.map(async (value, idx) => { + dynamiczoneValues.map(async (value) => { const { id } = await createComponent(value.__component, value); return { id, __component: value.__component, __pivot: { - order: idx + 1, field: attributeName, }, }; @@ -145,11 +142,10 @@ const updateComponents = async (uid, entityToUpdate, data) => { componentValue.map((value) => updateOrCreateComponent(componentUID, value)) ); - componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }, idx) => { + componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => { return { id, __pivot: { - order: idx + 1, field: attributeName, component_type: componentUID, }, @@ -160,7 +156,6 @@ const updateComponents = async (uid, entityToUpdate, data) => { componentBody[attributeName] = component && { id: component.id, __pivot: { - order: 1, field: attributeName, component_type: componentUID, }, @@ -180,14 +175,13 @@ const updateComponents = async (uid, entityToUpdate, data) => { } componentBody[attributeName] = await Promise.all( - dynamiczoneValues.map(async (value, idx) => { + dynamiczoneValues.map(async (value) => { const { id } = await updateOrCreateComponent(value.__component, value); return { id, __component: value.__component, __pivot: { - order: idx + 1, field: attributeName, }, }; From 59417498abf1a61bf29aa9b6027fedcc1445795d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Thu, 15 Sep 2022 10:52:38 +0200 Subject: [PATCH 06/28] use NULL as default order + better handle migration --- .../core/database/lib/entity-manager/index.js | 257 ++++++++++-------- .../core/database/lib/metadata/relations.js | 4 +- 2 files changed, 140 insertions(+), 121 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 60b9dc4960..7239f4a7a0 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -509,18 +509,20 @@ const createEntityManager = (db) => { currentRelsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]); for (const relToDelete of currentRelsToDelete) { - await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) - .where({ - [joinColumn.name]: relToDelete[joinColumn.name], - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); + if (relToDelete[orderColumnName] !== null) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: relToDelete[joinColumn.name], + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + } } } @@ -762,19 +764,21 @@ const createEntityManager = (db) => { const updateInverseOrderPromises = []; for (const relToDelete of relsToDelete) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - updateInverseOrderPromises.push(updatePromise); + if (relToDelete[inverseOrderColumnName] !== null) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } } await Promise.all(updateInverseOrderPromises); @@ -825,18 +829,20 @@ const createEntityManager = (db) => { relsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]); for (const relToDelete of relsToDelete) { - await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) - .where({ - [joinColumn.name]: id, - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); + if (relToDelete[orderColumnName] !== null) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: id, + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + } } } @@ -844,19 +850,21 @@ const createEntityManager = (db) => { if (isBidirectional(attribute) && isManyToAny(attribute)) { const updateInverseOrderPromises = []; for (const relToDelete of relsToDelete) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - updateInverseOrderPromises.push(updatePromise); + if (relToDelete[inverseOrderColumnName] !== null) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } } await Promise.all(updateInverseOrderPromises); @@ -900,28 +908,31 @@ const createEntityManager = (db) => { // const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0]; const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0]; if (currentRel && isAnyToMany(attribute)) { - await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', currentRel[orderColumnName]), - }) - .where({ - [joinColumn.name]: id, - [orderColumnName]: { $gt: currentRel[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); + const currentOrderIsNull = currentRel[orderColumnName] === null; + if (!currentOrderIsNull) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', currentRel[orderColumnName]), + }) + .where({ + [joinColumn.name]: id, + [orderColumnName]: { $gt: currentRel[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); - currentMovingRels.forEach((rel) => { - if (rel[orderColumnName] > currentRel[orderColumnName]) { - rel[orderColumnName] -= 1; - } - }); + currentMovingRels.forEach((rel) => { + if (rel[orderColumnName] > currentRel[orderColumnName]) { + rel[orderColumnName] -= 1; + } + }); + } await this.createQueryBuilder(joinTable.name) .update({ - [orderColumnName]: max, + [orderColumnName]: currentOrderIsNull ? max + 1 : max, }) .where({ [joinColumn.name]: id, @@ -988,19 +999,21 @@ const createEntityManager = (db) => { relsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]); for (const relToDelete of relsToDelete) { - await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) - .where({ - [joinColumn.name]: id, - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - // manque le pivot ici - .execute(); + if (relToDelete[orderColumnName] !== null) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: id, + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + // manque le pivot ici + .execute(); + } } } @@ -1008,19 +1021,21 @@ const createEntityManager = (db) => { if (isBidirectional(attribute) && isManyToAny(attribute)) { const updateInverseOrderPromises = []; for (const relToDelete of relsToDelete) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - updateInverseOrderPromises.push(updatePromise); + if (relToDelete[inverseOrderColumnName] !== null) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } } await Promise.all(updateInverseOrderPromises); @@ -1106,18 +1121,20 @@ const createEntityManager = (db) => { currentRelsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]); for (const relToDelete of currentRelsToDelete) { - await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) - .where({ - [joinColumn.name]: relToDelete[joinColumn.name], - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); + if (relToDelete[orderColumnName] !== null) { + await this.createQueryBuilder(joinTable.name) + .update({ + [orderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[orderColumnName]), + }) + .where({ + [joinColumn.name]: relToDelete[joinColumn.name], + [orderColumnName]: { $gt: relToDelete[orderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + } } } @@ -1270,19 +1287,21 @@ const createEntityManager = (db) => { const updateInverseOrderPromises = []; for (const relToDelete of relsToDelete) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - updateInverseOrderPromises.push(updatePromise); + if (relToDelete[inverseOrderColumnName] !== null) { + const updatePromise = this.createQueryBuilder(joinTable.name) + .update({ + [inverseOrderColumnName]: db + .getConnection() + .raw('?? - 1', relToDelete[inverseOrderColumnName]), + }) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + updateInverseOrderPromises.push(updatePromise); + } } await Promise.all(updateInverseOrderPromises); diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index 988e581c81..23c3f0a340 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -481,7 +481,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { type: 'integer', column: { unsigned: true, - defaultTo: 0, + defaultTo: null, }, }; metadataSchema.indexes.push({ @@ -498,7 +498,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { type: 'integer', column: { unsigned: true, - defaultTo: 0, + defaultTo: null, }, }; From eaf0d842245eaf9f753854e12aabfd5535304d42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Thu, 15 Sep 2022 11:15:05 +0200 Subject: [PATCH 07/28] add decrements to queryBuilder + use it --- .../core/database/lib/entity-manager/index.js | 54 ++++--------------- .../core/database/lib/query/query-builder.js | 28 +++++++++- 2 files changed, 36 insertions(+), 46 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 7239f4a7a0..a9b153edc6 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -511,11 +511,7 @@ const createEntityManager = (db) => { for (const relToDelete of currentRelsToDelete) { if (relToDelete[orderColumnName] !== null) { await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) + .decrement(orderColumnName, 1) .where({ [joinColumn.name]: relToDelete[joinColumn.name], [orderColumnName]: { $gt: relToDelete[orderColumnName] }, @@ -766,11 +762,7 @@ const createEntityManager = (db) => { for (const relToDelete of relsToDelete) { if (relToDelete[inverseOrderColumnName] !== null) { const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) + .decrement(inverseOrderColumnName, 1) .where({ [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, @@ -831,11 +823,7 @@ const createEntityManager = (db) => { for (const relToDelete of relsToDelete) { if (relToDelete[orderColumnName] !== null) { await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) + .decrement(orderColumnName, 1) .where({ [joinColumn.name]: id, [orderColumnName]: { $gt: relToDelete[orderColumnName] }, @@ -852,11 +840,7 @@ const createEntityManager = (db) => { for (const relToDelete of relsToDelete) { if (relToDelete[inverseOrderColumnName] !== null) { const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) + .decrement(inverseOrderColumnName, 1) .where({ [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, @@ -911,11 +895,7 @@ const createEntityManager = (db) => { const currentOrderIsNull = currentRel[orderColumnName] === null; if (!currentOrderIsNull) { await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', currentRel[orderColumnName]), - }) + .decrement(orderColumnName, 1) .where({ [joinColumn.name]: id, [orderColumnName]: { $gt: currentRel[orderColumnName] }, @@ -1001,11 +981,7 @@ const createEntityManager = (db) => { for (const relToDelete of relsToDelete) { if (relToDelete[orderColumnName] !== null) { await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) + .decrement(orderColumnName, 1) .where({ [joinColumn.name]: id, [orderColumnName]: { $gt: relToDelete[orderColumnName] }, @@ -1023,11 +999,7 @@ const createEntityManager = (db) => { for (const relToDelete of relsToDelete) { if (relToDelete[inverseOrderColumnName] !== null) { const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) + .decrement(inverseOrderColumnName, 1) .where({ [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, @@ -1123,11 +1095,7 @@ const createEntityManager = (db) => { for (const relToDelete of currentRelsToDelete) { if (relToDelete[orderColumnName] !== null) { await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[orderColumnName]), - }) + .decrement(orderColumnName, 1) .where({ [joinColumn.name]: relToDelete[joinColumn.name], [orderColumnName]: { $gt: relToDelete[orderColumnName] }, @@ -1289,11 +1257,7 @@ const createEntityManager = (db) => { for (const relToDelete of relsToDelete) { if (relToDelete[inverseOrderColumnName] !== null) { const updatePromise = this.createQueryBuilder(joinTable.name) - .update({ - [inverseOrderColumnName]: db - .getConnection() - .raw('?? - 1', relToDelete[inverseOrderColumnName]), - }) + .decrement(inverseOrderColumnName, 1) .where({ [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, diff --git a/packages/core/database/lib/query/query-builder.js b/packages/core/database/lib/query/query-builder.js index ff2e1399dd..f9aea4e312 100644 --- a/packages/core/database/lib/query/query-builder.js +++ b/packages/core/database/lib/query/query-builder.js @@ -25,6 +25,8 @@ const createQueryBuilder = (uid, db, initialState = {}) => { forUpdate: false, orderBy: [], groupBy: [], + increments: [], + decrements: [], aliasCounter: 0, }, initialState @@ -84,6 +86,20 @@ const createQueryBuilder = (uid, db, initialState = {}) => { return this; }, + increment(column, amount = 1) { + state.type = 'update'; + state.increments.push({ column, amount }); + + return this; + }, + + decrement(column, amount = 1) { + state.type = 'update'; + state.decrements.push({ column, amount }); + + return this; + }, + count(count = 'id') { state.type = 'count'; state.count = count; @@ -349,7 +365,9 @@ const createQueryBuilder = (uid, db, initialState = {}) => { break; } case 'update': { - qb.update(state.data); + if (state.data) { + qb.update(state.data); + } break; } case 'delete': { @@ -374,6 +392,14 @@ const createQueryBuilder = (uid, db, initialState = {}) => { qb.forUpdate(); } + if (!_.isEmpty(state.increments)) { + state.increments.forEach((incr) => qb.increment(incr.column, incr.amount)); + } + + if (!_.isEmpty(state.decrements)) { + state.decrements.forEach((decr) => qb.decrement(decr.column, decr.amount)); + } + if (state.limit) { qb.limit(state.limit); } From e2d4cb483c3ef2c561ca52e0c7d92311253dfa83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Thu, 15 Sep 2022 15:27:59 +0200 Subject: [PATCH 08/28] fix delete previous relations for anyToOne --- .../core/database/lib/entity-manager/index.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index a9b153edc6..23bc7c1b4d 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -1116,6 +1116,44 @@ const createEntityManager = (db) => { .where(joinTable.on || {}) .execute(); } + + // 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 this.createQueryBuilder(joinTable.name) + .select(select) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $ne: relIdsToaddOrMove[0] }, + }) + .where(joinTable.on || {}) + .execute(); + + for (const relToDelete of currentRelsToDelete) { + if (relToDelete[inverseOrderColumnName] !== null) { + await this.createQueryBuilder(joinTable.name) + .decrement(inverseOrderColumnName, 1) + .where({ + [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], + [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, + }) + .where(joinTable.on || {}) + .execute(); + } + } + } + + // delete previous oneToAny relations + await this.createQueryBuilder(joinTable.name) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, + }) + .where(joinTable.on || {}) + .execute(); + } } } } From e7428cdf835337adb78232c63b26f38c5a16d1f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Thu, 15 Sep 2022 16:48:30 +0200 Subject: [PATCH 09/28] fix tests --- packages/core/database/lib/entity-manager/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 23bc7c1b4d..548b1554ce 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -1125,7 +1125,7 @@ const createEntityManager = (db) => { .select(select) .where({ [joinColumn.name]: id, - [inverseJoinColumn.name]: { $ne: relIdsToaddOrMove[0] }, + [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, }) .where(joinTable.on || {}) .execute(); From 5d0eb61faeb05b21e68a8248fedd0c250528069e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Fri, 16 Sep 2022 18:38:46 +0200 Subject: [PATCH 10/28] start factorizing code --- .../core/database/lib/entity-manager/index.js | 367 ++---------------- .../core/database/lib/entity-manager/utils.js | 200 ++++++++++ 2 files changed, 225 insertions(+), 342 deletions(-) create mode 100644 packages/core/database/lib/entity-manager/utils.js diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 548b1554ce..0218082707 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -21,13 +21,12 @@ const { createField } = require('../fields'); const { createQueryBuilder } = require('../query'); const { createRepository } = require('./entity-repository'); const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations'); +const { isBidirectional, isManyToAny, isAnyToOne, isAnyToMany } = require('../metadata/relations'); const { - isBidirectional, - isOneToAny, - isManyToAny, - isAnyToOne, - isAnyToMany, -} = require('../metadata/relations'); + deletePreviousOneToAnyRelations, + deletePreviousAnyToOneRelations, + deleteAllRelations, +} = require('./utils'); const toId = (value) => value.id || value; const toIds = (value) => castArray(value || []).map(toId); @@ -493,42 +492,7 @@ const createEntityManager = (db) => { const relsToAdd = uniqBy('id', cleanRelationData.connect || cleanRelationData); const relIdsToadd = toIds(relsToAdd); - // 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 this.createQueryBuilder(joinTable.name) - .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 this.createQueryBuilder(joinTable.name) - .decrement(orderColumnName, 1) - .where({ - [joinColumn.name]: relToDelete[joinColumn.name], - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - } - } - } - - // delete previous oneToAny relations - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ [inverseJoinColumn.name]: relIdsToadd }) - .where(joinTable.on || {}) - .execute(); - } + await deletePreviousOneToAnyRelations({ id, attribute, joinTable, relIdsToadd, db }); // prepare new relations to insert const insert = relsToAdd.map((data) => { @@ -738,50 +702,7 @@ const createEntityManager = (db) => { // only delete relations if (isNull(data[attributeName])) { - // INVERSE ORDER UPDATE - if (isBidirectional(attribute) && isManyToAny(attribute)) { - let lastId = 0; - let done = false; - const batchSize = 100; - while (!done) { - const relsToDelete = await this.createQueryBuilder(joinTable.name) - .select(select) - .where({ - [joinColumn.name]: id, - id: { $gt: lastId }, - }) - .where(joinTable.on || {}) - .orderBy('id') - .limit(batchSize) - .execute(); - // TODO: cannot put pivot here... - done = relsToDelete.length < batchSize; - lastId = relsToDelete[relsToDelete.length - 1]?.id; - - const updateInverseOrderPromises = []; - for (const relToDelete of relsToDelete) { - if (relToDelete[inverseOrderColumnName] !== null) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .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 this.createQueryBuilder(joinTable.name) - .delete() - .where({ [joinColumn.name]: id }) - .where(joinTable.on || {}) - .execute(); + await deleteAllRelations({ id, attribute, joinTable, db }); } else { const cleanRelationData = toAssocs(data[attributeName]); const isPartialUpdate = @@ -800,70 +721,7 @@ const createEntityManager = (db) => { // DELETE relations in disconnect const relIdsToDelete = toIds(differenceBy('id', disconnect, connect)); - // UPDATE RELEVANT ORDERS - if ( - isAnyToMany(attribute) || - (isBidirectional(attribute) && isManyToAny(attribute)) - ) { - const relsToDelete = await this.createQueryBuilder(joinTable.name) - .select(select) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $in: relIdsToDelete }, - }) - .where(joinTable.on || {}) - .execute(); - - // 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]); - - for (const relToDelete of relsToDelete) { - if (relToDelete[orderColumnName] !== null) { - await this.createQueryBuilder(joinTable.name) - .decrement(orderColumnName, 1) - .where({ - [joinColumn.name]: id, - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - } - } - } - - // INVERSE ORDER UPDATE - if (isBidirectional(attribute) && isManyToAny(attribute)) { - const updateInverseOrderPromises = []; - for (const relToDelete of relsToDelete) { - if (relToDelete[inverseOrderColumnName] !== null) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .decrement(inverseOrderColumnName, 1) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - updateInverseOrderPromises.push(updatePromise); - } - } - - await Promise.all(updateInverseOrderPromises); - } - } - - // DELETE - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $in: relIdsToDelete }, - }) - .where(joinTable.on || {}) - .execute(); + await deleteAllRelations({ id, attribute, joinTable, onlyFor: relIdsToDelete, db }); // add/move let max; @@ -950,80 +808,7 @@ const createEntityManager = (db) => { // overwrite all relations const relsToAdd = uniqBy('id', cleanRelationData); relIdsToaddOrMove = toIds(relsToAdd); - - // UPDATE RELEVANT ORDERS BEFORE DELETE - if (isAnyToMany(attribute) || isManyToAny(attribute)) { - let lastId = 0; - let done = false; - const batchSize = 100; - while (!done) { - const relsToDelete = await this.createQueryBuilder(joinTable.name) - .select(select) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, - id: { $gt: lastId }, - }) - .where(joinTable.on || {}) - .orderBy('id') - .limit(batchSize) - .execute(); - - done = relsToDelete.length < batchSize; - lastId = relsToDelete[relsToDelete.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]); - - for (const relToDelete of relsToDelete) { - if (relToDelete[orderColumnName] !== null) { - await this.createQueryBuilder(joinTable.name) - .decrement(orderColumnName, 1) - .where({ - [joinColumn.name]: id, - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - // manque le pivot ici - .execute(); - } - } - } - - // INVERSE ORDER UPDATE - if (isBidirectional(attribute) && isManyToAny(attribute)) { - const updateInverseOrderPromises = []; - for (const relToDelete of relsToDelete) { - if (relToDelete[inverseOrderColumnName] !== null) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .decrement(inverseOrderColumnName, 1) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - updateInverseOrderPromises.push(updatePromise); - } - } - - await Promise.all(updateInverseOrderPromises); - } - } - } - - // DELETE - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, - }) - .where(joinTable.on || {}) - .execute(); + await deleteAllRelations({ id, attribute, joinTable, except: relIdsToaddOrMove, db }); const currentMovingRels = await this.createQueryBuilder(joinTable.name) .select(select) @@ -1053,6 +838,7 @@ const createEntityManager = (db) => { const insert = { [joinColumn.name]: id, [inverseJoinColumn.name]: relToAdd.id, + ...(joinTable.on || {}), ...(relToAdd.__pivot || {}), }; @@ -1078,82 +864,22 @@ const createEntityManager = (db) => { } // 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 this.createQueryBuilder(joinTable.name) - .select(select) - .where({ - [inverseJoinColumn.name]: relIdsToaddOrMove, - [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 this.createQueryBuilder(joinTable.name) - .decrement(orderColumnName, 1) - .where({ - [joinColumn.name]: relToDelete[joinColumn.name], - [orderColumnName]: { $gt: relToDelete[orderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - } - } - } - - // delete previous oneToAny relations - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ - [inverseJoinColumn.name]: relIdsToaddOrMove, - [joinColumn.name]: { $ne: id }, - }) - .where(joinTable.on || {}) - .execute(); - } + await deletePreviousOneToAnyRelations({ + id, + attribute, + joinTable, + relIdsToadd: relIdsToaddOrMove, + db, + }); // 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 this.createQueryBuilder(joinTable.name) - .select(select) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, - }) - .where(joinTable.on || {}) - .execute(); - - for (const relToDelete of currentRelsToDelete) { - if (relToDelete[inverseOrderColumnName] !== null) { - await this.createQueryBuilder(joinTable.name) - .decrement(inverseOrderColumnName, 1) - .where({ - [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name], - [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] }, - }) - .where(joinTable.on || {}) - .execute(); - } - } - } - - // delete previous oneToAny relations - await this.createQueryBuilder(joinTable.name) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove }, - }) - .where(joinTable.on || {}) - .execute(); - } + await deletePreviousAnyToOneRelations({ + id, + attribute, + joinTable, + relIdsToadd: relIdsToaddOrMove, + db, + }); } } } @@ -1270,51 +996,8 @@ const createEntityManager = (db) => { if (attribute.joinTable) { const { joinTable } = attribute; - const { joinColumn, inverseJoinColumn, inverseOrderColumnName } = joinTable; - // INVERSE ORDER UPDATE - if (isBidirectional(attribute) && isManyToAny(attribute)) { - let lastId = 0; - let done = false; - const batchSize = 100; - while (!done) { - const relsToDelete = await this.createQueryBuilder(joinTable.name) - .select(inverseJoinColumn.name, inverseOrderColumnName) - .where({ - [joinColumn.name]: id, - id: { $gt: lastId }, - }) - .where(joinTable.on || {}) - .orderBy('id') - .limit(batchSize) - .execute(); - done = relsToDelete.length < batchSize; - lastId = relsToDelete[relsToDelete.length - 1]?.id; - - const updateInverseOrderPromises = []; - for (const relToDelete of relsToDelete) { - if (relToDelete[inverseOrderColumnName] !== null) { - const updatePromise = this.createQueryBuilder(joinTable.name) - .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 this.createQueryBuilder(joinTable.name) - .delete() - .where({ [joinColumn.name]: id }) - .where(joinTable.on || {}) - .execute(); + await deleteAllRelations({ id, attribute, joinTable, db }); } } }, diff --git a/packages/core/database/lib/entity-manager/utils.js b/packages/core/database/lib/entity-manager/utils.js new file mode 100644 index 0000000000..37f3f5d76c --- /dev/null +++ b/packages/core/database/lib/entity-manager/utils.js @@ -0,0 +1,200 @@ +'use strict'; + +const { + isBidirectional, + isOneToAny, + isManyToAny, + isAnyToOne, + isAnyToMany, +} = 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); + + // 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() + .where({ + [inverseJoinColumn.name]: relIdsToadd, + [joinColumn.name]: { $ne: id }, + }) + .where(joinTable.on || {}) + .execute(); + } +}; + +const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relIdsToadd, db }) => { + const { joinColumn, inverseJoinColumn, inverseOrderColumnName } = joinTable; + const select = getSelect(joinTable, attribute); + + // 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) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $notIn: relIdsToadd }, + }) + .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(); + } + } + } + + // delete previous oneToAny relations + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $notIn: relIdsToadd }, + }) + .where(joinTable.on || {}) + .execute(); + } +}; + +// INVERSE ORDER UPDATE +const deleteAllRelations = async ({ + id, + attribute, + joinTable, + except = undefined, + onlyFor = undefined, + db, +}) => { + const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; + const select = getSelect(joinTable, attribute); + + if (isAnyToMany(attribute) || (isBidirectional(attribute) && isManyToAny(attribute))) { + let lastId = 0; + let done = false; + const batchSize = 100; + while (!done) { + const relsToDelete = await createQueryBuilder(joinTable.name, db) + .select(select) + .where({ + [joinColumn.name]: id, + id: { $gt: lastId }, + ...(except ? { [inverseJoinColumn.name]: { $notIn: except } } : {}), + ...(onlyFor ? { [inverseJoinColumn.name]: { $in: onlyFor } } : {}), + }) + .where(joinTable.on || {}) + .orderBy('id') + .limit(batchSize) + .execute(); + done = relsToDelete.length < batchSize; + lastId = relsToDelete[relsToDelete.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]); + + 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(); + } + } + } + + 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 createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + ...(except ? { [inverseJoinColumn.name]: { $notIn: except } } : {}), + ...(onlyFor ? { [inverseJoinColumn.name]: { $in: onlyFor } } : {}), + }) + .where(joinTable.on || {}) + .execute(); +}; + +module.exports = { + deletePreviousOneToAnyRelations, + deletePreviousAnyToOneRelations, + deleteAllRelations, +}; From c1d82b6f9ed29f14a861e3475ee06e1f4a44c44a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Mon, 19 Sep 2022 14:39:59 +0200 Subject: [PATCH 11/28] use set format --- .../core/database/lib/entity-manager/index.js | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 0218082707..c440956254 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -15,6 +15,7 @@ const { uniqBy, differenceBy, groupBy, + isNumber, } = require('lodash/fp'); const types = require('../types'); const { createField } = require('../fields'); @@ -32,15 +33,8 @@ const toId = (value) => value.id || value; const toIds = (value) => castArray(value || []).map(toId); const isValidId = (value) => isString(value) || isInteger(value); -const toAssocs = (data) => { - if (data?.connect || data?.disconnect || (isPlainObject(data) && !data.id)) { - return { - connect: toAssocs(data.connect), - disconnect: toAssocs(data.disconnect), - }; - } - - return castArray(data) +const toIdArray = (data) => { + const array = castArray(data) .filter((datum) => !isNil(datum)) .map((datum) => { // if it is a string or an integer return an obj with id = to datum @@ -55,6 +49,26 @@ const toAssocs = (data) => { return datum; }); + return uniqBy('id', array); +}; + +const toAssocs = (data) => { + if (isArray(data) || isString(data) || isNumber(data) || data?.id) { + return { + set: isNull(data) ? data : toIdArray(data), + }; + } + + if (data?.set) { + return { + set: isNull(data.set) ? data.set : toIdArray(data.set), + }; + } + + return { + connect: toIdArray(data?.connect), + disconnect: toIdArray(data?.disconnect), + }; }; const processData = (metadata, data = {}, { withDefaults = false } = {}) => { @@ -389,7 +403,7 @@ const createEntityManager = (db) => { const { idColumn, typeColumn } = morphColumn; - const rows = toAssocs(data[attributeName]).map((data, idx) => { + const rows = toAssocs(data[attributeName]).set.map((data, idx) => { return { [joinColumn.name]: data.id, [idColumn.name]: id, @@ -418,7 +432,7 @@ const createEntityManager = (db) => { const { idColumn, typeColumn, typeField = '__type' } = morphColumn; - const rows = toAssocs(data[attributeName]).map((data) => ({ + const rows = toAssocs(data[attributeName]).set.map((data) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], @@ -489,7 +503,7 @@ const createEntityManager = (db) => { } const cleanRelationData = toAssocs(data[attributeName]); - const relsToAdd = uniqBy('id', cleanRelationData.connect || cleanRelationData); + const relsToAdd = cleanRelationData.set || cleanRelationData.connect; const relIdsToadd = toIds(relsToAdd); await deletePreviousOneToAnyRelations({ id, attribute, joinTable, relIdsToadd, db }); @@ -599,7 +613,7 @@ const createEntityManager = (db) => { }) .execute(); - const rows = toAssocs(data[attributeName]).map((data, idx) => ({ + const rows = toAssocs(data[attributeName]).set.map((data, idx) => ({ [joinColumn.name]: data.id, [idColumn.name]: id, [typeColumn.name]: uid, @@ -638,7 +652,7 @@ const createEntityManager = (db) => { }) .execute(); - const rows = toAssocs(data[attributeName]).map((data) => ({ + const rows = toAssocs(data[attributeName]).set.map((data) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], @@ -705,21 +719,20 @@ const createEntityManager = (db) => { await deleteAllRelations({ id, attribute, joinTable, db }); } else { const cleanRelationData = toAssocs(data[attributeName]); - const isPartialUpdate = - has('connect', cleanRelationData) || has('disconnect', cleanRelationData); + const isPartialUpdate = !has('set', cleanRelationData); let relIdsToaddOrMove; if (isPartialUpdate) { // does not support pivot - let connect = uniqBy('id', cleanRelationData.connect || []); - let disconnect = uniqBy('id', cleanRelationData.disconnect || []); if (isAnyToOne(attribute)) { - connect = connect.slice(-1); - disconnect = []; + cleanRelationData.connect = cleanRelationData.connect.slice(-1); + cleanRelationData.disconnect = []; } - relIdsToaddOrMove = toIds(connect); + relIdsToaddOrMove = toIds(cleanRelationData.connect); // DELETE relations in disconnect - const relIdsToDelete = toIds(differenceBy('id', disconnect, connect)); + const relIdsToDelete = toIds( + differenceBy('id', cleanRelationData.disconnect, cleanRelationData.connect) + ); await deleteAllRelations({ id, attribute, joinTable, onlyFor: relIdsToDelete, db }); @@ -746,8 +759,7 @@ const createEntityManager = (db) => { ).max; } - for (const relToAddOrMove of connect) { - // const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0]; + for (const relToAddOrMove of cleanRelationData.connect) { const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0]; if (currentRel && isAnyToMany(attribute)) { const currentOrderIsNull = currentRel[orderColumnName] === null; @@ -783,6 +795,7 @@ const createEntityManager = (db) => { [joinColumn.name]: id, [inverseJoinColumn.name]: relToAddOrMove.id, ...(relToAddOrMove.__pivot || {}), + ...(joinTable.on || {}), }; if (isAnyToMany(attribute)) { @@ -806,8 +819,7 @@ const createEntityManager = (db) => { } } else { // overwrite all relations - const relsToAdd = uniqBy('id', cleanRelationData); - relIdsToaddOrMove = toIds(relsToAdd); + relIdsToaddOrMove = toIds(cleanRelationData.set); await deleteAllRelations({ id, attribute, joinTable, except: relIdsToaddOrMove, db }); const currentMovingRels = await this.createQueryBuilder(joinTable.name) @@ -821,7 +833,7 @@ const createEntityManager = (db) => { const currentMovingRelsMap = groupBy(inverseJoinColumn.name, currentMovingRels); let max = 0; - for (const relToAdd of relsToAdd) { + for (const relToAdd of cleanRelationData.set) { const currentRel = currentMovingRelsMap[relToAdd.id]?.[0]; if (currentRel && isAnyToMany(attribute)) { From 4ef5e1fa48fcb5a452632758b57a34416662c263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Mon, 19 Sep 2022 17:38:24 +0200 Subject: [PATCH 12/28] fix set issues --- .../core/database/lib/entity-manager/index.js | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index c440956254..327b9b5d37 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -12,8 +12,9 @@ const { isEmpty, isArray, isNull, - uniqBy, - differenceBy, + uniqWith, + isEqual, + differenceWith, groupBy, isNumber, } = require('lodash/fp'); @@ -49,11 +50,11 @@ const toIdArray = (data) => { return datum; }); - return uniqBy('id', array); + return uniqWith(isEqual, array); }; const toAssocs = (data) => { - if (isArray(data) || isString(data) || isNumber(data) || data?.id) { + if (isArray(data) || isString(data) || isNumber(data) || isNull(data) || data?.id) { return { set: isNull(data) ? data : toIdArray(data), }; @@ -384,6 +385,8 @@ const createEntityManager = (db) => { continue; } + const cleanRelationData = toAssocs(data[attributeName]); + if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { const { target, morphBy } = attribute; @@ -393,9 +396,11 @@ const createEntityManager = (db) => { // set columns const { idColumn, typeColumn } = targetAttribute.morphColumn; + const relId = toId(cleanRelationData.set[0]); + await this.createQueryBuilder(target) .update({ [idColumn.name]: id, [typeColumn.name]: uid }) - .where({ id: toId(data[attributeName]) }) + .where({ id: relId }) .execute(); } else if (targetAttribute.relation === 'morphToMany') { const { joinTable } = targetAttribute; @@ -403,7 +408,7 @@ const createEntityManager = (db) => { const { idColumn, typeColumn } = morphColumn; - const rows = toAssocs(data[attributeName]).set.map((data, idx) => { + const rows = cleanRelationData.set.map((data, idx) => { return { [joinColumn.name]: data.id, [idColumn.name]: id, @@ -432,7 +437,7 @@ const createEntityManager = (db) => { const { idColumn, typeColumn, typeField = '__type' } = morphColumn; - const rows = toAssocs(data[attributeName]).set.map((data) => ({ + const rows = cleanRelationData.set.map((data) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], @@ -458,13 +463,14 @@ const createEntityManager = (db) => { } if (attribute.joinColumn && attribute.owner) { + const relIdsToAdd = toIds(cleanRelationData.set); if ( attribute.relation === 'oneToOne' && isBidirectional(attribute) && - data[attributeName] + relIdsToAdd.length ) { await this.createQueryBuilder(uid) - .where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } }) + .where({ [attribute.joinColumn.name]: relIdsToAdd, id: { $ne: id } }) .update({ [attribute.joinColumn.name]: null }) .execute(); } @@ -478,6 +484,7 @@ const createEntityManager = (db) => { const { target } = attribute; // TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL) + const relIdsToAdd = toIds(cleanRelationData.set); await this.createQueryBuilder(target) .where({ [attribute.joinColumn.referencedColumn]: id }) @@ -487,7 +494,7 @@ const createEntityManager = (db) => { await this.createQueryBuilder(target) .update({ [attribute.joinColumn.referencedColumn]: id }) // NOTE: works if it is an array or a single id - .where({ id: data[attributeName] }) + .where({ id: relIdsToAdd }) .execute(); } @@ -502,7 +509,6 @@ const createEntityManager = (db) => { select.push(orderColumnName); } - const cleanRelationData = toAssocs(data[attributeName]); const relsToAdd = cleanRelationData.set || cleanRelationData.connect; const relIdsToadd = toIds(relsToAdd); @@ -574,6 +580,7 @@ const createEntityManager = (db) => { if (attribute.type !== 'relation' || !has(attributeName, data)) { continue; } + const cleanRelationData = toAssocs(data[attributeName]); if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { const { target, morphBy } = attribute; @@ -591,10 +598,11 @@ const createEntityManager = (db) => { .where({ [idColumn.name]: id, [typeColumn.name]: uid }) .execute(); - if (!isNull(data[attributeName])) { + if (!isNull(cleanRelationData.set)) { + const relId = toIds(cleanRelationData.set[0]); await this.createQueryBuilder(target) .update({ [idColumn.name]: id, [typeColumn.name]: uid }) - .where({ id: toId(data[attributeName]) }) + .where({ id: relId }) .execute(); } } else if (targetAttribute.relation === 'morphToMany') { @@ -613,7 +621,7 @@ const createEntityManager = (db) => { }) .execute(); - const rows = toAssocs(data[attributeName]).set.map((data, idx) => ({ + const rows = cleanRelationData.set.map((data, idx) => ({ [joinColumn.name]: data.id, [idColumn.name]: id, [typeColumn.name]: uid, @@ -652,7 +660,7 @@ const createEntityManager = (db) => { }) .execute(); - const rows = toAssocs(data[attributeName]).set.map((data) => ({ + const rows = cleanRelationData.set.map((data) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], @@ -693,10 +701,10 @@ const createEntityManager = (db) => { .update({ [attribute.joinColumn.referencedColumn]: null }) .execute(); - if (!isNull(data[attributeName])) { + if (!isNull(cleanRelationData.set)) { + const relIdsToAdd = toIds(cleanRelationData.set); await this.createQueryBuilder(target) - // NOTE: works if it is an array or a single id - .where({ id: data[attributeName] }) + .where({ id: relIdsToAdd }) .update({ [attribute.joinColumn.referencedColumn]: id }) .execute(); } @@ -715,10 +723,9 @@ const createEntityManager = (db) => { } // only delete relations - if (isNull(data[attributeName])) { + if (isNull(cleanRelationData.set)) { await deleteAllRelations({ id, attribute, joinTable, db }); } else { - const cleanRelationData = toAssocs(data[attributeName]); const isPartialUpdate = !has('set', cleanRelationData); let relIdsToaddOrMove; @@ -731,7 +738,7 @@ const createEntityManager = (db) => { relIdsToaddOrMove = toIds(cleanRelationData.connect); // DELETE relations in disconnect const relIdsToDelete = toIds( - differenceBy('id', cleanRelationData.disconnect, cleanRelationData.connect) + differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect) ); await deleteAllRelations({ id, attribute, joinTable, onlyFor: relIdsToDelete, db }); From 4ce09c51932bc030ab9de30e55381934a8c2fdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Mon, 19 Sep 2022 17:58:00 +0200 Subject: [PATCH 13/28] various fixes --- .../core/database/lib/entity-manager/index.js | 29 ++++++++++++------- .../core/database/lib/metadata/relations.js | 4 +-- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 327b9b5d37..789848cb5c 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -15,7 +15,6 @@ const { uniqWith, isEqual, differenceWith, - groupBy, isNumber, } = require('lodash/fp'); const types = require('../types'); @@ -753,7 +752,10 @@ const createEntityManager = (db) => { }) .where(joinTable.on || {}) .execute(); - const currentMovingRelsMap = groupBy(inverseJoinColumn.name, currentMovingRels); + const currentMovingRelsMap = currentMovingRels.reduce( + (acc, rel) => Object.assign(acc, { [rel[inverseJoinColumn.name]]: rel }), + {} + ); if (isAnyToMany(attribute)) { max = ( @@ -767,7 +769,7 @@ const createEntityManager = (db) => { } for (const relToAddOrMove of cleanRelationData.connect) { - const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0]; + const currentRel = currentMovingRelsMap[relToAddOrMove.id]; if (currentRel && isAnyToMany(attribute)) { const currentOrderIsNull = currentRel[orderColumnName] === null; if (!currentOrderIsNull) { @@ -787,9 +789,13 @@ const createEntityManager = (db) => { }); } + if (currentOrderIsNull) { + max += 1; + } + await this.createQueryBuilder(joinTable.name) .update({ - [orderColumnName]: currentOrderIsNull ? max + 1 : max, + [orderColumnName]: max, }) .where({ [joinColumn.name]: id, @@ -837,14 +843,17 @@ const createEntityManager = (db) => { }) .where(joinTable.on || {}) .execute(); - const currentMovingRelsMap = groupBy(inverseJoinColumn.name, currentMovingRels); + const currentMovingRelsMap = currentMovingRels.reduce( + (acc, rel) => Object.assign(acc, { [rel[inverseJoinColumn.name]]: rel }), + {} + ); - let max = 0; + let index = 0; for (const relToAdd of cleanRelationData.set) { - const currentRel = currentMovingRelsMap[relToAdd.id]?.[0]; + const currentRel = currentMovingRelsMap[relToAdd.id]; if (currentRel && isAnyToMany(attribute)) { - const update = { [orderColumnName]: max + 1 }; + const update = { [orderColumnName]: index + 1 }; await this.createQueryBuilder(joinTable.name) .update(update) .where({ @@ -862,7 +871,7 @@ const createEntityManager = (db) => { }; if (isAnyToMany(attribute)) { - insert[orderColumnName] = max + 1; + insert[orderColumnName] = index + 1; } // can be optimized in one query if (isBidirectional(attribute) && isManyToAny(attribute)) { @@ -878,7 +887,7 @@ const createEntityManager = (db) => { await this.createQueryBuilder(joinTable.name).insert(insert).execute(); } - max += 1; + index += 1; } } diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index 23c3f0a340..a592c9be6e 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -406,8 +406,8 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { inverseJoinColumnName = `inv_${inverseJoinColumnName}`; } - const orderColumnName = _.snakeCase(`${meta.singularName}_order`); - let inverseOrderColumnName = _.snakeCase(`${targetMeta.singularName}_order`); + const orderColumnName = _.snakeCase(`${targetMeta.singularName}_order`); + let inverseOrderColumnName = _.snakeCase(`${meta.singularName}_order`); // if relation is self referencing if (attribute.relation === 'manyToMany' && joinColumnName === inverseJoinColumnName) { From 3b0d6b6e7f7b4616e07a2a36c56148d26b58f7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 20 Sep 2022 11:18:32 +0200 Subject: [PATCH 14/28] refactor deleteAllRelations --- .../core/database/lib/entity-manager/index.js | 18 ++++++++++----- .../{utils.js => regular-relations.js} | 23 ++++++++----------- 2 files changed, 22 insertions(+), 19 deletions(-) rename packages/core/database/lib/entity-manager/{utils.js => regular-relations.js} (93%) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 789848cb5c..3801afa9d1 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -26,8 +26,8 @@ const { isBidirectional, isManyToAny, isAnyToOne, isAnyToMany } = require('../me const { deletePreviousOneToAnyRelations, deletePreviousAnyToOneRelations, - deleteAllRelations, -} = require('./utils'); + deleteRelations, +} = require('./regular-relations'); const toId = (value) => value.id || value; const toIds = (value) => castArray(value || []).map(toId); @@ -723,7 +723,7 @@ const createEntityManager = (db) => { // only delete relations if (isNull(cleanRelationData.set)) { - await deleteAllRelations({ id, attribute, joinTable, db }); + await deleteRelations({ id, attribute, joinTable, db }, { relsToDelete: 'all' }); } else { const isPartialUpdate = !has('set', cleanRelationData); let relIdsToaddOrMove; @@ -740,7 +740,10 @@ const createEntityManager = (db) => { differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect) ); - await deleteAllRelations({ id, attribute, joinTable, onlyFor: relIdsToDelete, db }); + await deleteRelations( + { id, attribute, joinTable, db }, + { relsToDelete: relIdsToDelete } + ); // add/move let max; @@ -833,7 +836,10 @@ const createEntityManager = (db) => { } else { // overwrite all relations relIdsToaddOrMove = toIds(cleanRelationData.set); - await deleteAllRelations({ id, attribute, joinTable, except: relIdsToaddOrMove, db }); + await deleteRelations( + { id, attribute, joinTable, db }, + { relsToDelete: 'all', relsToNotDelete: relIdsToaddOrMove } + ); const currentMovingRels = await this.createQueryBuilder(joinTable.name) .select(select) @@ -1025,7 +1031,7 @@ const createEntityManager = (db) => { if (attribute.joinTable) { const { joinTable } = attribute; - await deleteAllRelations({ id, attribute, joinTable, db }); + await deleteRelations({ id, attribute, joinTable, db }, { relsToDelete: 'all' }); } } }, diff --git a/packages/core/database/lib/entity-manager/utils.js b/packages/core/database/lib/entity-manager/regular-relations.js similarity index 93% rename from packages/core/database/lib/entity-manager/utils.js rename to packages/core/database/lib/entity-manager/regular-relations.js index 37f3f5d76c..3855687a2e 100644 --- a/packages/core/database/lib/entity-manager/utils.js +++ b/packages/core/database/lib/entity-manager/regular-relations.js @@ -110,16 +110,13 @@ const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relId }; // INVERSE ORDER UPDATE -const deleteAllRelations = async ({ - id, - attribute, - joinTable, - except = undefined, - onlyFor = undefined, - db, -}) => { +const deleteRelations = async ( + { id, attribute, joinTable, db }, + { relsToNotDelete = [], relsToDelete = [] } +) => { const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; const select = getSelect(joinTable, attribute); + const all = relsToDelete === 'all'; if (isAnyToMany(attribute) || (isBidirectional(attribute) && isManyToAny(attribute))) { let lastId = 0; @@ -131,8 +128,8 @@ const deleteAllRelations = async ({ .where({ [joinColumn.name]: id, id: { $gt: lastId }, - ...(except ? { [inverseJoinColumn.name]: { $notIn: except } } : {}), - ...(onlyFor ? { [inverseJoinColumn.name]: { $in: onlyFor } } : {}), + [inverseJoinColumn.name]: { $notIn: relsToNotDelete }, + ...(all ? {} : { [inverseJoinColumn.name]: { $in: relsToDelete } }), }) .where(joinTable.on || {}) .orderBy('id') @@ -186,8 +183,8 @@ const deleteAllRelations = async ({ .delete() .where({ [joinColumn.name]: id, - ...(except ? { [inverseJoinColumn.name]: { $notIn: except } } : {}), - ...(onlyFor ? { [inverseJoinColumn.name]: { $in: onlyFor } } : {}), + [inverseJoinColumn.name]: { $notIn: relsToNotDelete }, + ...(all ? {} : { [inverseJoinColumn.name]: { $in: relsToDelete } }), }) .where(joinTable.on || {}) .execute(); @@ -196,5 +193,5 @@ const deleteAllRelations = async ({ module.exports = { deletePreviousOneToAnyRelations, deletePreviousAnyToOneRelations, - deleteAllRelations, + deleteRelations, }; From a9819dc375d96582aad06a7c9d8b5ccc17af66d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 20 Sep 2022 15:53:17 +0200 Subject: [PATCH 15/28] use onConflict in updateRelations --- .../core/database/lib/entity-manager/index.js | 131 ++++++++++-------- .../lib/entity-manager/regular-relations.js | 12 +- packages/core/database/lib/metadata/index.js | 5 + .../core/database/lib/metadata/relations.js | 5 + .../core/database/lib/query/query-builder.js | 29 ++++ 5 files changed, 116 insertions(+), 66 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 3801afa9d1..e45aef9f8e 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -16,6 +16,8 @@ const { isEqual, differenceWith, isNumber, + map, + difference, } = require('lodash/fp'); const types = require('../types'); const { createField } = require('../fields'); @@ -723,7 +725,7 @@ const createEntityManager = (db) => { // only delete relations if (isNull(cleanRelationData.set)) { - await deleteRelations({ id, attribute, joinTable, db }, { relsToDelete: 'all' }); + await deleteRelations({ id, attribute, joinTable, db }, { relIdsToDelete: 'all' }); } else { const isPartialUpdate = !has('set', cleanRelationData); let relIdsToaddOrMove; @@ -740,10 +742,7 @@ const createEntityManager = (db) => { differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect) ); - await deleteRelations( - { id, attribute, joinTable, db }, - { relsToDelete: relIdsToDelete } - ); + await deleteRelations({ id, attribute, joinTable, db }, { relIdsToDelete }); // add/move let max; @@ -838,63 +837,75 @@ const createEntityManager = (db) => { relIdsToaddOrMove = toIds(cleanRelationData.set); await deleteRelations( { id, attribute, joinTable, db }, - { relsToDelete: 'all', relsToNotDelete: relIdsToaddOrMove } + { relIdsToDelete: 'all', relIdsToNotDelete: relIdsToaddOrMove } ); - const currentMovingRels = await this.createQueryBuilder(joinTable.name) - .select(select) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, - }) - .where(joinTable.on || {}) - .execute(); - const currentMovingRelsMap = currentMovingRels.reduce( - (acc, rel) => Object.assign(acc, { [rel[inverseJoinColumn.name]]: rel }), - {} - ); - - let index = 0; - for (const relToAdd of cleanRelationData.set) { - const currentRel = currentMovingRelsMap[relToAdd.id]; - - if (currentRel && isAnyToMany(attribute)) { - const update = { [orderColumnName]: index + 1 }; - await this.createQueryBuilder(joinTable.name) - .update(update) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: relToAdd.id, - }) - .where(joinTable.on || {}) - .execute(); - } else if (!currentRel) { - const insert = { - [joinColumn.name]: id, - [inverseJoinColumn.name]: relToAdd.id, - ...(joinTable.on || {}), - ...(relToAdd.__pivot || {}), - }; - - if (isAnyToMany(attribute)) { - insert[orderColumnName] = index + 1; - } - // can be optimized in one query - if (isBidirectional(attribute) && isManyToAny(attribute)) { - const { max: reverseMax } = await this.createQueryBuilder(joinTable.name) - .max(inverseOrderColumnName) - .where({ [inverseJoinColumn.name]: id }) - .where(joinTable.on || {}) - .first() - .execute(); - - insert[inverseOrderColumnName] = reverseMax + 1; - } - - await this.createQueryBuilder(joinTable.name).insert(insert).execute(); - } - index += 1; + if (isEmpty(cleanRelationData.set)) { + continue; } + + const insert = cleanRelationData.set.map((relToAdd) => ({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: relToAdd.id, + ...(joinTable.on || {}), + ...(relToAdd.__pivot || {}), + })); + + // add order value + if (isAnyToMany(attribute)) { + insert.forEach((row, idx) => { + row[orderColumnName] = idx + 1; + }); + } + + // add inv order value + if (isBidirectional(attribute) && isManyToAny(attribute)) { + const existingRels = await this.createQueryBuilder(joinTable.name) + .select('id') + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, + }) + .where(joinTable.on || {}) + .execute(); + + const nonExistingRelsIds = difference(relIdsToaddOrMove, map('id', existingRels)); + + const maxResults = await db + .getConnection() + .select(inverseJoinColumn.name) + .max(inverseOrderColumnName, { as: 'max' }) + .whereIn(inverseJoinColumn.name, nonExistingRelsIds) + .where(joinTable.on || {}) + .groupBy(inverseJoinColumn.name) + .from(joinTable.name); + + const maxMap = maxResults.reduce( + (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }), + {} + ); + + insert.forEach((row) => { + row[inverseOrderColumnName] = (maxMap[row[inverseJoinColumn.name]] || 0) + 1; + }); + } + + // insert rows + const query = this.createQueryBuilder(joinTable.name) + .insert(insert) + .onConflict([ + joinColumn.name, + inverseJoinColumn.name, + ...Object.keys(joinTable.on || {}), + ]); + + if (isAnyToMany(attribute)) { + query.merge([orderColumnName]); + } else { + query.ignore(); + } + + await query.execute(); } // Delete the previous relations for oneToAny relations @@ -1031,7 +1042,7 @@ const createEntityManager = (db) => { if (attribute.joinTable) { const { joinTable } = attribute; - await deleteRelations({ id, attribute, joinTable, db }, { relsToDelete: 'all' }); + await deleteRelations({ id, attribute, joinTable, db }, { relIdsToDelete: 'all' }); } } }, diff --git a/packages/core/database/lib/entity-manager/regular-relations.js b/packages/core/database/lib/entity-manager/regular-relations.js index 3855687a2e..e6255bd6c7 100644 --- a/packages/core/database/lib/entity-manager/regular-relations.js +++ b/packages/core/database/lib/entity-manager/regular-relations.js @@ -112,11 +112,11 @@ const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relId // INVERSE ORDER UPDATE const deleteRelations = async ( { id, attribute, joinTable, db }, - { relsToNotDelete = [], relsToDelete = [] } + { relIdsToNotDelete = [], relIdsToDelete = [] } ) => { const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; const select = getSelect(joinTable, attribute); - const all = relsToDelete === 'all'; + const all = relIdsToDelete === 'all'; if (isAnyToMany(attribute) || (isBidirectional(attribute) && isManyToAny(attribute))) { let lastId = 0; @@ -128,8 +128,8 @@ const deleteRelations = async ( .where({ [joinColumn.name]: id, id: { $gt: lastId }, - [inverseJoinColumn.name]: { $notIn: relsToNotDelete }, - ...(all ? {} : { [inverseJoinColumn.name]: { $in: relsToDelete } }), + [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete }, + ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }), }) .where(joinTable.on || {}) .orderBy('id') @@ -183,8 +183,8 @@ const deleteRelations = async ( .delete() .where({ [joinColumn.name]: id, - [inverseJoinColumn.name]: { $notIn: relsToNotDelete }, - ...(all ? {} : { [inverseJoinColumn.name]: { $in: relsToDelete } }), + [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete }, + ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }), }) .where(joinTable.on || {}) .execute(); diff --git a/packages/core/database/lib/metadata/index.js b/packages/core/database/lib/metadata/index.js index ee67afd09f..ea3856f1ef 100644 --- a/packages/core/database/lib/metadata/index.js +++ b/packages/core/database/lib/metadata/index.js @@ -142,6 +142,11 @@ const createCompoLinkModelMeta = (baseModelMeta) => { name: `${baseModelMeta.tableName}_entity_fk`, columns: ['entity_id'], }, + { + name: `${baseModelMeta.tableName}_unique`, + columns: ['entity_id', 'component_id', 'field'], + type: 'unique', + }, ], foreignKeys: [ { diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index a592c9be6e..07707e0935 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -444,6 +444,11 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { name: `${joinTableName}_inv_fk`, columns: [inverseJoinColumnName], }, + { + name: `${joinTableName}_unique`, + columns: [joinColumnName, inverseJoinColumnName], + type: 'unique', + }, ], foreignKeys: [ { diff --git a/packages/core/database/lib/query/query-builder.js b/packages/core/database/lib/query/query-builder.js index f9aea4e312..3411b2ba3d 100644 --- a/packages/core/database/lib/query/query-builder.js +++ b/packages/core/database/lib/query/query-builder.js @@ -23,6 +23,9 @@ const createQueryBuilder = (uid, db, initialState = {}) => { offset: null, transaction: null, forUpdate: false, + onConflict: null, + merge: null, + ignore: false, orderBy: [], groupBy: [], increments: [], @@ -69,6 +72,24 @@ const createQueryBuilder = (uid, db, initialState = {}) => { return this; }, + onConflict(args) { + state.onConflict = args; + + return this; + }, + + merge(args) { + state.merge = args; + + return this; + }, + + ignore() { + state.ignore = true; + + return this; + }, + delete() { state.type = 'delete'; @@ -400,6 +421,14 @@ const createQueryBuilder = (uid, db, initialState = {}) => { state.decrements.forEach((decr) => qb.decrement(decr.column, decr.amount)); } + if (state.onConflict) { + if (state.merge) { + qb.onConflict(state.onConflict).merge(state.merge); + } else if (state.ignore) { + qb.onConflict(state.onConflict).ignore(); + } + } + if (state.limit) { qb.limit(state.limit); } From 2cebbc2de498305dc60c181dbce9537fe5f9a6ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Wed, 21 Sep 2022 11:01:06 +0200 Subject: [PATCH 16/28] fix unique index --- packages/core/database/lib/metadata/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/database/lib/metadata/index.js b/packages/core/database/lib/metadata/index.js index ea3856f1ef..f19a361edd 100644 --- a/packages/core/database/lib/metadata/index.js +++ b/packages/core/database/lib/metadata/index.js @@ -144,7 +144,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => { }, { name: `${baseModelMeta.tableName}_unique`, - columns: ['entity_id', 'component_id', 'field'], + columns: ['entity_id', 'component_id', 'field', 'component_type'], type: 'unique', }, ], From 43106732a272106a5f5fd828de4816b56bf96af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Wed, 21 Sep 2022 11:17:12 +0200 Subject: [PATCH 17/28] improve max query in insertRelations --- .../core/database/lib/entity-manager/index.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index e45aef9f8e..d22a5b4cfd 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -525,30 +525,30 @@ const createEntityManager = (db) => { }; }); - // add order value when relevant + // add order value if (isAnyToMany(attribute)) { insert.forEach((rel, idx) => { rel[orderColumnName] = idx + 1; }); } - // add inv_order value when relevant + // add inv_order value if (isBidirectional(attribute) && isManyToAny(attribute)) { - const maxMap = {}; - await Promise.all( - relIdsToadd.map(async (relId) => { - const { max } = await this.createQueryBuilder(joinTable.name) - .max(inverseOrderColumnName) - .where({ [inverseJoinColumn.name]: relId }) - .where(joinTable.on || {}) - .first() - .execute(); + const maxResults = await db + .getConnection() + .select(inverseJoinColumn.name) + .max(inverseOrderColumnName, { as: 'max' }) + .whereIn(inverseJoinColumn.name, relIdsToadd) + .where(joinTable.on || {}) + .groupBy(inverseJoinColumn.name) + .from(joinTable.name); - maxMap[relId] = max; - }) + const maxMap = maxResults.reduce( + (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }), + {} ); insert.forEach((rel) => { - rel[inverseOrderColumnName] = maxMap[rel[inverseJoinColumn.name]] + 1; + rel[inverseOrderColumnName] = (maxMap[rel[inverseJoinColumn.name]] || 0) + 1; }); } From 3ffc748b860228c8a5354c78b9935b5d3aa0a058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Wed, 21 Sep 2022 11:35:47 +0200 Subject: [PATCH 18/28] improve max query in updateRelations - connect --- .../core/database/lib/entity-manager/index.js | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index d22a5b4cfd..caea519418 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -731,13 +731,11 @@ const createEntityManager = (db) => { let relIdsToaddOrMove; if (isPartialUpdate) { - // does not support pivot if (isAnyToOne(attribute)) { cleanRelationData.connect = cleanRelationData.connect.slice(-1); cleanRelationData.disconnect = []; } relIdsToaddOrMove = toIds(cleanRelationData.connect); - // DELETE relations in disconnect const relIdsToDelete = toIds( differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect) ); @@ -770,6 +768,25 @@ const createEntityManager = (db) => { ).max; } + const nonExistingRelsIds = difference( + relIdsToaddOrMove, + map(inverseJoinColumn.name, currentMovingRels) + ); + + const maxResults = await db + .getConnection() + .select(inverseJoinColumn.name) + .max(inverseOrderColumnName, { as: 'max' }) + .whereIn(inverseJoinColumn.name, nonExistingRelsIds) + .where(joinTable.on || {}) + .groupBy(inverseJoinColumn.name) + .from(joinTable.name); + + const maxMap = maxResults.reduce( + (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }), + {} + ); + for (const relToAddOrMove of cleanRelationData.connect) { const currentRel = currentMovingRelsMap[relToAddOrMove.id]; if (currentRel && isAnyToMany(attribute)) { @@ -818,14 +835,7 @@ const createEntityManager = (db) => { } if (isBidirectional(attribute) && isManyToAny(attribute)) { - const { max: reverseMax } = await this.createQueryBuilder(joinTable.name) - .max(inverseOrderColumnName) - .where({ [inverseJoinColumn.name]: relToAddOrMove.id }) - .where(joinTable.on || {}) - .first() - .execute(); - - insert[inverseOrderColumnName] = reverseMax + 1; + insert[inverseOrderColumnName] = (maxMap[relToAddOrMove.id] || 0) + 1; } await this.createQueryBuilder(joinTable.name).insert(insert).execute(); @@ -861,7 +871,7 @@ const createEntityManager = (db) => { // add inv order value if (isBidirectional(attribute) && isManyToAny(attribute)) { const existingRels = await this.createQueryBuilder(joinTable.name) - .select('id') + .select(inverseJoinColumn.name) .where({ [joinColumn.name]: id, [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, @@ -869,7 +879,10 @@ const createEntityManager = (db) => { .where(joinTable.on || {}) .execute(); - const nonExistingRelsIds = difference(relIdsToaddOrMove, map('id', existingRels)); + const nonExistingRelsIds = difference( + relIdsToaddOrMove, + map(inverseJoinColumn.name, existingRels) + ); const maxResults = await db .getConnection() From eb8d79e0c87ce1c05ea4364b356370bea8c4a37c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Thu, 22 Sep 2022 14:38:58 +0200 Subject: [PATCH 19/28] restructure updateRelations - connect --- .../core/database/lib/entity-manager/index.js | 159 +++++++++--------- .../find-existing-relations.test.e2e.js | 1 - 2 files changed, 79 insertions(+), 81 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index caea519418..50d5508d33 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -505,10 +505,6 @@ const createEntityManager = (db) => { const { joinTable } = attribute; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; - const select = [joinColumn.name]; - if (isAnyToMany(attribute)) { - select.push(orderColumnName); - } const relsToAdd = cleanRelationData.set || cleanRelationData.connect; const relIdsToadd = toIds(relsToAdd); @@ -740,25 +736,39 @@ const createEntityManager = (db) => { differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect) ); + if (isEmpty(cleanRelationData.connect)) { + continue; + } + await deleteRelations({ id, attribute, joinTable, db }, { relIdsToDelete }); - // add/move - let max; - const currentMovingRels = await this.createQueryBuilder(joinTable.name) - .select(select) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, - }) - .where(joinTable.on || {}) - .execute(); - const currentMovingRelsMap = currentMovingRels.reduce( - (acc, rel) => Object.assign(acc, { [rel[inverseJoinColumn.name]]: rel }), - {} - ); + // Fetch current relations to handle ordering + let currentMovingRels; + if ( + isAnyToMany(attribute) || + (isBidirectional(attribute) && isManyToAny(attribute)) + ) { + currentMovingRels = await this.createQueryBuilder(joinTable.name) + .select(select) + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: relIdsToaddOrMove }, + }) + .where(joinTable.on || {}) + .execute(); + } + // prepare relations to insert + const insert = cleanRelationData.connect.map((relToAdd) => ({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: relToAdd.id, + ...(joinTable.on || {}), + ...(relToAdd.__pivot || {}), + })); + + // add order value if (isAnyToMany(attribute)) { - max = ( + const orderMax = ( await this.createQueryBuilder(joinTable.name) .max(orderColumnName) .where({ [joinColumn.name]: id }) @@ -766,32 +776,60 @@ const createEntityManager = (db) => { .first() .execute() ).max; + + insert.forEach((row, idx) => { + row[orderColumnName] = orderMax + idx + 1; + }); } - const nonExistingRelsIds = difference( - relIdsToaddOrMove, - map(inverseJoinColumn.name, currentMovingRels) - ); + // add inv order value + if (isBidirectional(attribute) && isManyToAny(attribute)) { + const nonExistingRelsIds = difference( + relIdsToaddOrMove, + map(inverseJoinColumn.name, currentMovingRels) + ); - const maxResults = await db - .getConnection() - .select(inverseJoinColumn.name) - .max(inverseOrderColumnName, { as: 'max' }) - .whereIn(inverseJoinColumn.name, nonExistingRelsIds) - .where(joinTable.on || {}) - .groupBy(inverseJoinColumn.name) - .from(joinTable.name); + const maxResults = await db + .getConnection() + .select(inverseJoinColumn.name) + .max(inverseOrderColumnName, { as: 'max' }) + .whereIn(inverseJoinColumn.name, nonExistingRelsIds) + .where(joinTable.on || {}) + .groupBy(inverseJoinColumn.name) + .from(joinTable.name); - const maxMap = maxResults.reduce( - (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }), - {} - ); + const maxMap = maxResults.reduce( + (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }), + {} + ); - for (const relToAddOrMove of cleanRelationData.connect) { - const currentRel = currentMovingRelsMap[relToAddOrMove.id]; - if (currentRel && isAnyToMany(attribute)) { - const currentOrderIsNull = currentRel[orderColumnName] === null; - if (!currentOrderIsNull) { + insert.forEach((row) => { + row[inverseOrderColumnName] = (maxMap[row[inverseJoinColumn.name]] || 0) + 1; + }); + } + + // insert rows + const query = this.createQueryBuilder(joinTable.name) + .insert(insert) + .onConflict([ + joinColumn.name, + inverseJoinColumn.name, + ...Object.keys(cleanRelationData.connect[0].__pivot || {}), + ]); + + if (isAnyToMany(attribute)) { + query.merge([orderColumnName]); + } else { + query.ignore(); + } + + 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({ @@ -800,46 +838,7 @@ const createEntityManager = (db) => { }) .where(joinTable.on || {}) .execute(); - - currentMovingRels.forEach((rel) => { - if (rel[orderColumnName] > currentRel[orderColumnName]) { - rel[orderColumnName] -= 1; - } - }); } - - if (currentOrderIsNull) { - max += 1; - } - - await this.createQueryBuilder(joinTable.name) - .update({ - [orderColumnName]: max, - }) - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: relToAddOrMove.id, - }) - .where(joinTable.on || {}) - .execute(); - } else if (!currentRel) { - const insert = { - [joinColumn.name]: id, - [inverseJoinColumn.name]: relToAddOrMove.id, - ...(relToAddOrMove.__pivot || {}), - ...(joinTable.on || {}), - }; - - if (isAnyToMany(attribute)) { - insert[orderColumnName] = max + 1; - } - - if (isBidirectional(attribute) && isManyToAny(attribute)) { - insert[inverseOrderColumnName] = (maxMap[relToAddOrMove.id] || 0) + 1; - } - - await this.createQueryBuilder(joinTable.name).insert(insert).execute(); - max += 1; } } } else { @@ -909,7 +908,7 @@ const createEntityManager = (db) => { .onConflict([ joinColumn.name, inverseJoinColumn.name, - ...Object.keys(joinTable.on || {}), + ...Object.keys(cleanRelationData.set[0].__pivot || {}), ]); if (isAnyToMany(attribute)) { diff --git a/packages/plugins/i18n/tests/content-manager/find-existing-relations.test.e2e.js b/packages/plugins/i18n/tests/content-manager/find-existing-relations.test.e2e.js index 77164f17f7..eee6e53daf 100644 --- a/packages/plugins/i18n/tests/content-manager/find-existing-relations.test.e2e.js +++ b/packages/plugins/i18n/tests/content-manager/find-existing-relations.test.e2e.js @@ -99,7 +99,6 @@ describe('i18n - Find existing relations', () => { rq = await createAuthRequest({ strapi }); data.shops = await builder.sanitizedFixturesFor(shopModel.singularName, strapi); - console.log('data.shops', data.shops); data.products = await builder.sanitizedFixturesFor(productModel.singularName, strapi); }); From 983091031224bd6279cc20ca69c6e0d947d1a923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Thu, 22 Sep 2022 17:42:30 +0200 Subject: [PATCH 20/28] fix morphToMany order --- packages/core/database/lib/entity-manager/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 50d5508d33..63ca0c3020 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -438,12 +438,13 @@ const createEntityManager = (db) => { const { idColumn, typeColumn, typeField = '__type' } = morphColumn; - const rows = cleanRelationData.set.map((data) => ({ + const rows = cleanRelationData.set.map((data, idx) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], ...(joinTable.on || {}), ...(data.__pivot || {}), + order: idx + 1, })); if (isEmpty(rows)) { @@ -657,12 +658,13 @@ const createEntityManager = (db) => { }) .execute(); - const rows = cleanRelationData.set.map((data) => ({ + const rows = cleanRelationData.set.map((data, idx) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, [typeColumn.name]: data[typeField], ...(joinTable.on || {}), ...(data.__pivot || {}), + order: idx + 1, })); if (isEmpty(rows)) { From a56879e12bcd66f59d80f39780c9397150f74cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Mon, 26 Sep 2022 16:22:22 +0200 Subject: [PATCH 21/28] implement cleanOrder function --- .../core/database/lib/entity-manager/index.js | 65 ++--- .../lib/entity-manager/regular-relations.js | 246 +++++++++--------- packages/core/database/lib/metadata/index.js | 6 +- .../core/database/lib/metadata/relations.js | 3 + 4 files changed, 157 insertions(+), 163 deletions(-) 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)) { From 8e5543b21db05451e7ce3033649633ec0d985219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 27 Sep 2022 11:56:46 +0200 Subject: [PATCH 22/28] fix cleanOrder + use hasOrderColumn and hasInverseOrderColumn --- .../core/database/lib/entity-manager/index.js | 57 +++--- .../lib/entity-manager/regular-relations.js | 167 +++++++++++------- .../core/database/lib/metadata/relations.js | 5 + 3 files changed, 139 insertions(+), 90 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index b798187878..c133f72039 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -24,7 +24,12 @@ const { createField } = require('../fields'); const { createQueryBuilder } = require('../query'); const { createRepository } = require('./entity-repository'); const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations'); -const { isBidirectional, isManyToAny, isAnyToOne, isAnyToMany } = require('../metadata/relations'); +const { + isBidirectional, + isAnyToOne, + hasOrderColumn, + hasInverseOrderColumn, +} = require('../metadata/relations'); const { deletePreviousOneToAnyRelations, deletePreviousAnyToOneRelations, @@ -511,7 +516,7 @@ const createEntityManager = (db) => { const relsToAdd = cleanRelationData.set || cleanRelationData.connect; const relIdsToadd = toIds(relsToAdd); - await deletePreviousOneToAnyRelations({ id, attribute, joinTable, relIdsToadd, db }); + await deletePreviousOneToAnyRelations({ id, attribute, relIdsToadd, db }); // prepare new relations to insert const insert = relsToAdd.map((data) => { @@ -524,13 +529,13 @@ const createEntityManager = (db) => { }); // add order value - if (isAnyToMany(attribute)) { + if (hasOrderColumn(attribute)) { insert.forEach((rel, idx) => { rel[orderColumnName] = idx + 1; }); } // add inv_order value - if (isBidirectional(attribute) && isManyToAny(attribute)) { + if (hasInverseOrderColumn(attribute)) { const maxResults = await db .getConnection() .select(inverseJoinColumn.name) @@ -715,16 +720,16 @@ const createEntityManager = (db) => { const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; const select = [joinColumn.name, inverseJoinColumn.name]; - if (isAnyToMany(attribute)) { + if (hasOrderColumn(attribute)) { select.push(orderColumnName); } - if (isBidirectional(attribute) && isManyToAny(attribute)) { + if (hasInverseOrderColumn(attribute)) { select.push(inverseOrderColumnName); } // only delete relations if (isNull(cleanRelationData.set)) { - await deleteRelations({ id, attribute, joinTable, db }, { relIdsToDelete: 'all' }); + await deleteRelations({ id, attribute, db, relIdsToDelete: 'all' }); } else { const isPartialUpdate = !has('set', cleanRelationData); let relIdsToaddOrMove; @@ -742,14 +747,11 @@ const createEntityManager = (db) => { continue; } - await deleteRelations({ id, attribute, joinTable, db }, { relIdsToDelete }); + await deleteRelations({ id, attribute, db, relIdsToDelete }); // Fetch current relations to handle ordering let currentMovingRels; - if ( - isAnyToMany(attribute) || - (isBidirectional(attribute) && isManyToAny(attribute)) - ) { + if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) { currentMovingRels = await this.createQueryBuilder(joinTable.name) .select(select) .where({ @@ -769,7 +771,7 @@ const createEntityManager = (db) => { })); // add order value - if (isAnyToMany(attribute)) { + if (hasOrderColumn(attribute)) { const orderMax = ( await this.createQueryBuilder(joinTable.name) .max(orderColumnName) @@ -785,7 +787,7 @@ const createEntityManager = (db) => { } // add inv order value - if (isBidirectional(attribute) && isManyToAny(attribute)) { + if (hasInverseOrderColumn(attribute)) { const nonExistingRelsIds = difference( relIdsToaddOrMove, map(inverseJoinColumn.name, currentMovingRels) @@ -815,7 +817,7 @@ const createEntityManager = (db) => { .insert(insert) .onConflict(joinTable.pivotColumns); - if (isAnyToMany(attribute)) { + if (hasOrderColumn(attribute)) { query.merge([orderColumnName]); } else { query.ignore(); @@ -824,17 +826,20 @@ const createEntityManager = (db) => { await query.execute(); // remove gap between orders - await cleanOrderColumns({ joinTable, attribute, db, id }); + await cleanOrderColumns({ attribute, db, id }); } else { if (isAnyToOne(attribute)) { cleanRelationData.set = cleanRelationData.set.slice(-1); } // overwrite all relations relIdsToaddOrMove = toIds(cleanRelationData.set); - await deleteRelations( - { id, attribute, joinTable, db }, - { relIdsToDelete: 'all', relIdsToNotDelete: relIdsToaddOrMove } - ); + await deleteRelations({ + id, + attribute, + db, + relIdsToDelete: 'all', + relIdsToNotDelete: relIdsToaddOrMove, + }); if (isEmpty(cleanRelationData.set)) { continue; @@ -848,14 +853,14 @@ const createEntityManager = (db) => { })); // add order value - if (isAnyToMany(attribute)) { + if (hasOrderColumn(attribute)) { insert.forEach((row, idx) => { row[orderColumnName] = idx + 1; }); } // add inv order value - if (isBidirectional(attribute) && isManyToAny(attribute)) { + if (hasInverseOrderColumn(attribute)) { const existingRels = await this.createQueryBuilder(joinTable.name) .select(inverseJoinColumn.name) .where({ @@ -894,7 +899,7 @@ const createEntityManager = (db) => { .insert(insert) .onConflict(joinTable.pivotColumns); - if (isAnyToMany(attribute)) { + if (hasOrderColumn(attribute)) { query.merge([orderColumnName]); } else { query.ignore(); @@ -908,7 +913,6 @@ const createEntityManager = (db) => { await deletePreviousOneToAnyRelations({ id, attribute, - joinTable, relIdsToadd: relIdsToaddOrMove, db, }); @@ -917,7 +921,6 @@ const createEntityManager = (db) => { await deletePreviousAnyToOneRelations({ id, attribute, - joinTable, relIdToadd: relIdsToaddOrMove[0], db, }); @@ -1037,9 +1040,7 @@ const createEntityManager = (db) => { } if (attribute.joinTable) { - const { joinTable } = attribute; - - await deleteRelations({ id, attribute, joinTable, db }, { relIdsToDelete: 'all' }); + await deleteRelations({ id, attribute, db, relIdsToDelete: 'all' }); } } }, diff --git a/packages/core/database/lib/entity-manager/regular-relations.js b/packages/core/database/lib/entity-manager/regular-relations.js index ac11f10449..52845afbd9 100644 --- a/packages/core/database/lib/entity-manager/regular-relations.js +++ b/packages/core/database/lib/entity-manager/regular-relations.js @@ -6,11 +6,21 @@ const { isOneToAny, isManyToAny, isAnyToOne, - isAnyToMany, + hasOrderColumn, + hasInverseOrderColumn, } = require('../metadata/relations'); const { createQueryBuilder } = require('../query'); -const deletePreviousOneToAnyRelations = async ({ id, attribute, joinTable, relIdsToadd, db }) => { +/** + * If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed. + * @param {Object} params + * @param {string} params.id - entity id on which the relations for entities relIdsToadd are created + * @param {string} params.attribute - attribute of the relation + * @param {string} params.inverseRelIds - entity ids of the inverse side for which the current relations will be deleted + * @param {string} params.db - database instance + */ +const deletePreviousOneToAnyRelations = async ({ id, attribute, relIdsToadd, db }) => { + const { joinTable } = attribute; const { joinColumn, inverseJoinColumn } = joinTable; // need to delete the previous relations for oneToAny relations @@ -25,16 +35,27 @@ const deletePreviousOneToAnyRelations = async ({ id, attribute, joinTable, relId .where(joinTable.on || {}) .execute(); - await cleanOrderColumns({ joinTable, attribute, db, inverseRelIds: relIdsToadd }); + await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd }); } }; -const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relIdToadd, db }) => { +/** + * If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed. + * @param {Object} params + * @param {string} params.id - entity id on which the relation for entity relIdToadd is created + * @param {string} params.attribute - attribute of the relation + * @param {string} params.relIdToadd - entity id of the new relation + * @param {string} params.db - database instance + */ +const deletePreviousAnyToOneRelations = async ({ id, attribute, relIdToadd, db }) => { + const { joinTable } = attribute; 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) + + // handling manyToOne if (isManyToAny(attribute)) { // if the database integrity was not broken relsToDelete is supposed to be of length 1 const relsToDelete = await createQueryBuilder(joinTable.name, db) @@ -48,7 +69,6 @@ const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relId const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete); - // delete previous anyToOne relations await createQueryBuilder(joinTable.name, db) .delete() .where({ @@ -58,9 +78,10 @@ const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relId .where(joinTable.on || {}) .execute(); - await cleanOrderColumns({ joinTable, attribute, db, inverseRelIds: relIdsToDelete }); + await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete }); + + // handling oneToOne } else { - // delete previous anyToOne relations await createQueryBuilder(joinTable.name, db) .delete() .where({ @@ -73,15 +94,27 @@ const deletePreviousAnyToOneRelations = async ({ id, attribute, joinTable, relId } }; -// INVERSE ORDER UPDATE -const deleteRelations = async ( - { id, attribute, joinTable, db }, - { relIdsToNotDelete = [], relIdsToDelete = [] } -) => { +/** + * Delete all or some relations of entity field + * @param {Object} params + * @param {string} params.id - entity id for which the relations will be deleted + * @param {string} params.attribute - attribute of the relation + * @param {string} params.db - database instance + * @param {string} params.relIdsToDelete - ids of entities to remove from the relations. Also accepts 'all' + * @param {string} params.relIdsToNotDelete - ids of entities to not remove from the relation when relIdsToDelete equals 'all' + */ +const deleteRelations = async ({ + id, + attribute, + db, + relIdsToNotDelete = [], + relIdsToDelete = [], +}) => { + const { joinTable } = attribute; const { joinColumn, inverseJoinColumn } = joinTable; const all = relIdsToDelete === 'all'; - if (isAnyToMany(attribute) || (isBidirectional(attribute) && isManyToAny(attribute))) { + if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) { let lastId = 0; let done = false; const batchSize = 100; @@ -112,7 +145,7 @@ const deleteRelations = async ( .where(joinTable.on || {}) .execute(); - await cleanOrderColumns({ joinTable, attribute, db, id, inverseRelIds: batchIds }); + await cleanOrderColumns({ attribute, db, id, inverseRelIds: batchIds }); } } else { await createQueryBuilder(joinTable.name, db) @@ -130,69 +163,79 @@ const deleteRelations = async ( /** * 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 + * @param {string} params.id - entity id for which the clean will be done + * @param {string} params.attribute - attribute of the relation + * @param {string} params.db - database instance + * @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 }) => { +const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds }) => { if ( - !(isAnyToMany(attribute) && id) && - !(isBidirectional(attribute) && isManyToAny(attribute) && !isEmpty(inverseRelIds)) + !(hasOrderColumn(attribute) && id) && + !(hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) ) { return; } + const { joinTable } = attribute; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable; - const knex = db.getConnection(); - const update = {}; - const subQuery = knex(joinTable.name).select('id'); + const update = []; + const updateBinding = []; + const select = ['??']; + const selectBinding = ['id']; + const where = []; + const whereBinding = []; - 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 (hasOrderColumn(attribute) && id) { + update.push('?? = t.src_order'); + updateBinding.push(orderColumnName); + select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order'); + selectBinding.push(joinColumn.name, orderColumnName); + where.push('?? = ?'); + whereBinding.push(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); + if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) { + update.push('?? = t.inv_order'); + updateBinding.push(inverseOrderColumnName); + select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order'); + selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName); + where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`); + whereBinding.push(inverseJoinColumn.name, ...inverseRelIds); } - await knex(joinTable.name) - .update(update) - .from(subQuery) - .where('t.id', knex.raw('??.id', joinTable.name)); - + // raw query as knex doesn't allow updating from a subquery + // https://github.com/knex/knex/issues/2504 /* - `UPDATE :joinTable: - SET :orderColumn: = t.order, :inverseOrderColumn: = t.inv_order + `UPDATE :joinTable: + SET :orderColumn: = t.src_order, :inverseOrderColumn: = t.inv_order + FROM ( + SELECT + id, + ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_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`, +*/ + await db.getConnection().raw( + `UPDATE ?? + SET ${update.join(', ')} 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) + SELECT ${select.join(', ')} + FROM ?? + WHERE ${where.join(' OR ')} ) AS t - WHERE t.id = :joinTable:.id`, - */ + WHERE t.id = ??.id`, + [ + joinTable.name, + ...updateBinding, + ...selectBinding, + joinTable.name, + ...whereBinding, + joinTable.name, + ] + ); }; module.exports = { diff --git a/packages/core/database/lib/metadata/relations.js b/packages/core/database/lib/metadata/relations.js index 3d2400f20b..f07955b70f 100644 --- a/packages/core/database/lib/metadata/relations.js +++ b/packages/core/database/lib/metadata/relations.js @@ -547,6 +547,9 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => { } }; +const hasOrderColumn = (attribute) => isAnyToMany(attribute); +const hasInverseOrderColumn = (attribute) => isBidirectional(attribute) && isManyToAny(attribute); + module.exports = { createRelation, @@ -555,4 +558,6 @@ module.exports = { isManyToAny, isAnyToOne, isAnyToMany, + hasOrderColumn, + hasInverseOrderColumn, }; From 3acb08cd5bafcd758d2239eff701db28443d8765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 27 Sep 2022 12:07:59 +0200 Subject: [PATCH 23/28] fix onConflict for components --- packages/core/database/lib/metadata/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/database/lib/metadata/index.js b/packages/core/database/lib/metadata/index.js index 96f6103c73..f68f1b1bd6 100644 --- a/packages/core/database/lib/metadata/index.js +++ b/packages/core/database/lib/metadata/index.js @@ -215,7 +215,7 @@ const createComponent = (attributeName, attribute, meta) => { orderBy: { order: 'asc', }, - pivotColumns: ['entity_id', 'component_id', 'field'], + pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'], }, }); }; From 8248458a1fd94abe128f3263a143d58e060d2cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 27 Sep 2022 15:11:11 +0200 Subject: [PATCH 24/28] fix cleanOrder query for mysql --- .../lib/entity-manager/regular-relations.js | 76 +++++++++++-------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/core/database/lib/entity-manager/regular-relations.js b/packages/core/database/lib/entity-manager/regular-relations.js index 52845afbd9..b0c7153b4e 100644 --- a/packages/core/database/lib/entity-manager/regular-relations.js +++ b/packages/core/database/lib/entity-manager/regular-relations.js @@ -186,7 +186,7 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds }) => { const whereBinding = []; if (hasOrderColumn(attribute) && id) { - update.push('?? = t.src_order'); + update.push('?? = b.src_order'); updateBinding.push(orderColumnName); select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order'); selectBinding.push(joinColumn.name, orderColumnName); @@ -195,7 +195,7 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds }) => { } if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) { - update.push('?? = t.inv_order'); + update.push('?? = b.inv_order'); updateBinding.push(inverseOrderColumnName); select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order'); selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName); @@ -205,37 +205,47 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds }) => { // raw query as knex doesn't allow updating from a subquery // https://github.com/knex/knex/issues/2504 - /* - `UPDATE :joinTable: - SET :orderColumn: = t.src_order, :inverseOrderColumn: = t.inv_order - FROM ( - SELECT - id, - ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_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`, -*/ - await db.getConnection().raw( - `UPDATE ?? - SET ${update.join(', ')} - FROM ( - SELECT ${select.join(', ')} - FROM ?? - WHERE ${where.join(' OR ')} - ) AS t - WHERE t.id = ??.id`, - [ - joinTable.name, - ...updateBinding, - ...selectBinding, - joinTable.name, - ...whereBinding, - joinTable.name, - ] - ); + switch (strapi.db.dialect.client) { + case 'mysql': + await db.getConnection().raw( + `UPDATE + ?? as a, + ( + SELECT ${select.join(', ')} + FROM ?? + WHERE ${where.join(' OR ')} + ) AS b + SET ${update.join(', ')} + WHERE b.id = a.id`, + [joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding] + ); + break; + default: + await db.getConnection().raw( + `UPDATE ?? as a + SET ${update.join(', ')} + FROM ( + SELECT ${select.join(', ')} + FROM ?? + WHERE ${where.join(' OR ')} + ) AS b + WHERE b.id = a.id`, + [joinTable.name, ...updateBinding, ...selectBinding, joinTable.name, ...whereBinding] + ); + /* + `UPDATE :joinTable: as a + SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order + FROM ( + SELECT + id, + ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order, + ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order + FROM :joinTable: + WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds) + ) AS b + WHERE b.id = a.id`, + */ + } }; module.exports = { From bf16d074637bc14f13875115339c66a803baddba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Tue, 27 Sep 2022 16:07:00 +0200 Subject: [PATCH 25/28] throw when deletePreviousAnyToOneRelations and deletePreviousOneToAnyRelations are called for non compatible relations --- .../core/database/lib/entity-manager/index.js | 11 ++- .../lib/entity-manager/regular-relations.js | 99 ++++++++++--------- 2 files changed, 58 insertions(+), 52 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index c133f72039..42d1711503 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -27,6 +27,7 @@ const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morp const { isBidirectional, isAnyToOne, + isOneToAny, hasOrderColumn, hasInverseOrderColumn, } = require('../metadata/relations'); @@ -516,7 +517,9 @@ const createEntityManager = (db) => { const relsToAdd = cleanRelationData.set || cleanRelationData.connect; const relIdsToadd = toIds(relsToAdd); - await deletePreviousOneToAnyRelations({ id, attribute, relIdsToadd, db }); + if (isBidirectional(attribute) && isOneToAny(attribute)) { + await deletePreviousOneToAnyRelations({ id, attribute, relIdsToadd, db }); + } // prepare new relations to insert const insert = relsToAdd.map((data) => { @@ -909,15 +912,17 @@ const createEntityManager = (db) => { } // Delete the previous relations for oneToAny relations - if (!isEmpty(relIdsToaddOrMove)) { + if (isBidirectional(attribute) && isOneToAny(attribute)) { await deletePreviousOneToAnyRelations({ id, attribute, relIdsToadd: relIdsToaddOrMove, db, }); + } - // Delete the previous relations for anyToOne relations + // Delete the previous relations for anyToOne relations + if (isBidirectional(attribute) && isAnyToOne(attribute)) { await deletePreviousAnyToOneRelations({ id, attribute, diff --git a/packages/core/database/lib/entity-manager/regular-relations.js b/packages/core/database/lib/entity-manager/regular-relations.js index b0c7153b4e..45ebb85b0f 100644 --- a/packages/core/database/lib/entity-manager/regular-relations.js +++ b/packages/core/database/lib/entity-manager/regular-relations.js @@ -20,23 +20,24 @@ const { createQueryBuilder } = require('../query'); * @param {string} params.db - database instance */ const deletePreviousOneToAnyRelations = async ({ id, attribute, relIdsToadd, db }) => { + if (!(isBidirectional(attribute) && isOneToAny(attribute))) { + throw new Error( + 'deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations' + ); + } const { joinTable } = attribute; const { joinColumn, inverseJoinColumn } = joinTable; - // need to delete the previous relations for oneToAny relations - if (isBidirectional(attribute) && isOneToAny(attribute)) { - // delete previous oneToAny relations - await createQueryBuilder(joinTable.name, db) - .delete() - .where({ - [inverseJoinColumn.name]: relIdsToadd, - [joinColumn.name]: { $ne: id }, - }) - .where(joinTable.on || {}) - .execute(); + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [inverseJoinColumn.name]: relIdsToadd, + [joinColumn.name]: { $ne: id }, + }) + .where(joinTable.on || {}) + .execute(); - await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd }); - } + await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd }); }; /** @@ -51,46 +52,46 @@ const deletePreviousAnyToOneRelations = async ({ id, attribute, relIdToadd, db } const { joinTable } = attribute; 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 (!(isBidirectional(attribute) && isAnyToOne(attribute))) { + throw new Error( + 'deletePreviousAnyToOneRelations can only be called for bidirectional anyToOne relations' + ); + } + // handling manyToOne + if (isManyToAny(attribute)) { + // 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]: { $ne: relIdToadd }, + }) + .where(joinTable.on || {}) + .execute(); - // handling manyToOne - if (isManyToAny(attribute)) { - // 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]: { $ne: relIdToadd }, - }) - .where(joinTable.on || {}) - .execute(); + const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete); - const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete); + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $in: relIdsToDelete }, + }) + .where(joinTable.on || {}) + .execute(); - await createQueryBuilder(joinTable.name, db) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $in: relIdsToDelete }, - }) - .where(joinTable.on || {}) - .execute(); + await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete }); - await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete }); - - // handling oneToOne - } else { - await createQueryBuilder(joinTable.name, db) - .delete() - .where({ - [joinColumn.name]: id, - [inverseJoinColumn.name]: { $ne: relIdToadd }, - }) - .where(joinTable.on || {}) - .execute(); - } + // handling oneToOne + } else { + await createQueryBuilder(joinTable.name, db) + .delete() + .where({ + [joinColumn.name]: id, + [inverseJoinColumn.name]: { $ne: relIdToadd }, + }) + .where(joinTable.on || {}) + .execute(); } }; From 3e60852fa0a4c6509b0fec16128a6499815b5872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Wed, 28 Sep 2022 13:28:02 +0200 Subject: [PATCH 26/28] fix partial update delete --- packages/core/database/lib/entity-manager/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 42d1711503..107f47a129 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -746,12 +746,14 @@ const createEntityManager = (db) => { differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect) ); + if (!isEmpty(relIdsToDelete)) { + await deleteRelations({ id, attribute, db, relIdsToDelete }); + } + if (isEmpty(cleanRelationData.connect)) { continue; } - await deleteRelations({ id, attribute, db, relIdsToDelete }); - // Fetch current relations to handle ordering let currentMovingRels; if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) { From 9edb46c3103f00ebfbb8cf12edfe5ca003c6add3 Mon Sep 17 00:00:00 2001 From: Marc-Roig Date: Fri, 30 Sep 2022 10:47:32 +0200 Subject: [PATCH 27/28] check if set is empty --- .../core/database/lib/entity-manager/index.js | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/core/database/lib/entity-manager/index.js b/packages/core/database/lib/entity-manager/index.js index 107f47a129..9248fb730d 100644 --- a/packages/core/database/lib/entity-manager/index.js +++ b/packages/core/database/lib/entity-manager/index.js @@ -416,6 +416,10 @@ const createEntityManager = (db) => { const { idColumn, typeColumn } = morphColumn; + if (isEmpty(cleanRelationData.set)) { + continue; + } + const rows = cleanRelationData.set.map((data, idx) => { return { [joinColumn.name]: data.id, @@ -428,10 +432,6 @@ const createEntityManager = (db) => { }; }); - if (isEmpty(rows)) { - continue; - } - await this.createQueryBuilder(joinTable.name).insert(rows).execute(); } @@ -445,6 +445,10 @@ const createEntityManager = (db) => { const { idColumn, typeColumn, typeField = '__type' } = morphColumn; + if (isEmpty(cleanRelationData.set)) { + continue; + } + const rows = cleanRelationData.set.map((data, idx) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, @@ -454,10 +458,6 @@ const createEntityManager = (db) => { order: idx + 1, })); - if (isEmpty(rows)) { - continue; - } - // delete previous relations await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { uid, @@ -628,6 +628,10 @@ const createEntityManager = (db) => { }) .execute(); + if (isEmpty(cleanRelationData.set)) { + continue; + } + const rows = cleanRelationData.set.map((data, idx) => ({ [joinColumn.name]: data.id, [idColumn.name]: id, @@ -638,10 +642,6 @@ const createEntityManager = (db) => { field: attributeName, })); - if (isEmpty(rows)) { - continue; - } - await this.createQueryBuilder(joinTable.name).insert(rows).execute(); } @@ -667,6 +667,10 @@ const createEntityManager = (db) => { }) .execute(); + if (isEmpty(cleanRelationData.set)) { + continue; + } + const rows = cleanRelationData.set.map((data, idx) => ({ [joinColumn.name]: id, [idColumn.name]: data.id, @@ -676,10 +680,6 @@ const createEntityManager = (db) => { order: idx + 1, })); - if (isEmpty(rows)) { - continue; - } - // delete previous relations await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { uid, From da0f17be754623d13674bb0e251a13b11e0c50fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20No=C3=ABl?= Date: Fri, 30 Sep 2022 15:38:49 +0200 Subject: [PATCH 28/28] add idsToInclude --- .../content-manager/server/controllers/relations.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/core/content-manager/server/controllers/relations.js b/packages/core/content-manager/server/controllers/relations.js index c51c407343..eb0ece9ced 100644 --- a/packages/core/content-manager/server/controllers/relations.js +++ b/packages/core/content-manager/server/controllers/relations.js @@ -24,7 +24,7 @@ module.exports = { await validateFindAvailable(ctx.request.query); - const { component, entityId, idsToOmit, _q, ...query } = ctx.request.query; + const { component, entityId, idsToOmit, idsToInclude, _q, ...query } = ctx.request.query; const sourceModelUid = component || model; @@ -103,8 +103,17 @@ module.exports = { const alias = subQuery.getAlias(); + const where = { + id: entityId, + [`${alias}.id`]: { $notNull: true }, + }; + + if (!isEmpty(idsToInclude)) { + where[`${alias}.id`].$notIn = idsToInclude; + } + const knexSubQuery = subQuery - .where({ id: entityId, [`${alias}.id`]: { $notNull: true } }) + .where(where) .join({ alias, targetField }) .select(`${alias}.id`) .getKnexQuery();