mirror of
https://github.com/strapi/strapi.git
synced 2025-10-13 09:03:25 +00:00
Merge pull request #14327 from strapi/relations-main-view/full-relation-update
Add partial relation update
This commit is contained in:
commit
76665ae056
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -12,20 +12,38 @@ const {
|
||||
isEmpty,
|
||||
isArray,
|
||||
isNull,
|
||||
uniqWith,
|
||||
isEqual,
|
||||
differenceWith,
|
||||
isNumber,
|
||||
map,
|
||||
difference,
|
||||
} = 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,
|
||||
isAnyToOne,
|
||||
isOneToAny,
|
||||
hasOrderColumn,
|
||||
hasInverseOrderColumn,
|
||||
} = require('../metadata/relations');
|
||||
const {
|
||||
deletePreviousOneToAnyRelations,
|
||||
deletePreviousAnyToOneRelations,
|
||||
deleteRelations,
|
||||
cleanOrderColumns,
|
||||
} = require('./regular-relations');
|
||||
|
||||
const toId = (value) => value.id || value;
|
||||
const toIds = (value) => castArray(value || []).map(toId);
|
||||
|
||||
const isValidId = (value) => isString(value) || isInteger(value);
|
||||
const toAssocs = (data) => {
|
||||
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
|
||||
@ -40,6 +58,26 @@ const toAssocs = (data) => {
|
||||
|
||||
return datum;
|
||||
});
|
||||
return uniqWith(isEqual, array);
|
||||
};
|
||||
|
||||
const toAssocs = (data) => {
|
||||
if (isArray(data) || isString(data) || isNumber(data) || isNull(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 } = {}) => {
|
||||
@ -355,6 +393,8 @@ const createEntityManager = (db) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
const cleanRelationData = toAssocs(data[attributeName]);
|
||||
|
||||
if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
|
||||
const { target, morphBy } = attribute;
|
||||
|
||||
@ -364,9 +404,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;
|
||||
@ -374,7 +416,11 @@ const createEntityManager = (db) => {
|
||||
|
||||
const { idColumn, typeColumn } = morphColumn;
|
||||
|
||||
const rows = toAssocs(data[attributeName]).map((data, idx) => {
|
||||
if (isEmpty(cleanRelationData.set)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rows = cleanRelationData.set.map((data, idx) => {
|
||||
return {
|
||||
[joinColumn.name]: data.id,
|
||||
[idColumn.name]: id,
|
||||
@ -386,10 +432,6 @@ const createEntityManager = (db) => {
|
||||
};
|
||||
});
|
||||
|
||||
if (isEmpty(rows)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.createQueryBuilder(joinTable.name).insert(rows).execute();
|
||||
}
|
||||
|
||||
@ -403,18 +445,19 @@ const createEntityManager = (db) => {
|
||||
|
||||
const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
|
||||
|
||||
const rows = toAssocs(data[attributeName]).map((data) => ({
|
||||
if (isEmpty(cleanRelationData.set)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// delete previous relations
|
||||
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
|
||||
uid,
|
||||
@ -429,13 +472,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();
|
||||
}
|
||||
@ -449,6 +493,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 })
|
||||
@ -458,7 +503,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();
|
||||
}
|
||||
|
||||
@ -466,17 +511,18 @@ 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(joinTable.on || {})
|
||||
.execute();
|
||||
const relsToAdd = cleanRelationData.set || cleanRelationData.connect;
|
||||
const relIdsToadd = toIds(relsToAdd);
|
||||
|
||||
if (isBidirectional(attribute) && isOneToAny(attribute)) {
|
||||
await deletePreviousOneToAnyRelations({ id, attribute, relIdsToadd, db });
|
||||
}
|
||||
|
||||
const insert = toAssocs(data[attributeName]).map((data) => {
|
||||
// prepare new relations to insert
|
||||
const insert = relsToAdd.map((data) => {
|
||||
return {
|
||||
[joinColumn.name]: id,
|
||||
[inverseJoinColumn.name]: data.id,
|
||||
@ -485,11 +531,38 @@ const createEntityManager = (db) => {
|
||||
};
|
||||
});
|
||||
|
||||
// if there is nothing to insert
|
||||
// add order value
|
||||
if (hasOrderColumn(attribute)) {
|
||||
insert.forEach((rel, idx) => {
|
||||
rel[orderColumnName] = idx + 1;
|
||||
});
|
||||
}
|
||||
// add inv_order value
|
||||
if (hasInverseOrderColumn(attribute)) {
|
||||
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);
|
||||
|
||||
const maxMap = maxResults.reduce(
|
||||
(acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }),
|
||||
{}
|
||||
);
|
||||
|
||||
insert.forEach((rel) => {
|
||||
rel[inverseOrderColumnName] = (maxMap[rel[inverseJoinColumn.name]] || 0) + 1;
|
||||
});
|
||||
}
|
||||
|
||||
if (insert.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// insert new relations
|
||||
await this.createQueryBuilder(joinTable.name).insert(insert).execute();
|
||||
}
|
||||
}
|
||||
@ -514,6 +587,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;
|
||||
@ -531,10 +605,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') {
|
||||
@ -553,7 +628,11 @@ const createEntityManager = (db) => {
|
||||
})
|
||||
.execute();
|
||||
|
||||
const rows = toAssocs(data[attributeName]).map((data, idx) => ({
|
||||
if (isEmpty(cleanRelationData.set)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rows = cleanRelationData.set.map((data, idx) => ({
|
||||
[joinColumn.name]: data.id,
|
||||
[idColumn.name]: id,
|
||||
[typeColumn.name]: uid,
|
||||
@ -563,10 +642,6 @@ const createEntityManager = (db) => {
|
||||
field: attributeName,
|
||||
}));
|
||||
|
||||
if (isEmpty(rows)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.createQueryBuilder(joinTable.name).insert(rows).execute();
|
||||
}
|
||||
|
||||
@ -592,18 +667,19 @@ const createEntityManager = (db) => {
|
||||
})
|
||||
.execute();
|
||||
|
||||
const rows = toAssocs(data[attributeName]).map((data) => ({
|
||||
if (isEmpty(cleanRelationData.set)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// delete previous relations
|
||||
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
|
||||
uid,
|
||||
@ -633,10 +709,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();
|
||||
}
|
||||
@ -644,42 +720,218 @@ const createEntityManager = (db) => {
|
||||
|
||||
if (attribute.joinTable) {
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn } = joinTable;
|
||||
|
||||
// clear previous associations in the joinTable
|
||||
await this.createQueryBuilder(joinTable.name)
|
||||
.delete()
|
||||
.where({ [joinColumn.name]: id })
|
||||
.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();
|
||||
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } =
|
||||
joinTable;
|
||||
const select = [joinColumn.name, inverseJoinColumn.name];
|
||||
if (hasOrderColumn(attribute)) {
|
||||
select.push(orderColumnName);
|
||||
}
|
||||
if (hasInverseOrderColumn(attribute)) {
|
||||
select.push(inverseOrderColumnName);
|
||||
}
|
||||
|
||||
if (!isNull(data[attributeName])) {
|
||||
const insert = toAssocs(data[attributeName]).map((data) => {
|
||||
return {
|
||||
[joinColumn.name]: id,
|
||||
[inverseJoinColumn.name]: data.id,
|
||||
...(joinTable.on || {}),
|
||||
...(data.__pivot || {}),
|
||||
};
|
||||
});
|
||||
// only delete relations
|
||||
if (isNull(cleanRelationData.set)) {
|
||||
await deleteRelations({ id, attribute, db, relIdsToDelete: 'all' });
|
||||
} else {
|
||||
const isPartialUpdate = !has('set', cleanRelationData);
|
||||
let relIdsToaddOrMove;
|
||||
|
||||
// if there is nothing to insert
|
||||
if (insert.length === 0) {
|
||||
if (isPartialUpdate) {
|
||||
if (isAnyToOne(attribute)) {
|
||||
cleanRelationData.connect = cleanRelationData.connect.slice(-1);
|
||||
}
|
||||
relIdsToaddOrMove = toIds(cleanRelationData.connect);
|
||||
const relIdsToDelete = toIds(
|
||||
differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect)
|
||||
);
|
||||
|
||||
if (!isEmpty(relIdsToDelete)) {
|
||||
await deleteRelations({ id, attribute, db, relIdsToDelete });
|
||||
}
|
||||
|
||||
if (isEmpty(cleanRelationData.connect)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.createQueryBuilder(joinTable.name).insert(insert).execute();
|
||||
// Fetch current relations to handle ordering
|
||||
let currentMovingRels;
|
||||
if (hasOrderColumn(attribute) || hasInverseOrderColumn(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 (hasOrderColumn(attribute)) {
|
||||
const orderMax = (
|
||||
await this.createQueryBuilder(joinTable.name)
|
||||
.max(orderColumnName)
|
||||
.where({ [joinColumn.name]: id })
|
||||
.where(joinTable.on || {})
|
||||
.first()
|
||||
.execute()
|
||||
).max;
|
||||
|
||||
insert.forEach((row, idx) => {
|
||||
row[orderColumnName] = orderMax + idx + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// add inv order value
|
||||
if (hasInverseOrderColumn(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 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(joinTable.pivotColumns);
|
||||
|
||||
if (hasOrderColumn(attribute)) {
|
||||
query.merge([orderColumnName]);
|
||||
} else {
|
||||
query.ignore();
|
||||
}
|
||||
|
||||
await query.execute();
|
||||
|
||||
// remove gap between orders
|
||||
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,
|
||||
db,
|
||||
relIdsToDelete: 'all',
|
||||
relIdsToNotDelete: relIdsToaddOrMove,
|
||||
});
|
||||
|
||||
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 (hasOrderColumn(attribute)) {
|
||||
insert.forEach((row, idx) => {
|
||||
row[orderColumnName] = idx + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// add inv order value
|
||||
if (hasInverseOrderColumn(attribute)) {
|
||||
const existingRels = await this.createQueryBuilder(joinTable.name)
|
||||
.select(inverseJoinColumn.name)
|
||||
.where({
|
||||
[joinColumn.name]: id,
|
||||
[inverseJoinColumn.name]: { $in: relIdsToaddOrMove },
|
||||
})
|
||||
.where(joinTable.on || {})
|
||||
.execute();
|
||||
|
||||
const nonExistingRelsIds = difference(
|
||||
relIdsToaddOrMove,
|
||||
map(inverseJoinColumn.name, 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(joinTable.pivotColumns);
|
||||
|
||||
if (hasOrderColumn(attribute)) {
|
||||
query.merge([orderColumnName]);
|
||||
} else {
|
||||
query.ignore();
|
||||
}
|
||||
|
||||
await query.execute();
|
||||
}
|
||||
|
||||
// Delete the previous relations for oneToAny relations
|
||||
if (isBidirectional(attribute) && isOneToAny(attribute)) {
|
||||
await deletePreviousOneToAnyRelations({
|
||||
id,
|
||||
attribute,
|
||||
relIdsToadd: relIdsToaddOrMove,
|
||||
db,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete the previous relations for anyToOne relations
|
||||
if (isBidirectional(attribute) && isAnyToOne(attribute)) {
|
||||
await deletePreviousAnyToOneRelations({
|
||||
id,
|
||||
attribute,
|
||||
relIdToadd: relIdsToaddOrMove[0],
|
||||
db,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -795,14 +1047,7 @@ const createEntityManager = (db) => {
|
||||
}
|
||||
|
||||
if (attribute.joinTable) {
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn } = joinTable;
|
||||
|
||||
await this.createQueryBuilder(joinTable.name)
|
||||
.delete()
|
||||
.where({ [joinColumn.name]: id })
|
||||
.where(joinTable.on || {})
|
||||
.execute();
|
||||
await deleteRelations({ id, attribute, db, relIdsToDelete: 'all' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
257
packages/core/database/lib/entity-manager/regular-relations.js
Normal file
257
packages/core/database/lib/entity-manager/regular-relations.js
Normal file
@ -0,0 +1,257 @@
|
||||
'use strict';
|
||||
|
||||
const { map, isEmpty } = require('lodash/fp');
|
||||
const {
|
||||
isBidirectional,
|
||||
isOneToAny,
|
||||
isManyToAny,
|
||||
isAnyToOne,
|
||||
hasOrderColumn,
|
||||
hasInverseOrderColumn,
|
||||
} = require('../metadata/relations');
|
||||
const { createQueryBuilder } = require('../query');
|
||||
|
||||
/**
|
||||
* 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 }) => {
|
||||
if (!(isBidirectional(attribute) && isOneToAny(attribute))) {
|
||||
throw new Error(
|
||||
'deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations'
|
||||
);
|
||||
}
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn } = joinTable;
|
||||
|
||||
await createQueryBuilder(joinTable.name, db)
|
||||
.delete()
|
||||
.where({
|
||||
[inverseJoinColumn.name]: relIdsToadd,
|
||||
[joinColumn.name]: { $ne: id },
|
||||
})
|
||||
.where(joinTable.on || {})
|
||||
.execute();
|
||||
|
||||
await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd });
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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();
|
||||
|
||||
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 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();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
|
||||
let lastId = 0;
|
||||
let done = false;
|
||||
const batchSize = 100;
|
||||
while (!done) {
|
||||
const batchToDelete = await createQueryBuilder(joinTable.name, db)
|
||||
.select(inverseJoinColumn.name)
|
||||
.where({
|
||||
[joinColumn.name]: id,
|
||||
id: { $gt: lastId },
|
||||
[inverseJoinColumn.name]: { $notIn: relIdsToNotDelete },
|
||||
...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }),
|
||||
})
|
||||
.where(joinTable.on || {})
|
||||
.orderBy('id')
|
||||
.limit(batchSize)
|
||||
.execute();
|
||||
done = batchToDelete.length < batchSize;
|
||||
lastId = batchToDelete[batchToDelete.length - 1]?.id;
|
||||
|
||||
const batchIds = map(inverseJoinColumn.name, batchToDelete);
|
||||
|
||||
await createQueryBuilder(joinTable.name, db)
|
||||
.delete()
|
||||
.where({
|
||||
[joinColumn.name]: id,
|
||||
[inverseJoinColumn.name]: { $in: batchIds },
|
||||
})
|
||||
.where(joinTable.on || {})
|
||||
.execute();
|
||||
|
||||
await cleanOrderColumns({ 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.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 ({ id, attribute, db, inverseRelIds }) => {
|
||||
if (
|
||||
!(hasOrderColumn(attribute) && id) &&
|
||||
!(hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { joinTable } = attribute;
|
||||
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
||||
const update = [];
|
||||
const updateBinding = [];
|
||||
const select = ['??'];
|
||||
const selectBinding = ['id'];
|
||||
const where = [];
|
||||
const whereBinding = [];
|
||||
|
||||
if (hasOrderColumn(attribute) && id) {
|
||||
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);
|
||||
where.push('?? = ?');
|
||||
whereBinding.push(joinColumn.name, id);
|
||||
}
|
||||
|
||||
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
||||
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);
|
||||
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
||||
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
||||
}
|
||||
|
||||
// raw query as knex doesn't allow updating from a subquery
|
||||
// https://github.com/knex/knex/issues/2504
|
||||
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 = {
|
||||
deletePreviousOneToAnyRelations,
|
||||
deletePreviousAnyToOneRelations,
|
||||
deleteRelations,
|
||||
cleanOrderColumns,
|
||||
};
|
@ -123,7 +123,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
|
||||
type: 'integer',
|
||||
column: {
|
||||
unsigned: true,
|
||||
defaultTo: 0,
|
||||
defaultTo: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -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', 'component_type'],
|
||||
type: 'unique',
|
||||
},
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
@ -183,6 +188,7 @@ const createDynamicZone = (attributeName, attribute, meta) => {
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -205,9 +211,11 @@ const createComponent = (attributeName, attribute, meta) => {
|
||||
on: {
|
||||
field: attributeName,
|
||||
},
|
||||
orderColumnName: 'order',
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
@ -269,6 +272,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
|
||||
orderBy: {
|
||||
order: 'asc',
|
||||
},
|
||||
pivotColumns: [joinColumnName, typeColumnName, idColumnName],
|
||||
};
|
||||
|
||||
attribute.joinTable = joinTable;
|
||||
@ -398,12 +402,20 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
||||
const joinColumnName = _.snakeCase(`${meta.singularName}_id`);
|
||||
let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
|
||||
|
||||
// if relation is slef referencing
|
||||
// if relation is self referencing
|
||||
if (joinColumnName === inverseJoinColumnName) {
|
||||
inverseJoinColumnName = `inv_${inverseJoinColumnName}`;
|
||||
}
|
||||
|
||||
metadata.add({
|
||||
const orderColumnName = _.snakeCase(`${targetMeta.singularName}_order`);
|
||||
let inverseOrderColumnName = _.snakeCase(`${meta.singularName}_order`);
|
||||
|
||||
// if relation is self referencing
|
||||
if (attribute.relation === 'manyToMany' && joinColumnName === inverseJoinColumnName) {
|
||||
inverseOrderColumnName = `inv_${inverseOrderColumnName}`;
|
||||
}
|
||||
|
||||
const metadataSchema = {
|
||||
uid: joinTableName,
|
||||
tableName: joinTableName,
|
||||
attributes: {
|
||||
@ -433,6 +445,11 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
||||
name: `${joinTableName}_inv_fk`,
|
||||
columns: [inverseJoinColumnName],
|
||||
},
|
||||
{
|
||||
name: `${joinTableName}_unique`,
|
||||
columns: [joinColumnName, inverseJoinColumnName],
|
||||
type: 'unique',
|
||||
},
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
@ -450,7 +467,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const joinTable = {
|
||||
name: joinTableName,
|
||||
@ -462,8 +479,46 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
||||
name: inverseJoinColumnName,
|
||||
referencedColumn: 'id',
|
||||
},
|
||||
pivotColumns: [joinColumnName, inverseJoinColumnName],
|
||||
};
|
||||
|
||||
// order
|
||||
if (isAnyToMany(attribute)) {
|
||||
metadataSchema.attributes[orderColumnName] = {
|
||||
type: 'integer',
|
||||
column: {
|
||||
unsigned: true,
|
||||
defaultTo: null,
|
||||
},
|
||||
};
|
||||
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: {
|
||||
unsigned: true,
|
||||
defaultTo: null,
|
||||
},
|
||||
};
|
||||
|
||||
metadataSchema.indexes.push({
|
||||
name: `${joinTableName}_order_inv_fk`,
|
||||
columns: [inverseOrderColumnName],
|
||||
});
|
||||
|
||||
joinTable.inverseOrderColumnName = inverseOrderColumnName;
|
||||
}
|
||||
|
||||
metadata.add(metadataSchema);
|
||||
|
||||
attribute.joinTable = joinTable;
|
||||
|
||||
if (isBidirectional(attribute)) {
|
||||
@ -479,13 +534,30 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
||||
name: joinTableName,
|
||||
joinColumn: joinTable.inverseJoinColumn,
|
||||
inverseJoinColumn: joinTable.joinColumn,
|
||||
pivotColumns: joinTable.pivotColumns,
|
||||
};
|
||||
|
||||
if (isManyToAny(attribute)) {
|
||||
inverseAttribute.joinTable.orderColumnName = inverseOrderColumnName;
|
||||
inverseAttribute.joinTable.orderBy = { [inverseOrderColumnName]: 'asc' };
|
||||
}
|
||||
if (isAnyToMany(attribute)) {
|
||||
inverseAttribute.joinTable.inverseOrderColumnName = orderColumnName;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const hasOrderColumn = (attribute) => isAnyToMany(attribute);
|
||||
const hasInverseOrderColumn = (attribute) => isBidirectional(attribute) && isManyToAny(attribute);
|
||||
|
||||
module.exports = {
|
||||
createRelation,
|
||||
|
||||
isBidirectional,
|
||||
isOneToAny,
|
||||
isManyToAny,
|
||||
isAnyToOne,
|
||||
isAnyToMany,
|
||||
hasOrderColumn,
|
||||
hasInverseOrderColumn,
|
||||
};
|
||||
|
@ -23,8 +23,13 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
|
||||
offset: null,
|
||||
transaction: null,
|
||||
forUpdate: false,
|
||||
onConflict: null,
|
||||
merge: null,
|
||||
ignore: false,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
increments: [],
|
||||
decrements: [],
|
||||
aliasCounter: 0,
|
||||
},
|
||||
initialState
|
||||
@ -67,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';
|
||||
|
||||
@ -84,6 +107,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 +386,9 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
if (state.data) {
|
||||
qb.update(state.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
@ -374,6 +413,22 @@ 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.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);
|
||||
}
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user