Merge pull request #14327 from strapi/relations-main-view/full-relation-update

Add partial relation update
This commit is contained in:
Pierre Noël 2022-09-30 23:31:54 +02:00 committed by GitHub
commit 76665ae056
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 737 additions and 186 deletions

View File

@ -24,7 +24,7 @@ module.exports = {
await validateFindAvailable(ctx.request.query); 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; const sourceModelUid = component || model;
@ -103,8 +103,17 @@ module.exports = {
const alias = subQuery.getAlias(); const alias = subQuery.getAlias();
const where = {
id: entityId,
[`${alias}.id`]: { $notNull: true },
};
if (!isEmpty(idsToInclude)) {
where[`${alias}.id`].$notIn = idsToInclude;
}
const knexSubQuery = subQuery const knexSubQuery = subQuery
.where({ id: entityId, [`${alias}.id`]: { $notNull: true } }) .where(where)
.join({ alias, targetField }) .join({ alias, targetField })
.select(`${alias}.id`) .select(`${alias}.id`)
.getKnexQuery(); .getKnexQuery();

View File

@ -91,15 +91,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe('tag1'); expect(body.name).toBe('tag1');
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -119,15 +115,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe('tag2'); expect(body.name).toBe('tag2');
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -147,15 +139,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe('tag3'); expect(body.name).toBe('tag3');
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -180,15 +168,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -216,15 +200,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -251,15 +231,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -282,15 +258,11 @@ describe('Relations', () => {
expect(body.title).toBe(data.articles[0].title); expect(body.title).toBe(data.articles[0].title);
expect(body.content).toBe(data.articles[0].content); expect(body.content).toBe(data.articles[0].content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -312,15 +284,11 @@ describe('Relations', () => {
expect(body.title).toBe(data.articles[0].title); expect(body.title).toBe(data.articles[0].title);
expect(body.content).toBe(data.articles[0].content); expect(body.content).toBe(data.articles[0].content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -346,15 +314,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -458,15 +422,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
@ -502,15 +462,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe('cat1'); expect(body.name).toBe('cat1');
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -533,15 +489,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe('cat2'); expect(body.name).toBe('cat2');
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -568,15 +520,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -603,15 +551,11 @@ describe('Relations', () => {
expect(body.title).toBe(data.articles[0].title); expect(body.title).toBe(data.articles[0].title);
expect(body.content).toBe(data.articles[0].content); expect(body.content).toBe(data.articles[0].content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
@ -640,15 +584,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
@ -671,15 +611,11 @@ describe('Relations', () => {
expect(body.title).toBe(data.articles[1].title); expect(body.title).toBe(data.articles[1].title);
expect(body.content).toBe(data.articles[1].content); expect(body.content).toBe(data.articles[1].content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
@ -704,15 +640,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe(data.categories[0].name); expect(body.name).toBe(data.categories[0].name);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
@ -737,15 +669,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe(entry.name); expect(body.name).toBe(entry.name);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
@ -845,15 +773,11 @@ describe('Relations', () => {
expect(body.id).toBeDefined(); expect(body.id).toBeDefined();
expect(body.name).toBe('ref1'); expect(body.name).toBe('ref1');
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
}); });
@ -876,15 +800,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.publishedAt).toBeUndefined(); expect(body.publishedAt).toBeUndefined();
@ -905,15 +825,11 @@ describe('Relations', () => {
expect(body.title).toBe(data.articles[0].title); expect(body.title).toBe(data.articles[0].title);
expect(body.content).toBe(data.articles[0].content); expect(body.content).toBe(data.articles[0].content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
@ -940,15 +856,11 @@ describe('Relations', () => {
expect(body.title).toBe(entry.title); expect(body.title).toBe(entry.title);
expect(body.content).toBe(entry.content); expect(body.content).toBe(entry.content);
expect(body.createdBy).toMatchObject({ expect(body.createdBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
expect(body.updatedBy).toMatchObject({ expect(body.updatedBy).toMatchObject({
firstname: 'admin',
id: 1, id: 1,
lastname: 'admin',
username: null, username: null,
}); });
const reference = await getRelations('article', 'reference', body.id); const reference = await getRelations('article', 'reference', body.id);

View File

@ -12,20 +12,38 @@ const {
isEmpty, isEmpty,
isArray, isArray,
isNull, isNull,
uniqWith,
isEqual,
differenceWith,
isNumber,
map,
difference,
} = require('lodash/fp'); } = require('lodash/fp');
const types = require('../types'); const types = require('../types');
const { createField } = require('../fields'); const { createField } = require('../fields');
const { createQueryBuilder } = require('../query'); const { createQueryBuilder } = require('../query');
const { createRepository } = require('./entity-repository'); const { createRepository } = require('./entity-repository');
const { isBidirectional, isOneToAny } = require('../metadata/relations');
const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-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 toId = (value) => value.id || value;
const toIds = (value) => castArray(value || []).map(toId); const toIds = (value) => castArray(value || []).map(toId);
const isValidId = (value) => isString(value) || isInteger(value); const isValidId = (value) => isString(value) || isInteger(value);
const toAssocs = (data) => { const toIdArray = (data) => {
return castArray(data) const array = castArray(data)
.filter((datum) => !isNil(datum)) .filter((datum) => !isNil(datum))
.map((datum) => { .map((datum) => {
// if it is a string or an integer return an obj with id = to 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 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 } = {}) => { const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
@ -355,6 +393,8 @@ const createEntityManager = (db) => {
continue; continue;
} }
const cleanRelationData = toAssocs(data[attributeName]);
if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
const { target, morphBy } = attribute; const { target, morphBy } = attribute;
@ -364,9 +404,11 @@ const createEntityManager = (db) => {
// set columns // set columns
const { idColumn, typeColumn } = targetAttribute.morphColumn; const { idColumn, typeColumn } = targetAttribute.morphColumn;
const relId = toId(cleanRelationData.set[0]);
await this.createQueryBuilder(target) await this.createQueryBuilder(target)
.update({ [idColumn.name]: id, [typeColumn.name]: uid }) .update({ [idColumn.name]: id, [typeColumn.name]: uid })
.where({ id: toId(data[attributeName]) }) .where({ id: relId })
.execute(); .execute();
} else if (targetAttribute.relation === 'morphToMany') { } else if (targetAttribute.relation === 'morphToMany') {
const { joinTable } = targetAttribute; const { joinTable } = targetAttribute;
@ -374,7 +416,11 @@ const createEntityManager = (db) => {
const { idColumn, typeColumn } = morphColumn; 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 { return {
[joinColumn.name]: data.id, [joinColumn.name]: data.id,
[idColumn.name]: id, [idColumn.name]: id,
@ -386,10 +432,6 @@ const createEntityManager = (db) => {
}; };
}); });
if (isEmpty(rows)) {
continue;
}
await this.createQueryBuilder(joinTable.name).insert(rows).execute(); await this.createQueryBuilder(joinTable.name).insert(rows).execute();
} }
@ -403,18 +445,19 @@ const createEntityManager = (db) => {
const { idColumn, typeColumn, typeField = '__type' } = morphColumn; 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, [joinColumn.name]: id,
[idColumn.name]: data.id, [idColumn.name]: data.id,
[typeColumn.name]: data[typeField], [typeColumn.name]: data[typeField],
...(joinTable.on || {}), ...(joinTable.on || {}),
...(data.__pivot || {}), ...(data.__pivot || {}),
order: idx + 1,
})); }));
if (isEmpty(rows)) {
continue;
}
// delete previous relations // delete previous relations
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
uid, uid,
@ -429,13 +472,14 @@ const createEntityManager = (db) => {
} }
if (attribute.joinColumn && attribute.owner) { if (attribute.joinColumn && attribute.owner) {
const relIdsToAdd = toIds(cleanRelationData.set);
if ( if (
attribute.relation === 'oneToOne' && attribute.relation === 'oneToOne' &&
isBidirectional(attribute) && isBidirectional(attribute) &&
data[attributeName] relIdsToAdd.length
) { ) {
await this.createQueryBuilder(uid) 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 }) .update({ [attribute.joinColumn.name]: null })
.execute(); .execute();
} }
@ -449,6 +493,7 @@ const createEntityManager = (db) => {
const { target } = attribute; 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) // 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) await this.createQueryBuilder(target)
.where({ [attribute.joinColumn.referencedColumn]: id }) .where({ [attribute.joinColumn.referencedColumn]: id })
@ -458,7 +503,7 @@ const createEntityManager = (db) => {
await this.createQueryBuilder(target) await this.createQueryBuilder(target)
.update({ [attribute.joinColumn.referencedColumn]: id }) .update({ [attribute.joinColumn.referencedColumn]: id })
// NOTE: works if it is an array or a single id // NOTE: works if it is an array or a single id
.where({ id: data[attributeName] }) .where({ id: relIdsToAdd })
.execute(); .execute();
} }
@ -466,17 +511,18 @@ const createEntityManager = (db) => {
// need to set the column on the target // need to set the column on the target
const { joinTable } = attribute; const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } =
joinTable;
if (isOneToAny(attribute) && isBidirectional(attribute)) { const relsToAdd = cleanRelationData.set || cleanRelationData.connect;
await this.createQueryBuilder(joinTable.name) const relIdsToadd = toIds(relsToAdd);
.delete()
.where({ [inverseJoinColumn.name]: castArray(data[attributeName]) }) if (isBidirectional(attribute) && isOneToAny(attribute)) {
.where(joinTable.on || {}) await deletePreviousOneToAnyRelations({ id, attribute, relIdsToadd, db });
.execute();
} }
const insert = toAssocs(data[attributeName]).map((data) => { // prepare new relations to insert
const insert = relsToAdd.map((data) => {
return { return {
[joinColumn.name]: id, [joinColumn.name]: id,
[inverseJoinColumn.name]: data.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) { if (insert.length === 0) {
continue; continue;
} }
// insert new relations
await this.createQueryBuilder(joinTable.name).insert(insert).execute(); await this.createQueryBuilder(joinTable.name).insert(insert).execute();
} }
} }
@ -514,6 +587,7 @@ const createEntityManager = (db) => {
if (attribute.type !== 'relation' || !has(attributeName, data)) { if (attribute.type !== 'relation' || !has(attributeName, data)) {
continue; continue;
} }
const cleanRelationData = toAssocs(data[attributeName]);
if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') { if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
const { target, morphBy } = attribute; const { target, morphBy } = attribute;
@ -531,10 +605,11 @@ const createEntityManager = (db) => {
.where({ [idColumn.name]: id, [typeColumn.name]: uid }) .where({ [idColumn.name]: id, [typeColumn.name]: uid })
.execute(); .execute();
if (!isNull(data[attributeName])) { if (!isNull(cleanRelationData.set)) {
const relId = toIds(cleanRelationData.set[0]);
await this.createQueryBuilder(target) await this.createQueryBuilder(target)
.update({ [idColumn.name]: id, [typeColumn.name]: uid }) .update({ [idColumn.name]: id, [typeColumn.name]: uid })
.where({ id: toId(data[attributeName]) }) .where({ id: relId })
.execute(); .execute();
} }
} else if (targetAttribute.relation === 'morphToMany') { } else if (targetAttribute.relation === 'morphToMany') {
@ -553,7 +628,11 @@ const createEntityManager = (db) => {
}) })
.execute(); .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, [joinColumn.name]: data.id,
[idColumn.name]: id, [idColumn.name]: id,
[typeColumn.name]: uid, [typeColumn.name]: uid,
@ -563,10 +642,6 @@ const createEntityManager = (db) => {
field: attributeName, field: attributeName,
})); }));
if (isEmpty(rows)) {
continue;
}
await this.createQueryBuilder(joinTable.name).insert(rows).execute(); await this.createQueryBuilder(joinTable.name).insert(rows).execute();
} }
@ -592,18 +667,19 @@ const createEntityManager = (db) => {
}) })
.execute(); .execute();
const rows = toAssocs(data[attributeName]).map((data) => ({ if (isEmpty(cleanRelationData.set)) {
continue;
}
const rows = cleanRelationData.set.map((data, idx) => ({
[joinColumn.name]: id, [joinColumn.name]: id,
[idColumn.name]: data.id, [idColumn.name]: data.id,
[typeColumn.name]: data[typeField], [typeColumn.name]: data[typeField],
...(joinTable.on || {}), ...(joinTable.on || {}),
...(data.__pivot || {}), ...(data.__pivot || {}),
order: idx + 1,
})); }));
if (isEmpty(rows)) {
continue;
}
// delete previous relations // delete previous relations
await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, { await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
uid, uid,
@ -633,10 +709,10 @@ const createEntityManager = (db) => {
.update({ [attribute.joinColumn.referencedColumn]: null }) .update({ [attribute.joinColumn.referencedColumn]: null })
.execute(); .execute();
if (!isNull(data[attributeName])) { if (!isNull(cleanRelationData.set)) {
const relIdsToAdd = toIds(cleanRelationData.set);
await this.createQueryBuilder(target) await this.createQueryBuilder(target)
// NOTE: works if it is an array or a single id .where({ id: relIdsToAdd })
.where({ id: data[attributeName] })
.update({ [attribute.joinColumn.referencedColumn]: id }) .update({ [attribute.joinColumn.referencedColumn]: id })
.execute(); .execute();
} }
@ -644,42 +720,218 @@ const createEntityManager = (db) => {
if (attribute.joinTable) { if (attribute.joinTable) {
const { joinTable } = attribute; const { joinTable } = attribute;
const { joinColumn, inverseJoinColumn } = joinTable; const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } =
joinTable;
// clear previous associations in the joinTable const select = [joinColumn.name, inverseJoinColumn.name];
await this.createQueryBuilder(joinTable.name) if (hasOrderColumn(attribute)) {
.delete() select.push(orderColumnName);
.where({ [joinColumn.name]: id }) }
.where(joinTable.on || {}) if (hasInverseOrderColumn(attribute)) {
.execute(); select.push(inverseOrderColumnName);
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])) { // only delete relations
const insert = toAssocs(data[attributeName]).map((data) => { if (isNull(cleanRelationData.set)) {
return { await deleteRelations({ id, attribute, db, relIdsToDelete: 'all' });
[joinColumn.name]: id, } else {
[inverseJoinColumn.name]: data.id, const isPartialUpdate = !has('set', cleanRelationData);
...(joinTable.on || {}), let relIdsToaddOrMove;
...(data.__pivot || {}),
};
});
// if there is nothing to insert if (isPartialUpdate) {
if (insert.length === 0) { if (isAnyToOne(attribute)) {
continue; 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;
}
// 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();
} }
await this.createQueryBuilder(joinTable.name).insert(insert).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) { if (attribute.joinTable) {
const { joinTable } = attribute; await deleteRelations({ id, attribute, db, relIdsToDelete: 'all' });
const { joinColumn } = joinTable;
await this.createQueryBuilder(joinTable.name)
.delete()
.where({ [joinColumn.name]: id })
.where(joinTable.on || {})
.execute();
} }
} }
}, },

View 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,
};

View File

@ -123,7 +123,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
type: 'integer', type: 'integer',
column: { column: {
unsigned: true, unsigned: true,
defaultTo: 0, defaultTo: null,
}, },
}, },
}, },
@ -142,6 +142,11 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
name: `${baseModelMeta.tableName}_entity_fk`, name: `${baseModelMeta.tableName}_entity_fk`,
columns: ['entity_id'], columns: ['entity_id'],
}, },
{
name: `${baseModelMeta.tableName}_unique`,
columns: ['entity_id', 'component_id', 'field', 'component_type'],
type: 'unique',
},
], ],
foreignKeys: [ foreignKeys: [
{ {
@ -183,6 +188,7 @@ const createDynamicZone = (attributeName, attribute, meta) => {
orderBy: { orderBy: {
order: 'asc', order: 'asc',
}, },
pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
}, },
}); });
}; };
@ -205,9 +211,11 @@ const createComponent = (attributeName, attribute, meta) => {
on: { on: {
field: attributeName, field: attributeName,
}, },
orderColumnName: 'order',
orderBy: { orderBy: {
order: 'asc', order: 'asc',
}, },
pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
}, },
}); });
}; };

View File

@ -10,6 +10,9 @@ const hasInversedBy = _.has('inversedBy');
const hasMappedBy = _.has('mappedBy'); const hasMappedBy = _.has('mappedBy');
const isOneToAny = (attribute) => ['oneToOne', 'oneToMany'].includes(attribute.relation); 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 isBidirectional = (attribute) => hasInversedBy(attribute) || hasMappedBy(attribute);
const isOwner = (attribute) => !isBidirectional(attribute) || hasInversedBy(attribute); const isOwner = (attribute) => !isBidirectional(attribute) || hasInversedBy(attribute);
const shouldUseJoinTable = (attribute) => attribute.useJoinTable !== false; const shouldUseJoinTable = (attribute) => attribute.useJoinTable !== false;
@ -269,6 +272,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
orderBy: { orderBy: {
order: 'asc', order: 'asc',
}, },
pivotColumns: [joinColumnName, typeColumnName, idColumnName],
}; };
attribute.joinTable = joinTable; attribute.joinTable = joinTable;
@ -398,12 +402,20 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
const joinColumnName = _.snakeCase(`${meta.singularName}_id`); const joinColumnName = _.snakeCase(`${meta.singularName}_id`);
let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`); let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
// if relation is slef referencing // if relation is self referencing
if (joinColumnName === inverseJoinColumnName) { if (joinColumnName === inverseJoinColumnName) {
inverseJoinColumnName = `inv_${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, uid: joinTableName,
tableName: joinTableName, tableName: joinTableName,
attributes: { attributes: {
@ -433,6 +445,11 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
name: `${joinTableName}_inv_fk`, name: `${joinTableName}_inv_fk`,
columns: [inverseJoinColumnName], columns: [inverseJoinColumnName],
}, },
{
name: `${joinTableName}_unique`,
columns: [joinColumnName, inverseJoinColumnName],
type: 'unique',
},
], ],
foreignKeys: [ foreignKeys: [
{ {
@ -450,7 +467,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
onDelete: 'CASCADE', onDelete: 'CASCADE',
}, },
], ],
}); };
const joinTable = { const joinTable = {
name: joinTableName, name: joinTableName,
@ -462,8 +479,46 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
name: inverseJoinColumnName, name: inverseJoinColumnName,
referencedColumn: 'id', 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; attribute.joinTable = joinTable;
if (isBidirectional(attribute)) { if (isBidirectional(attribute)) {
@ -479,13 +534,30 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
name: joinTableName, name: joinTableName,
joinColumn: joinTable.inverseJoinColumn, joinColumn: joinTable.inverseJoinColumn,
inverseJoinColumn: joinTable.joinColumn, inverseJoinColumn: joinTable.joinColumn,
pivotColumns: joinTable.pivotColumns,
}; };
if (isManyToAny(attribute)) {
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 = { module.exports = {
createRelation, createRelation,
isBidirectional, isBidirectional,
isOneToAny, isOneToAny,
isManyToAny,
isAnyToOne,
isAnyToMany,
hasOrderColumn,
hasInverseOrderColumn,
}; };

View File

@ -23,8 +23,13 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
offset: null, offset: null,
transaction: null, transaction: null,
forUpdate: false, forUpdate: false,
onConflict: null,
merge: null,
ignore: false,
orderBy: [], orderBy: [],
groupBy: [], groupBy: [],
increments: [],
decrements: [],
aliasCounter: 0, aliasCounter: 0,
}, },
initialState initialState
@ -67,6 +72,24 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
return this; return this;
}, },
onConflict(args) {
state.onConflict = args;
return this;
},
merge(args) {
state.merge = args;
return this;
},
ignore() {
state.ignore = true;
return this;
},
delete() { delete() {
state.type = 'delete'; state.type = 'delete';
@ -84,6 +107,20 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
return this; 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') { count(count = 'id') {
state.type = 'count'; state.type = 'count';
state.count = count; state.count = count;
@ -349,7 +386,9 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
break; break;
} }
case 'update': { case 'update': {
qb.update(state.data); if (state.data) {
qb.update(state.data);
}
break; break;
} }
case 'delete': { case 'delete': {
@ -374,6 +413,22 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
qb.forUpdate(); 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) { if (state.limit) {
qb.limit(state.limit); qb.limit(state.limit);
} }

View File

@ -48,11 +48,10 @@ const createComponents = async (uid, data) => {
); );
// TODO: add order // TODO: add order
componentBody[attributeName] = components.map(({ id }, idx) => { componentBody[attributeName] = components.map(({ id }) => {
return { return {
id, id,
__pivot: { __pivot: {
order: idx + 1,
field: attributeName, field: attributeName,
component_type: componentUID, component_type: componentUID,
}, },
@ -63,7 +62,6 @@ const createComponents = async (uid, data) => {
componentBody[attributeName] = { componentBody[attributeName] = {
id: component.id, id: component.id,
__pivot: { __pivot: {
order: 1,
field: attributeName, field: attributeName,
component_type: componentUID, component_type: componentUID,
}, },
@ -81,13 +79,12 @@ const createComponents = async (uid, data) => {
} }
componentBody[attributeName] = await Promise.all( componentBody[attributeName] = await Promise.all(
dynamiczoneValues.map(async (value, idx) => { dynamiczoneValues.map(async (value) => {
const { id } = await createComponent(value.__component, value); const { id } = await createComponent(value.__component, value);
return { return {
id, id,
__component: value.__component, __component: value.__component,
__pivot: { __pivot: {
order: idx + 1,
field: attributeName, field: attributeName,
}, },
}; };
@ -145,11 +142,10 @@ const updateComponents = async (uid, entityToUpdate, data) => {
componentValue.map((value) => updateOrCreateComponent(componentUID, value)) componentValue.map((value) => updateOrCreateComponent(componentUID, value))
); );
componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }, idx) => { componentBody[attributeName] = components.filter(_.negate(_.isNil)).map(({ id }) => {
return { return {
id, id,
__pivot: { __pivot: {
order: idx + 1,
field: attributeName, field: attributeName,
component_type: componentUID, component_type: componentUID,
}, },
@ -160,7 +156,6 @@ const updateComponents = async (uid, entityToUpdate, data) => {
componentBody[attributeName] = component && { componentBody[attributeName] = component && {
id: component.id, id: component.id,
__pivot: { __pivot: {
order: 1,
field: attributeName, field: attributeName,
component_type: componentUID, component_type: componentUID,
}, },
@ -180,14 +175,13 @@ const updateComponents = async (uid, entityToUpdate, data) => {
} }
componentBody[attributeName] = await Promise.all( componentBody[attributeName] = await Promise.all(
dynamiczoneValues.map(async (value, idx) => { dynamiczoneValues.map(async (value) => {
const { id } = await updateOrCreateComponent(value.__component, value); const { id } = await updateOrCreateComponent(value.__component, value);
return { return {
id, id,
__component: value.__component, __component: value.__component,
__pivot: { __pivot: {
order: idx + 1,
field: attributeName, field: attributeName,
}, },
}; };

View File

@ -99,7 +99,6 @@ describe('i18n - Find existing relations', () => {
rq = await createAuthRequest({ strapi }); rq = await createAuthRequest({ strapi });
data.shops = await builder.sanitizedFixturesFor(shopModel.singularName, strapi); data.shops = await builder.sanitizedFixturesFor(shopModel.singularName, strapi);
console.log('data.shops', data.shops);
data.products = await builder.sanitizedFixturesFor(productModel.singularName, strapi); data.products = await builder.sanitizedFixturesFor(productModel.singularName, strapi);
}); });