@strapi/database 4.4.3 → 4.5.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,20 +12,38 @@ const {
12
12
  isEmpty,
13
13
  isArray,
14
14
  isNull,
15
+ uniqWith,
16
+ isEqual,
17
+ differenceWith,
18
+ isNumber,
19
+ map,
20
+ difference,
15
21
  } = require('lodash/fp');
16
22
  const types = require('../types');
17
23
  const { createField } = require('../fields');
18
24
  const { createQueryBuilder } = require('../query');
19
25
  const { createRepository } = require('./entity-repository');
20
- const { isBidirectional, isOneToAny } = require('../metadata/relations');
21
26
  const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations');
27
+ const {
28
+ isBidirectional,
29
+ isAnyToOne,
30
+ isOneToAny,
31
+ hasOrderColumn,
32
+ hasInverseOrderColumn,
33
+ } = require('../metadata/relations');
34
+ const {
35
+ deletePreviousOneToAnyRelations,
36
+ deletePreviousAnyToOneRelations,
37
+ deleteRelations,
38
+ cleanOrderColumns,
39
+ } = require('./regular-relations');
22
40
 
23
41
  const toId = (value) => value.id || value;
24
42
  const toIds = (value) => castArray(value || []).map(toId);
25
43
 
26
44
  const isValidId = (value) => isString(value) || isInteger(value);
27
- const toAssocs = (data) => {
28
- return castArray(data)
45
+ const toIdArray = (data) => {
46
+ const array = castArray(data)
29
47
  .filter((datum) => !isNil(datum))
30
48
  .map((datum) => {
31
49
  // if it is a string or an integer return an obj with id = to datum
@@ -40,6 +58,26 @@ const toAssocs = (data) => {
40
58
 
41
59
  return datum;
42
60
  });
61
+ return uniqWith(isEqual, array);
62
+ };
63
+
64
+ const toAssocs = (data) => {
65
+ if (isArray(data) || isString(data) || isNumber(data) || isNull(data) || data?.id) {
66
+ return {
67
+ set: isNull(data) ? data : toIdArray(data),
68
+ };
69
+ }
70
+
71
+ if (data?.set) {
72
+ return {
73
+ set: isNull(data.set) ? data.set : toIdArray(data.set),
74
+ };
75
+ }
76
+
77
+ return {
78
+ connect: toIdArray(data?.connect),
79
+ disconnect: toIdArray(data?.disconnect),
80
+ };
43
81
  };
44
82
 
45
83
  const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
@@ -173,12 +211,24 @@ const createEntityManager = (db) => {
173
211
  }
174
212
 
175
213
  const dataToInsert = processData(metadata, data, { withDefaults: true });
214
+ let id;
176
215
 
177
- const res = await this.createQueryBuilder(uid).insert(dataToInsert).execute();
216
+ const trx = await strapi.db.transaction();
217
+ try {
218
+ const res = await this.createQueryBuilder(uid)
219
+ .insert(dataToInsert)
220
+ .transacting(trx)
221
+ .execute();
178
222
 
179
- const id = res[0].id || res[0];
223
+ id = res[0].id || res[0];
180
224
 
181
- await this.attachRelations(uid, id, data);
225
+ await this.attachRelations(uid, id, data, { transaction: trx });
226
+
227
+ await trx.commit();
228
+ } catch (e) {
229
+ await trx.rollback();
230
+ throw e;
231
+ }
182
232
 
183
233
  // TODO: in case there is no select or populate specified return the inserted data ?
184
234
  // TODO: do not trigger the findOne lifecycles ?
@@ -243,13 +293,25 @@ const createEntityManager = (db) => {
243
293
 
244
294
  const { id } = entity;
245
295
 
246
- const dataToUpdate = processData(metadata, data);
296
+ const trx = await strapi.db.transaction();
297
+ try {
298
+ const dataToUpdate = processData(metadata, data);
247
299
 
248
- if (!isEmpty(dataToUpdate)) {
249
- await this.createQueryBuilder(uid).where({ id }).update(dataToUpdate).execute();
250
- }
300
+ if (!isEmpty(dataToUpdate)) {
301
+ await this.createQueryBuilder(uid)
302
+ .where({ id })
303
+ .update(dataToUpdate)
304
+ .transacting(trx)
305
+ .execute();
306
+ }
251
307
 
252
- await this.updateRelations(uid, id, data);
308
+ await this.updateRelations(uid, id, data, { transaction: trx });
309
+
310
+ await trx.commit();
311
+ } catch (e) {
312
+ await trx.rollback();
313
+ throw e;
314
+ }
253
315
 
254
316
  // TODO: do not trigger the findOne lifecycles ?
255
317
  const result = await this.findOne(uid, {
@@ -310,9 +372,17 @@ const createEntityManager = (db) => {
310
372
 
311
373
  const { id } = entity;
312
374
 
313
- await this.createQueryBuilder(uid).where({ id }).delete().execute();
375
+ const trx = await strapi.db.transaction();
376
+ try {
377
+ await this.createQueryBuilder(uid).where({ id }).delete().transacting(trx).execute();
314
378
 
315
- await this.deleteRelations(uid, id);
379
+ await this.deleteRelations(uid, id, { transaction: trx });
380
+
381
+ await trx.commit();
382
+ } catch (e) {
383
+ await trx.rollback();
384
+ throw e;
385
+ }
316
386
 
317
387
  await db.lifecycles.run('afterDelete', uid, { params, result: entity }, states);
318
388
 
@@ -342,8 +412,7 @@ const createEntityManager = (db) => {
342
412
  * @param {ID} id - entity ID
343
413
  * @param {object} data - data received for creation
344
414
  */
345
- // TODO: wrap Transaction
346
- async attachRelations(uid, id, data) {
415
+ async attachRelations(uid, id, data, { transaction: trx }) {
347
416
  const { attributes } = db.metadata.get(uid);
348
417
 
349
418
  for (const attributeName of Object.keys(attributes)) {
@@ -355,6 +424,8 @@ const createEntityManager = (db) => {
355
424
  continue;
356
425
  }
357
426
 
427
+ const cleanRelationData = toAssocs(data[attributeName]);
428
+
358
429
  if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
359
430
  const { target, morphBy } = attribute;
360
431
 
@@ -364,9 +435,12 @@ const createEntityManager = (db) => {
364
435
  // set columns
365
436
  const { idColumn, typeColumn } = targetAttribute.morphColumn;
366
437
 
438
+ const relId = toId(cleanRelationData.set[0]);
439
+
367
440
  await this.createQueryBuilder(target)
368
441
  .update({ [idColumn.name]: id, [typeColumn.name]: uid })
369
- .where({ id: toId(data[attributeName]) })
442
+ .where({ id: relId })
443
+ .transacting(trx)
370
444
  .execute();
371
445
  } else if (targetAttribute.relation === 'morphToMany') {
372
446
  const { joinTable } = targetAttribute;
@@ -374,7 +448,11 @@ const createEntityManager = (db) => {
374
448
 
375
449
  const { idColumn, typeColumn } = morphColumn;
376
450
 
377
- const rows = toAssocs(data[attributeName]).map((data, idx) => {
451
+ if (isEmpty(cleanRelationData.set)) {
452
+ continue;
453
+ }
454
+
455
+ const rows = cleanRelationData.set.map((data, idx) => {
378
456
  return {
379
457
  [joinColumn.name]: data.id,
380
458
  [idColumn.name]: id,
@@ -386,11 +464,7 @@ const createEntityManager = (db) => {
386
464
  };
387
465
  });
388
466
 
389
- if (isEmpty(rows)) {
390
- continue;
391
- }
392
-
393
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
467
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
394
468
  }
395
469
 
396
470
  continue;
@@ -403,40 +477,44 @@ const createEntityManager = (db) => {
403
477
 
404
478
  const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
405
479
 
406
- const rows = toAssocs(data[attributeName]).map((data) => ({
480
+ if (isEmpty(cleanRelationData.set)) {
481
+ continue;
482
+ }
483
+
484
+ const rows = cleanRelationData.set.map((data, idx) => ({
407
485
  [joinColumn.name]: id,
408
486
  [idColumn.name]: data.id,
409
487
  [typeColumn.name]: data[typeField],
410
488
  ...(joinTable.on || {}),
411
489
  ...(data.__pivot || {}),
490
+ order: idx + 1,
412
491
  }));
413
492
 
414
- if (isEmpty(rows)) {
415
- continue;
416
- }
417
-
418
493
  // delete previous relations
419
494
  await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
420
495
  uid,
421
496
  attributeName,
422
497
  joinTable,
423
498
  db,
499
+ transaction: trx,
424
500
  });
425
501
 
426
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
502
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
427
503
 
428
504
  continue;
429
505
  }
430
506
 
431
507
  if (attribute.joinColumn && attribute.owner) {
508
+ const relIdsToAdd = toIds(cleanRelationData.set);
432
509
  if (
433
510
  attribute.relation === 'oneToOne' &&
434
511
  isBidirectional(attribute) &&
435
- data[attributeName]
512
+ relIdsToAdd.length
436
513
  ) {
437
514
  await this.createQueryBuilder(uid)
438
- .where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } })
515
+ .where({ [attribute.joinColumn.name]: relIdsToAdd, id: { $ne: id } })
439
516
  .update({ [attribute.joinColumn.name]: null })
517
+ .transacting(trx)
440
518
  .execute();
441
519
  }
442
520
 
@@ -449,16 +527,19 @@ const createEntityManager = (db) => {
449
527
  const { target } = attribute;
450
528
 
451
529
  // TODO: check it is an id & the entity exists (will throw due to FKs otherwise so not a big pbl in SQL)
530
+ const relIdsToAdd = toIds(cleanRelationData.set);
452
531
 
453
532
  await this.createQueryBuilder(target)
454
533
  .where({ [attribute.joinColumn.referencedColumn]: id })
455
534
  .update({ [attribute.joinColumn.referencedColumn]: null })
535
+ .transacting(trx)
456
536
  .execute();
457
537
 
458
538
  await this.createQueryBuilder(target)
459
539
  .update({ [attribute.joinColumn.referencedColumn]: id })
460
540
  // NOTE: works if it is an array or a single id
461
- .where({ id: data[attributeName] })
541
+ .where({ id: relIdsToAdd })
542
+ .transacting(trx)
462
543
  .execute();
463
544
  }
464
545
 
@@ -466,17 +547,24 @@ const createEntityManager = (db) => {
466
547
  // need to set the column on the target
467
548
 
468
549
  const { joinTable } = attribute;
469
- const { joinColumn, inverseJoinColumn } = joinTable;
470
-
471
- if (isOneToAny(attribute) && isBidirectional(attribute)) {
472
- await this.createQueryBuilder(joinTable.name)
473
- .delete()
474
- .where({ [inverseJoinColumn.name]: castArray(data[attributeName]) })
475
- .where(joinTable.on || {})
476
- .execute();
550
+ const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } =
551
+ joinTable;
552
+
553
+ const relsToAdd = cleanRelationData.set || cleanRelationData.connect;
554
+ const relIdsToadd = toIds(relsToAdd);
555
+
556
+ if (isBidirectional(attribute) && isOneToAny(attribute)) {
557
+ await deletePreviousOneToAnyRelations({
558
+ id,
559
+ attribute,
560
+ relIdsToadd,
561
+ db,
562
+ transaction: trx,
563
+ });
477
564
  }
478
565
 
479
- const insert = toAssocs(data[attributeName]).map((data) => {
566
+ // prepare new relations to insert
567
+ const insert = relsToAdd.map((data) => {
480
568
  return {
481
569
  [joinColumn.name]: id,
482
570
  [inverseJoinColumn.name]: data.id,
@@ -485,12 +573,40 @@ const createEntityManager = (db) => {
485
573
  };
486
574
  });
487
575
 
488
- // if there is nothing to insert
576
+ // add order value
577
+ if (hasOrderColumn(attribute)) {
578
+ insert.forEach((rel, idx) => {
579
+ rel[orderColumnName] = idx + 1;
580
+ });
581
+ }
582
+ // add inv_order value
583
+ if (hasInverseOrderColumn(attribute)) {
584
+ const maxResults = await db
585
+ .getConnection()
586
+ .select(inverseJoinColumn.name)
587
+ .max(inverseOrderColumnName, { as: 'max' })
588
+ .whereIn(inverseJoinColumn.name, relIdsToadd)
589
+ .where(joinTable.on || {})
590
+ .groupBy(inverseJoinColumn.name)
591
+ .from(joinTable.name)
592
+ .transacting(trx);
593
+
594
+ const maxMap = maxResults.reduce(
595
+ (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }),
596
+ {}
597
+ );
598
+
599
+ insert.forEach((rel) => {
600
+ rel[inverseOrderColumnName] = (maxMap[rel[inverseJoinColumn.name]] || 0) + 1;
601
+ });
602
+ }
603
+
489
604
  if (insert.length === 0) {
490
605
  continue;
491
606
  }
492
607
 
493
- await this.createQueryBuilder(joinTable.name).insert(insert).execute();
608
+ // insert new relations
609
+ await this.createQueryBuilder(joinTable.name).insert(insert).transacting(trx).execute();
494
610
  }
495
611
  }
496
612
  },
@@ -504,8 +620,7 @@ const createEntityManager = (db) => {
504
620
  * @param {object} data - data received for creation
505
621
  */
506
622
  // TODO: check relation exists (handled by FKs except for polymorphics)
507
- // TODO: wrap Transaction
508
- async updateRelations(uid, id, data) {
623
+ async updateRelations(uid, id, data, { transaction: trx }) {
509
624
  const { attributes } = db.metadata.get(uid);
510
625
 
511
626
  for (const attributeName of Object.keys(attributes)) {
@@ -514,6 +629,7 @@ const createEntityManager = (db) => {
514
629
  if (attribute.type !== 'relation' || !has(attributeName, data)) {
515
630
  continue;
516
631
  }
632
+ const cleanRelationData = toAssocs(data[attributeName]);
517
633
 
518
634
  if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
519
635
  const { target, morphBy } = attribute;
@@ -529,12 +645,15 @@ const createEntityManager = (db) => {
529
645
  await this.createQueryBuilder(target)
530
646
  .update({ [idColumn.name]: null, [typeColumn.name]: null })
531
647
  .where({ [idColumn.name]: id, [typeColumn.name]: uid })
648
+ .transacting(trx)
532
649
  .execute();
533
650
 
534
- if (!isNull(data[attributeName])) {
651
+ if (!isNull(cleanRelationData.set)) {
652
+ const relId = toIds(cleanRelationData.set[0]);
535
653
  await this.createQueryBuilder(target)
536
654
  .update({ [idColumn.name]: id, [typeColumn.name]: uid })
537
- .where({ id: toId(data[attributeName]) })
655
+ .where({ id: relId })
656
+ .transacting(trx)
538
657
  .execute();
539
658
  }
540
659
  } else if (targetAttribute.relation === 'morphToMany') {
@@ -551,9 +670,14 @@ const createEntityManager = (db) => {
551
670
  ...(joinTable.on || {}),
552
671
  field: attributeName,
553
672
  })
673
+ .transacting(trx)
554
674
  .execute();
555
675
 
556
- const rows = toAssocs(data[attributeName]).map((data, idx) => ({
676
+ if (isEmpty(cleanRelationData.set)) {
677
+ continue;
678
+ }
679
+
680
+ const rows = cleanRelationData.set.map((data, idx) => ({
557
681
  [joinColumn.name]: data.id,
558
682
  [idColumn.name]: id,
559
683
  [typeColumn.name]: uid,
@@ -563,11 +687,7 @@ const createEntityManager = (db) => {
563
687
  field: attributeName,
564
688
  }));
565
689
 
566
- if (isEmpty(rows)) {
567
- continue;
568
- }
569
-
570
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
690
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
571
691
  }
572
692
 
573
693
  continue;
@@ -590,29 +710,32 @@ const createEntityManager = (db) => {
590
710
  [joinColumn.name]: id,
591
711
  ...(joinTable.on || {}),
592
712
  })
713
+ .transacting(trx)
593
714
  .execute();
594
715
 
595
- const rows = toAssocs(data[attributeName]).map((data) => ({
716
+ if (isEmpty(cleanRelationData.set)) {
717
+ continue;
718
+ }
719
+
720
+ const rows = cleanRelationData.set.map((data, idx) => ({
596
721
  [joinColumn.name]: id,
597
722
  [idColumn.name]: data.id,
598
723
  [typeColumn.name]: data[typeField],
599
724
  ...(joinTable.on || {}),
600
725
  ...(data.__pivot || {}),
726
+ order: idx + 1,
601
727
  }));
602
728
 
603
- if (isEmpty(rows)) {
604
- continue;
605
- }
606
-
607
729
  // delete previous relations
608
730
  await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
609
731
  uid,
610
732
  attributeName,
611
733
  joinTable,
612
734
  db,
735
+ transaction: trx,
613
736
  });
614
737
 
615
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
738
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
616
739
 
617
740
  continue;
618
741
  }
@@ -631,55 +754,243 @@ const createEntityManager = (db) => {
631
754
  await this.createQueryBuilder(target)
632
755
  .where({ [attribute.joinColumn.referencedColumn]: id })
633
756
  .update({ [attribute.joinColumn.referencedColumn]: null })
757
+ .transacting(trx)
634
758
  .execute();
635
759
 
636
- if (!isNull(data[attributeName])) {
760
+ if (!isNull(cleanRelationData.set)) {
761
+ const relIdsToAdd = toIds(cleanRelationData.set);
637
762
  await this.createQueryBuilder(target)
638
- // NOTE: works if it is an array or a single id
639
- .where({ id: data[attributeName] })
763
+ .where({ id: relIdsToAdd })
640
764
  .update({ [attribute.joinColumn.referencedColumn]: id })
765
+ .transacting(trx)
641
766
  .execute();
642
767
  }
643
768
  }
644
769
 
645
770
  if (attribute.joinTable) {
646
771
  const { joinTable } = attribute;
647
- const { joinColumn, inverseJoinColumn } = joinTable;
648
-
649
- // clear previous associations in the joinTable
650
- await this.createQueryBuilder(joinTable.name)
651
- .delete()
652
- .where({ [joinColumn.name]: id })
653
- .where(joinTable.on || {})
654
- .execute();
655
-
656
- if (
657
- isBidirectional(attribute) &&
658
- ['oneToOne', 'oneToMany'].includes(attribute.relation)
659
- ) {
660
- await this.createQueryBuilder(joinTable.name)
661
- .delete()
662
- .where({ [inverseJoinColumn.name]: toIds(data[attributeName]) })
663
- .where(joinTable.on || {})
664
- .execute();
772
+ const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } =
773
+ joinTable;
774
+ const select = [joinColumn.name, inverseJoinColumn.name];
775
+ if (hasOrderColumn(attribute)) {
776
+ select.push(orderColumnName);
777
+ }
778
+ if (hasInverseOrderColumn(attribute)) {
779
+ select.push(inverseOrderColumnName);
665
780
  }
666
781
 
667
- if (!isNull(data[attributeName])) {
668
- const insert = toAssocs(data[attributeName]).map((data) => {
669
- return {
782
+ // only delete relations
783
+ if (isNull(cleanRelationData.set)) {
784
+ await deleteRelations({ id, attribute, db, relIdsToDelete: 'all', transaction: trx });
785
+ } else {
786
+ const isPartialUpdate = !has('set', cleanRelationData);
787
+ let relIdsToaddOrMove;
788
+
789
+ if (isPartialUpdate) {
790
+ if (isAnyToOne(attribute)) {
791
+ cleanRelationData.connect = cleanRelationData.connect.slice(-1);
792
+ }
793
+ relIdsToaddOrMove = toIds(cleanRelationData.connect);
794
+ const relIdsToDelete = toIds(
795
+ differenceWith(isEqual, cleanRelationData.disconnect, cleanRelationData.connect)
796
+ );
797
+
798
+ if (!isEmpty(relIdsToDelete)) {
799
+ await deleteRelations({ id, attribute, db, relIdsToDelete, transaction: trx });
800
+ }
801
+
802
+ if (isEmpty(cleanRelationData.connect)) {
803
+ continue;
804
+ }
805
+
806
+ // Fetch current relations to handle ordering
807
+ let currentMovingRels;
808
+ if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
809
+ currentMovingRels = await this.createQueryBuilder(joinTable.name)
810
+ .select(select)
811
+ .where({
812
+ [joinColumn.name]: id,
813
+ [inverseJoinColumn.name]: { $in: relIdsToaddOrMove },
814
+ })
815
+ .where(joinTable.on || {})
816
+ .transacting(trx)
817
+ .execute();
818
+ }
819
+
820
+ // prepare relations to insert
821
+ const insert = cleanRelationData.connect.map((relToAdd) => ({
670
822
  [joinColumn.name]: id,
671
- [inverseJoinColumn.name]: data.id,
823
+ [inverseJoinColumn.name]: relToAdd.id,
672
824
  ...(joinTable.on || {}),
673
- ...(data.__pivot || {}),
674
- };
675
- });
825
+ ...(relToAdd.__pivot || {}),
826
+ }));
827
+
828
+ // add order value
829
+ if (hasOrderColumn(attribute)) {
830
+ const orderMax = (
831
+ await this.createQueryBuilder(joinTable.name)
832
+ .max(orderColumnName)
833
+ .where({ [joinColumn.name]: id })
834
+ .where(joinTable.on || {})
835
+ .first()
836
+ .transacting(trx)
837
+ .execute()
838
+ ).max;
839
+
840
+ insert.forEach((row, idx) => {
841
+ row[orderColumnName] = orderMax + idx + 1;
842
+ });
843
+ }
844
+
845
+ // add inv order value
846
+ if (hasInverseOrderColumn(attribute)) {
847
+ const nonExistingRelsIds = difference(
848
+ relIdsToaddOrMove,
849
+ map(inverseJoinColumn.name, currentMovingRels)
850
+ );
851
+
852
+ const maxResults = await db
853
+ .getConnection()
854
+ .select(inverseJoinColumn.name)
855
+ .max(inverseOrderColumnName, { as: 'max' })
856
+ .whereIn(inverseJoinColumn.name, nonExistingRelsIds)
857
+ .where(joinTable.on || {})
858
+ .groupBy(inverseJoinColumn.name)
859
+ .from(joinTable.name)
860
+ .transacting(trx);
861
+
862
+ const maxMap = maxResults.reduce(
863
+ (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }),
864
+ {}
865
+ );
866
+
867
+ insert.forEach((row) => {
868
+ row[inverseOrderColumnName] = (maxMap[row[inverseJoinColumn.name]] || 0) + 1;
869
+ });
870
+ }
871
+
872
+ // insert rows
873
+ const query = this.createQueryBuilder(joinTable.name)
874
+ .insert(insert)
875
+ .onConflict(joinTable.pivotColumns)
876
+ .transacting(trx);
877
+
878
+ if (hasOrderColumn(attribute)) {
879
+ query.merge([orderColumnName]);
880
+ } else {
881
+ query.ignore();
882
+ }
883
+
884
+ await query.execute();
885
+
886
+ // remove gap between orders
887
+ await cleanOrderColumns({ attribute, db, id, transaction: trx });
888
+ } else {
889
+ if (isAnyToOne(attribute)) {
890
+ cleanRelationData.set = cleanRelationData.set.slice(-1);
891
+ }
892
+ // overwrite all relations
893
+ relIdsToaddOrMove = toIds(cleanRelationData.set);
894
+ await deleteRelations({
895
+ id,
896
+ attribute,
897
+ db,
898
+ relIdsToDelete: 'all',
899
+ relIdsToNotDelete: relIdsToaddOrMove,
900
+ transaction: trx,
901
+ });
902
+
903
+ if (isEmpty(cleanRelationData.set)) {
904
+ continue;
905
+ }
906
+
907
+ const insert = cleanRelationData.set.map((relToAdd) => ({
908
+ [joinColumn.name]: id,
909
+ [inverseJoinColumn.name]: relToAdd.id,
910
+ ...(joinTable.on || {}),
911
+ ...(relToAdd.__pivot || {}),
912
+ }));
913
+
914
+ // add order value
915
+ if (hasOrderColumn(attribute)) {
916
+ insert.forEach((row, idx) => {
917
+ row[orderColumnName] = idx + 1;
918
+ });
919
+ }
920
+
921
+ // add inv order value
922
+ if (hasInverseOrderColumn(attribute)) {
923
+ const existingRels = await this.createQueryBuilder(joinTable.name)
924
+ .select(inverseJoinColumn.name)
925
+ .where({
926
+ [joinColumn.name]: id,
927
+ [inverseJoinColumn.name]: { $in: relIdsToaddOrMove },
928
+ })
929
+ .where(joinTable.on || {})
930
+ .transacting(trx)
931
+ .execute();
932
+
933
+ const nonExistingRelsIds = difference(
934
+ relIdsToaddOrMove,
935
+ map(inverseJoinColumn.name, existingRels)
936
+ );
937
+
938
+ const maxResults = await db
939
+ .getConnection()
940
+ .select(inverseJoinColumn.name)
941
+ .max(inverseOrderColumnName, { as: 'max' })
942
+ .whereIn(inverseJoinColumn.name, nonExistingRelsIds)
943
+ .where(joinTable.on || {})
944
+ .groupBy(inverseJoinColumn.name)
945
+ .from(joinTable.name)
946
+ .transacting(trx);
947
+
948
+ const maxMap = maxResults.reduce(
949
+ (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }),
950
+ {}
951
+ );
952
+
953
+ insert.forEach((row) => {
954
+ row[inverseOrderColumnName] = (maxMap[row[inverseJoinColumn.name]] || 0) + 1;
955
+ });
956
+ }
957
+
958
+ // insert rows
959
+ const query = this.createQueryBuilder(joinTable.name)
960
+ .insert(insert)
961
+ .onConflict(joinTable.pivotColumns)
962
+ .transacting(trx);
963
+
964
+ if (hasOrderColumn(attribute)) {
965
+ query.merge([orderColumnName]);
966
+ } else {
967
+ query.ignore();
968
+ }
969
+
970
+ await query.execute();
971
+ }
676
972
 
677
- // if there is nothing to insert
678
- if (insert.length === 0) {
679
- continue;
973
+ // Delete the previous relations for oneToAny relations
974
+ if (isBidirectional(attribute) && isOneToAny(attribute)) {
975
+ await deletePreviousOneToAnyRelations({
976
+ id,
977
+ attribute,
978
+ relIdsToadd: relIdsToaddOrMove,
979
+ db,
980
+ transaction: trx,
981
+ });
680
982
  }
681
983
 
682
- await this.createQueryBuilder(joinTable.name).insert(insert).execute();
984
+ // Delete the previous relations for anyToOne relations
985
+ if (isBidirectional(attribute) && isAnyToOne(attribute)) {
986
+ await deletePreviousAnyToOneRelations({
987
+ id,
988
+ attribute,
989
+ relIdToadd: relIdsToaddOrMove[0],
990
+ db,
991
+ transaction: trx,
992
+ });
993
+ }
683
994
  }
684
995
  }
685
996
  }
@@ -694,8 +1005,7 @@ const createEntityManager = (db) => {
694
1005
  * @param {Metadata} metadata - model metadta
695
1006
  * @param {ID} id - entity ID
696
1007
  */
697
- // TODO: wrap Transaction
698
- async deleteRelations(uid, id) {
1008
+ async deleteRelations(uid, id, { transaction: trx }) {
699
1009
  const { attributes } = db.metadata.get(uid);
700
1010
 
701
1011
  for (const attributeName of Object.keys(attributes)) {
@@ -724,6 +1034,7 @@ const createEntityManager = (db) => {
724
1034
  await this.createQueryBuilder(target)
725
1035
  .update({ [idColumn.name]: null, [typeColumn.name]: null })
726
1036
  .where({ [idColumn.name]: id, [typeColumn.name]: uid })
1037
+ .transacting(trx)
727
1038
  .execute();
728
1039
  } else if (targetAttribute.relation === 'morphToMany') {
729
1040
  const { joinTable } = targetAttribute;
@@ -739,6 +1050,7 @@ const createEntityManager = (db) => {
739
1050
  ...(joinTable.on || {}),
740
1051
  field: attributeName,
741
1052
  })
1053
+ .transacting(trx)
742
1054
  .execute();
743
1055
  }
744
1056
 
@@ -767,6 +1079,7 @@ const createEntityManager = (db) => {
767
1079
  [joinColumn.name]: id,
768
1080
  ...(joinTable.on || {}),
769
1081
  })
1082
+ .transacting(trx)
770
1083
  .execute();
771
1084
 
772
1085
  continue;
@@ -791,18 +1104,12 @@ const createEntityManager = (db) => {
791
1104
  await this.createQueryBuilder(target)
792
1105
  .where({ [attribute.joinColumn.referencedColumn]: id })
793
1106
  .update({ [attribute.joinColumn.referencedColumn]: null })
1107
+ .transacting(trx)
794
1108
  .execute();
795
1109
  }
796
1110
 
797
1111
  if (attribute.joinTable) {
798
- const { joinTable } = attribute;
799
- const { joinColumn } = joinTable;
800
-
801
- await this.createQueryBuilder(joinTable.name)
802
- .delete()
803
- .where({ [joinColumn.name]: id })
804
- .where(joinTable.on || {})
805
- .execute();
1112
+ await deleteRelations({ id, attribute, db, relIdsToDelete: 'all', transaction: trx });
806
1113
  }
807
1114
  }
808
1115
  },