mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-03 19:36:20 +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) {
 | 
			
		||||
              continue;
 | 
			
		||||
            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;
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              // 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) {
 | 
			
		||||
          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': {
 | 
			
		||||
          qb.update(state.data);
 | 
			
		||||
          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