@strapi/database 4.5.0-alpha.0 → 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,9 +12,12 @@ const {
12
12
  isEmpty,
13
13
  isArray,
14
14
  isNull,
15
- uniqBy,
16
- differenceBy,
17
- groupBy,
15
+ uniqWith,
16
+ isEqual,
17
+ differenceWith,
18
+ isNumber,
19
+ map,
20
+ difference,
18
21
  } = require('lodash/fp');
19
22
  const types = require('../types');
20
23
  const { createField } = require('../fields');
@@ -23,25 +26,24 @@ const { createRepository } = require('./entity-repository');
23
26
  const { deleteRelatedMorphOneRelationsAfterMorphToManyUpdate } = require('./morph-relations');
24
27
  const {
25
28
  isBidirectional,
26
- isOneToAny,
27
- isManyToAny,
28
29
  isAnyToOne,
29
- isAnyToMany,
30
+ isOneToAny,
31
+ hasOrderColumn,
32
+ hasInverseOrderColumn,
30
33
  } = require('../metadata/relations');
34
+ const {
35
+ deletePreviousOneToAnyRelations,
36
+ deletePreviousAnyToOneRelations,
37
+ deleteRelations,
38
+ cleanOrderColumns,
39
+ } = require('./regular-relations');
31
40
 
32
41
  const toId = (value) => value.id || value;
33
42
  const toIds = (value) => castArray(value || []).map(toId);
34
43
 
35
44
  const isValidId = (value) => isString(value) || isInteger(value);
36
- const toAssocs = (data) => {
37
- if (data?.connect || data?.disconnect || (isPlainObject(data) && !data.id)) {
38
- return {
39
- connect: toAssocs(data.connect),
40
- disconnect: toAssocs(data.disconnect),
41
- };
42
- }
43
-
44
- return castArray(data)
45
+ const toIdArray = (data) => {
46
+ const array = castArray(data)
45
47
  .filter((datum) => !isNil(datum))
46
48
  .map((datum) => {
47
49
  // if it is a string or an integer return an obj with id = to datum
@@ -56,6 +58,26 @@ const toAssocs = (data) => {
56
58
 
57
59
  return datum;
58
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
+ };
59
81
  };
60
82
 
61
83
  const processData = (metadata, data = {}, { withDefaults = false } = {}) => {
@@ -189,12 +211,24 @@ const createEntityManager = (db) => {
189
211
  }
190
212
 
191
213
  const dataToInsert = processData(metadata, data, { withDefaults: true });
214
+ let id;
215
+
216
+ const trx = await strapi.db.transaction();
217
+ try {
218
+ const res = await this.createQueryBuilder(uid)
219
+ .insert(dataToInsert)
220
+ .transacting(trx)
221
+ .execute();
192
222
 
193
- const res = await this.createQueryBuilder(uid).insert(dataToInsert).execute();
223
+ id = res[0].id || res[0];
194
224
 
195
- const id = res[0].id || res[0];
225
+ await this.attachRelations(uid, id, data, { transaction: trx });
196
226
 
197
- await this.attachRelations(uid, id, data);
227
+ await trx.commit();
228
+ } catch (e) {
229
+ await trx.rollback();
230
+ throw e;
231
+ }
198
232
 
199
233
  // TODO: in case there is no select or populate specified return the inserted data ?
200
234
  // TODO: do not trigger the findOne lifecycles ?
@@ -259,13 +293,25 @@ const createEntityManager = (db) => {
259
293
 
260
294
  const { id } = entity;
261
295
 
262
- const dataToUpdate = processData(metadata, data);
296
+ const trx = await strapi.db.transaction();
297
+ try {
298
+ const dataToUpdate = processData(metadata, data);
263
299
 
264
- if (!isEmpty(dataToUpdate)) {
265
- await this.createQueryBuilder(uid).where({ id }).update(dataToUpdate).execute();
266
- }
300
+ if (!isEmpty(dataToUpdate)) {
301
+ await this.createQueryBuilder(uid)
302
+ .where({ id })
303
+ .update(dataToUpdate)
304
+ .transacting(trx)
305
+ .execute();
306
+ }
267
307
 
268
- 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
+ }
269
315
 
270
316
  // TODO: do not trigger the findOne lifecycles ?
271
317
  const result = await this.findOne(uid, {
@@ -326,9 +372,17 @@ const createEntityManager = (db) => {
326
372
 
327
373
  const { id } = entity;
328
374
 
329
- 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();
330
378
 
331
- 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
+ }
332
386
 
333
387
  await db.lifecycles.run('afterDelete', uid, { params, result: entity }, states);
334
388
 
@@ -358,8 +412,7 @@ const createEntityManager = (db) => {
358
412
  * @param {ID} id - entity ID
359
413
  * @param {object} data - data received for creation
360
414
  */
361
- // TODO: wrap Transaction
362
- async attachRelations(uid, id, data) {
415
+ async attachRelations(uid, id, data, { transaction: trx }) {
363
416
  const { attributes } = db.metadata.get(uid);
364
417
 
365
418
  for (const attributeName of Object.keys(attributes)) {
@@ -371,6 +424,8 @@ const createEntityManager = (db) => {
371
424
  continue;
372
425
  }
373
426
 
427
+ const cleanRelationData = toAssocs(data[attributeName]);
428
+
374
429
  if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
375
430
  const { target, morphBy } = attribute;
376
431
 
@@ -380,9 +435,12 @@ const createEntityManager = (db) => {
380
435
  // set columns
381
436
  const { idColumn, typeColumn } = targetAttribute.morphColumn;
382
437
 
438
+ const relId = toId(cleanRelationData.set[0]);
439
+
383
440
  await this.createQueryBuilder(target)
384
441
  .update({ [idColumn.name]: id, [typeColumn.name]: uid })
385
- .where({ id: toId(data[attributeName]) })
442
+ .where({ id: relId })
443
+ .transacting(trx)
386
444
  .execute();
387
445
  } else if (targetAttribute.relation === 'morphToMany') {
388
446
  const { joinTable } = targetAttribute;
@@ -390,7 +448,11 @@ const createEntityManager = (db) => {
390
448
 
391
449
  const { idColumn, typeColumn } = morphColumn;
392
450
 
393
- 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) => {
394
456
  return {
395
457
  [joinColumn.name]: data.id,
396
458
  [idColumn.name]: id,
@@ -402,11 +464,7 @@ const createEntityManager = (db) => {
402
464
  };
403
465
  });
404
466
 
405
- if (isEmpty(rows)) {
406
- continue;
407
- }
408
-
409
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
467
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
410
468
  }
411
469
 
412
470
  continue;
@@ -419,40 +477,44 @@ const createEntityManager = (db) => {
419
477
 
420
478
  const { idColumn, typeColumn, typeField = '__type' } = morphColumn;
421
479
 
422
- const rows = toAssocs(data[attributeName]).map((data) => ({
480
+ if (isEmpty(cleanRelationData.set)) {
481
+ continue;
482
+ }
483
+
484
+ const rows = cleanRelationData.set.map((data, idx) => ({
423
485
  [joinColumn.name]: id,
424
486
  [idColumn.name]: data.id,
425
487
  [typeColumn.name]: data[typeField],
426
488
  ...(joinTable.on || {}),
427
489
  ...(data.__pivot || {}),
490
+ order: idx + 1,
428
491
  }));
429
492
 
430
- if (isEmpty(rows)) {
431
- continue;
432
- }
433
-
434
493
  // delete previous relations
435
494
  await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
436
495
  uid,
437
496
  attributeName,
438
497
  joinTable,
439
498
  db,
499
+ transaction: trx,
440
500
  });
441
501
 
442
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
502
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
443
503
 
444
504
  continue;
445
505
  }
446
506
 
447
507
  if (attribute.joinColumn && attribute.owner) {
508
+ const relIdsToAdd = toIds(cleanRelationData.set);
448
509
  if (
449
510
  attribute.relation === 'oneToOne' &&
450
511
  isBidirectional(attribute) &&
451
- data[attributeName]
512
+ relIdsToAdd.length
452
513
  ) {
453
514
  await this.createQueryBuilder(uid)
454
- .where({ [attribute.joinColumn.name]: data[attributeName], id: { $ne: id } })
515
+ .where({ [attribute.joinColumn.name]: relIdsToAdd, id: { $ne: id } })
455
516
  .update({ [attribute.joinColumn.name]: null })
517
+ .transacting(trx)
456
518
  .execute();
457
519
  }
458
520
 
@@ -465,16 +527,19 @@ const createEntityManager = (db) => {
465
527
  const { target } = attribute;
466
528
 
467
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);
468
531
 
469
532
  await this.createQueryBuilder(target)
470
533
  .where({ [attribute.joinColumn.referencedColumn]: id })
471
534
  .update({ [attribute.joinColumn.referencedColumn]: null })
535
+ .transacting(trx)
472
536
  .execute();
473
537
 
474
538
  await this.createQueryBuilder(target)
475
539
  .update({ [attribute.joinColumn.referencedColumn]: id })
476
540
  // NOTE: works if it is an array or a single id
477
- .where({ id: data[attributeName] })
541
+ .where({ id: relIdsToAdd })
542
+ .transacting(trx)
478
543
  .execute();
479
544
  }
480
545
 
@@ -484,50 +549,18 @@ const createEntityManager = (db) => {
484
549
  const { joinTable } = attribute;
485
550
  const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } =
486
551
  joinTable;
487
- const select = [joinColumn.name];
488
- if (isAnyToMany(attribute)) {
489
- select.push(orderColumnName);
490
- }
491
552
 
492
- const cleanRelationData = toAssocs(data[attributeName]);
493
- const relsToAdd = uniqBy('id', cleanRelationData.connect || cleanRelationData);
553
+ const relsToAdd = cleanRelationData.set || cleanRelationData.connect;
494
554
  const relIdsToadd = toIds(relsToAdd);
495
555
 
496
- // need to delete the previous relations for oneToAny relations
497
556
  if (isBidirectional(attribute) && isOneToAny(attribute)) {
498
- // update orders for previous oneToAny relations that will be deleted if it has order (oneToMany)
499
- if (isAnyToMany(attribute)) {
500
- const currentRelsToDelete = await this.createQueryBuilder(joinTable.name)
501
- .select(select)
502
- .where({
503
- [inverseJoinColumn.name]: relIdsToadd,
504
- [joinColumn.name]: { $ne: id },
505
- })
506
- .where(joinTable.on || {})
507
- .execute();
508
-
509
- currentRelsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]);
510
-
511
- for (const relToDelete of currentRelsToDelete) {
512
- if (relToDelete[orderColumnName] !== null) {
513
- await this.createQueryBuilder(joinTable.name)
514
- .decrement(orderColumnName, 1)
515
- .where({
516
- [joinColumn.name]: relToDelete[joinColumn.name],
517
- [orderColumnName]: { $gt: relToDelete[orderColumnName] },
518
- })
519
- .where(joinTable.on || {})
520
- .execute();
521
- }
522
- }
523
- }
524
-
525
- // delete previous oneToAny relations
526
- await this.createQueryBuilder(joinTable.name)
527
- .delete()
528
- .where({ [inverseJoinColumn.name]: relIdsToadd })
529
- .where(joinTable.on || {})
530
- .execute();
557
+ await deletePreviousOneToAnyRelations({
558
+ id,
559
+ attribute,
560
+ relIdsToadd,
561
+ db,
562
+ transaction: trx,
563
+ });
531
564
  }
532
565
 
533
566
  // prepare new relations to insert
@@ -540,30 +573,31 @@ const createEntityManager = (db) => {
540
573
  };
541
574
  });
542
575
 
543
- // add order value when relevant
544
- if (isAnyToMany(attribute)) {
576
+ // add order value
577
+ if (hasOrderColumn(attribute)) {
545
578
  insert.forEach((rel, idx) => {
546
579
  rel[orderColumnName] = idx + 1;
547
580
  });
548
581
  }
549
- // add inv_order value when relevant
550
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
551
- const maxMap = {};
552
- await Promise.all(
553
- relIdsToadd.map(async (relId) => {
554
- const { max } = await this.createQueryBuilder(joinTable.name)
555
- .max(inverseOrderColumnName)
556
- .where({ [inverseJoinColumn.name]: relId })
557
- .where(joinTable.on || {})
558
- .first()
559
- .execute();
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);
560
593
 
561
- maxMap[relId] = max;
562
- })
594
+ const maxMap = maxResults.reduce(
595
+ (acc, res) => Object.assign(acc, { [res[inverseJoinColumn.name]]: res.max }),
596
+ {}
563
597
  );
564
598
 
565
599
  insert.forEach((rel) => {
566
- rel[inverseOrderColumnName] = maxMap[rel[inverseJoinColumn.name]] + 1;
600
+ rel[inverseOrderColumnName] = (maxMap[rel[inverseJoinColumn.name]] || 0) + 1;
567
601
  });
568
602
  }
569
603
 
@@ -572,7 +606,7 @@ const createEntityManager = (db) => {
572
606
  }
573
607
 
574
608
  // insert new relations
575
- await this.createQueryBuilder(joinTable.name).insert(insert).execute();
609
+ await this.createQueryBuilder(joinTable.name).insert(insert).transacting(trx).execute();
576
610
  }
577
611
  }
578
612
  },
@@ -586,8 +620,7 @@ const createEntityManager = (db) => {
586
620
  * @param {object} data - data received for creation
587
621
  */
588
622
  // TODO: check relation exists (handled by FKs except for polymorphics)
589
- // TODO: wrap Transaction
590
- async updateRelations(uid, id, data) {
623
+ async updateRelations(uid, id, data, { transaction: trx }) {
591
624
  const { attributes } = db.metadata.get(uid);
592
625
 
593
626
  for (const attributeName of Object.keys(attributes)) {
@@ -596,6 +629,7 @@ const createEntityManager = (db) => {
596
629
  if (attribute.type !== 'relation' || !has(attributeName, data)) {
597
630
  continue;
598
631
  }
632
+ const cleanRelationData = toAssocs(data[attributeName]);
599
633
 
600
634
  if (attribute.relation === 'morphOne' || attribute.relation === 'morphMany') {
601
635
  const { target, morphBy } = attribute;
@@ -611,12 +645,15 @@ const createEntityManager = (db) => {
611
645
  await this.createQueryBuilder(target)
612
646
  .update({ [idColumn.name]: null, [typeColumn.name]: null })
613
647
  .where({ [idColumn.name]: id, [typeColumn.name]: uid })
648
+ .transacting(trx)
614
649
  .execute();
615
650
 
616
- if (!isNull(data[attributeName])) {
651
+ if (!isNull(cleanRelationData.set)) {
652
+ const relId = toIds(cleanRelationData.set[0]);
617
653
  await this.createQueryBuilder(target)
618
654
  .update({ [idColumn.name]: id, [typeColumn.name]: uid })
619
- .where({ id: toId(data[attributeName]) })
655
+ .where({ id: relId })
656
+ .transacting(trx)
620
657
  .execute();
621
658
  }
622
659
  } else if (targetAttribute.relation === 'morphToMany') {
@@ -633,9 +670,14 @@ const createEntityManager = (db) => {
633
670
  ...(joinTable.on || {}),
634
671
  field: attributeName,
635
672
  })
673
+ .transacting(trx)
636
674
  .execute();
637
675
 
638
- 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) => ({
639
681
  [joinColumn.name]: data.id,
640
682
  [idColumn.name]: id,
641
683
  [typeColumn.name]: uid,
@@ -645,11 +687,7 @@ const createEntityManager = (db) => {
645
687
  field: attributeName,
646
688
  }));
647
689
 
648
- if (isEmpty(rows)) {
649
- continue;
650
- }
651
-
652
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
690
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
653
691
  }
654
692
 
655
693
  continue;
@@ -672,29 +710,32 @@ const createEntityManager = (db) => {
672
710
  [joinColumn.name]: id,
673
711
  ...(joinTable.on || {}),
674
712
  })
713
+ .transacting(trx)
675
714
  .execute();
676
715
 
677
- const rows = toAssocs(data[attributeName]).map((data) => ({
716
+ if (isEmpty(cleanRelationData.set)) {
717
+ continue;
718
+ }
719
+
720
+ const rows = cleanRelationData.set.map((data, idx) => ({
678
721
  [joinColumn.name]: id,
679
722
  [idColumn.name]: data.id,
680
723
  [typeColumn.name]: data[typeField],
681
724
  ...(joinTable.on || {}),
682
725
  ...(data.__pivot || {}),
726
+ order: idx + 1,
683
727
  }));
684
728
 
685
- if (isEmpty(rows)) {
686
- continue;
687
- }
688
-
689
729
  // delete previous relations
690
730
  await deleteRelatedMorphOneRelationsAfterMorphToManyUpdate(rows, {
691
731
  uid,
692
732
  attributeName,
693
733
  joinTable,
694
734
  db,
735
+ transaction: trx,
695
736
  });
696
737
 
697
- await this.createQueryBuilder(joinTable.name).insert(rows).execute();
738
+ await this.createQueryBuilder(joinTable.name).insert(rows).transacting(trx).execute();
698
739
 
699
740
  continue;
700
741
  }
@@ -713,13 +754,15 @@ const createEntityManager = (db) => {
713
754
  await this.createQueryBuilder(target)
714
755
  .where({ [attribute.joinColumn.referencedColumn]: id })
715
756
  .update({ [attribute.joinColumn.referencedColumn]: null })
757
+ .transacting(trx)
716
758
  .execute();
717
759
 
718
- if (!isNull(data[attributeName])) {
760
+ if (!isNull(cleanRelationData.set)) {
761
+ const relIdsToAdd = toIds(cleanRelationData.set);
719
762
  await this.createQueryBuilder(target)
720
- // NOTE: works if it is an array or a single id
721
- .where({ id: data[attributeName] })
763
+ .where({ id: relIdsToAdd })
722
764
  .update({ [attribute.joinColumn.referencedColumn]: id })
765
+ .transacting(trx)
723
766
  .execute();
724
767
  }
725
768
  }
@@ -729,430 +772,224 @@ const createEntityManager = (db) => {
729
772
  const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } =
730
773
  joinTable;
731
774
  const select = [joinColumn.name, inverseJoinColumn.name];
732
- if (isAnyToMany(attribute)) {
775
+ if (hasOrderColumn(attribute)) {
733
776
  select.push(orderColumnName);
734
777
  }
735
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
778
+ if (hasInverseOrderColumn(attribute)) {
736
779
  select.push(inverseOrderColumnName);
737
780
  }
738
781
 
739
782
  // only delete relations
740
- if (isNull(data[attributeName])) {
741
- // INVERSE ORDER UPDATE
742
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
743
- let lastId = 0;
744
- let done = false;
745
- const batchSize = 100;
746
- while (!done) {
747
- const relsToDelete = await this.createQueryBuilder(joinTable.name)
748
- .select(select)
749
- .where({
750
- [joinColumn.name]: id,
751
- id: { $gt: lastId },
752
- })
753
- .where(joinTable.on || {})
754
- .orderBy('id')
755
- .limit(batchSize)
756
- .execute();
757
- // TODO: cannot put pivot here...
758
- done = relsToDelete.length < batchSize;
759
- lastId = relsToDelete[relsToDelete.length - 1]?.id;
760
-
761
- const updateInverseOrderPromises = [];
762
- for (const relToDelete of relsToDelete) {
763
- if (relToDelete[inverseOrderColumnName] !== null) {
764
- const updatePromise = this.createQueryBuilder(joinTable.name)
765
- .decrement(inverseOrderColumnName, 1)
766
- .where({
767
- [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name],
768
- [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] },
769
- })
770
- .where(joinTable.on || {})
771
- .execute();
772
- updateInverseOrderPromises.push(updatePromise);
773
- }
774
- }
775
-
776
- await Promise.all(updateInverseOrderPromises);
777
- }
778
- }
779
-
780
- await this.createQueryBuilder(joinTable.name)
781
- .delete()
782
- .where({ [joinColumn.name]: id })
783
- .where(joinTable.on || {})
784
- .execute();
783
+ if (isNull(cleanRelationData.set)) {
784
+ await deleteRelations({ id, attribute, db, relIdsToDelete: 'all', transaction: trx });
785
785
  } else {
786
- const cleanRelationData = toAssocs(data[attributeName]);
787
- const isPartialUpdate =
788
- has('connect', cleanRelationData) || has('disconnect', cleanRelationData);
786
+ const isPartialUpdate = !has('set', cleanRelationData);
789
787
  let relIdsToaddOrMove;
790
788
 
791
789
  if (isPartialUpdate) {
792
- // does not support pivot
793
- let connect = uniqBy('id', cleanRelationData.connect || []);
794
- let disconnect = uniqBy('id', cleanRelationData.disconnect || []);
795
790
  if (isAnyToOne(attribute)) {
796
- connect = connect.slice(-1);
797
- disconnect = [];
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;
798
804
  }
799
- relIdsToaddOrMove = toIds(connect);
800
- // DELETE relations in disconnect
801
- const relIdsToDelete = toIds(differenceBy('id', disconnect, connect));
802
-
803
- // UPDATE RELEVANT ORDERS
804
- if (
805
- isAnyToMany(attribute) ||
806
- (isBidirectional(attribute) && isManyToAny(attribute))
807
- ) {
808
- const relsToDelete = await this.createQueryBuilder(joinTable.name)
805
+
806
+ // Fetch current relations to handle ordering
807
+ let currentMovingRels;
808
+ if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
809
+ currentMovingRels = await this.createQueryBuilder(joinTable.name)
809
810
  .select(select)
810
811
  .where({
811
812
  [joinColumn.name]: id,
812
- [inverseJoinColumn.name]: { $in: relIdsToDelete },
813
+ [inverseJoinColumn.name]: { $in: relIdsToaddOrMove },
813
814
  })
814
815
  .where(joinTable.on || {})
816
+ .transacting(trx)
815
817
  .execute();
816
-
817
- // ORDER UPDATE
818
- if (isAnyToMany(attribute)) {
819
- // sort by order DESC so that the order updates are done in the correct order
820
- // avoiding one to interfere with the others
821
- relsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]);
822
-
823
- for (const relToDelete of relsToDelete) {
824
- if (relToDelete[orderColumnName] !== null) {
825
- await this.createQueryBuilder(joinTable.name)
826
- .decrement(orderColumnName, 1)
827
- .where({
828
- [joinColumn.name]: id,
829
- [orderColumnName]: { $gt: relToDelete[orderColumnName] },
830
- })
831
- .where(joinTable.on || {})
832
- .execute();
833
- }
834
- }
835
- }
836
-
837
- // INVERSE ORDER UPDATE
838
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
839
- const updateInverseOrderPromises = [];
840
- for (const relToDelete of relsToDelete) {
841
- if (relToDelete[inverseOrderColumnName] !== null) {
842
- const updatePromise = this.createQueryBuilder(joinTable.name)
843
- .decrement(inverseOrderColumnName, 1)
844
- .where({
845
- [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name],
846
- [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] },
847
- })
848
- .where(joinTable.on || {})
849
- .execute();
850
- updateInverseOrderPromises.push(updatePromise);
851
- }
852
- }
853
-
854
- await Promise.all(updateInverseOrderPromises);
855
- }
856
818
  }
857
819
 
858
- // DELETE
859
- await this.createQueryBuilder(joinTable.name)
860
- .delete()
861
- .where({
862
- [joinColumn.name]: id,
863
- [inverseJoinColumn.name]: { $in: relIdsToDelete },
864
- })
865
- .where(joinTable.on || {})
866
- .execute();
867
-
868
- // add/move
869
- let max;
870
- const currentMovingRels = await this.createQueryBuilder(joinTable.name)
871
- .select(select)
872
- .where({
873
- [joinColumn.name]: id,
874
- [inverseJoinColumn.name]: { $in: relIdsToaddOrMove },
875
- })
876
- .where(joinTable.on || {})
877
- .execute();
878
- const currentMovingRelsMap = groupBy(inverseJoinColumn.name, currentMovingRels);
820
+ // prepare relations to insert
821
+ const insert = cleanRelationData.connect.map((relToAdd) => ({
822
+ [joinColumn.name]: id,
823
+ [inverseJoinColumn.name]: relToAdd.id,
824
+ ...(joinTable.on || {}),
825
+ ...(relToAdd.__pivot || {}),
826
+ }));
879
827
 
880
- if (isAnyToMany(attribute)) {
881
- max = (
828
+ // add order value
829
+ if (hasOrderColumn(attribute)) {
830
+ const orderMax = (
882
831
  await this.createQueryBuilder(joinTable.name)
883
832
  .max(orderColumnName)
884
833
  .where({ [joinColumn.name]: id })
885
834
  .where(joinTable.on || {})
886
835
  .first()
836
+ .transacting(trx)
887
837
  .execute()
888
838
  ).max;
839
+
840
+ insert.forEach((row, idx) => {
841
+ row[orderColumnName] = orderMax + idx + 1;
842
+ });
889
843
  }
890
844
 
891
- for (const relToAddOrMove of connect) {
892
- // const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0];
893
- const currentRel = currentMovingRelsMap[relToAddOrMove.id]?.[0];
894
- if (currentRel && isAnyToMany(attribute)) {
895
- const currentOrderIsNull = currentRel[orderColumnName] === null;
896
- if (!currentOrderIsNull) {
897
- await this.createQueryBuilder(joinTable.name)
898
- .decrement(orderColumnName, 1)
899
- .where({
900
- [joinColumn.name]: id,
901
- [orderColumnName]: { $gt: currentRel[orderColumnName] },
902
- })
903
- .where(joinTable.on || {})
904
- .execute();
905
-
906
- currentMovingRels.forEach((rel) => {
907
- if (rel[orderColumnName] > currentRel[orderColumnName]) {
908
- rel[orderColumnName] -= 1;
909
- }
910
- });
911
- }
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
+ }
912
871
 
913
- await this.createQueryBuilder(joinTable.name)
914
- .update({
915
- [orderColumnName]: currentOrderIsNull ? max + 1 : max,
916
- })
917
- .where({
918
- [joinColumn.name]: id,
919
- [inverseJoinColumn.name]: relToAddOrMove.id,
920
- })
921
- .where(joinTable.on || {})
922
- .execute();
923
- } else if (!currentRel) {
924
- const insert = {
925
- [joinColumn.name]: id,
926
- [inverseJoinColumn.name]: relToAddOrMove.id,
927
- ...(relToAddOrMove.__pivot || {}),
928
- };
929
-
930
- if (isAnyToMany(attribute)) {
931
- insert[orderColumnName] = max + 1;
932
- }
933
-
934
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
935
- const { max: reverseMax } = await this.createQueryBuilder(joinTable.name)
936
- .max(inverseOrderColumnName)
937
- .where({ [inverseJoinColumn.name]: relToAddOrMove.id })
938
- .where(joinTable.on || {})
939
- .first()
940
- .execute();
941
-
942
- insert[inverseOrderColumnName] = reverseMax + 1;
943
- }
944
-
945
- await this.createQueryBuilder(joinTable.name).insert(insert).execute();
946
- max += 1;
947
- }
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();
948
882
  }
883
+
884
+ await query.execute();
885
+
886
+ // remove gap between orders
887
+ await cleanOrderColumns({ attribute, db, id, transaction: trx });
949
888
  } else {
889
+ if (isAnyToOne(attribute)) {
890
+ cleanRelationData.set = cleanRelationData.set.slice(-1);
891
+ }
950
892
  // overwrite all relations
951
- const relsToAdd = uniqBy('id', cleanRelationData);
952
- relIdsToaddOrMove = toIds(relsToAdd);
953
-
954
- // UPDATE RELEVANT ORDERS BEFORE DELETE
955
- if (isAnyToMany(attribute) || isManyToAny(attribute)) {
956
- let lastId = 0;
957
- let done = false;
958
- const batchSize = 100;
959
- while (!done) {
960
- const relsToDelete = await this.createQueryBuilder(joinTable.name)
961
- .select(select)
962
- .where({
963
- [joinColumn.name]: id,
964
- [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove },
965
- id: { $gt: lastId },
966
- })
967
- .where(joinTable.on || {})
968
- .orderBy('id')
969
- .limit(batchSize)
970
- .execute();
971
-
972
- done = relsToDelete.length < batchSize;
973
- lastId = relsToDelete[relsToDelete.length - 1]?.id;
974
-
975
- // ORDER UPDATE
976
- if (isAnyToMany(attribute)) {
977
- // sort by order DESC so that the order updates are done in the correct order
978
- // avoiding one to interfere with the others
979
- relsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]);
980
-
981
- for (const relToDelete of relsToDelete) {
982
- if (relToDelete[orderColumnName] !== null) {
983
- await this.createQueryBuilder(joinTable.name)
984
- .decrement(orderColumnName, 1)
985
- .where({
986
- [joinColumn.name]: id,
987
- [orderColumnName]: { $gt: relToDelete[orderColumnName] },
988
- })
989
- .where(joinTable.on || {})
990
- // manque le pivot ici
991
- .execute();
992
- }
993
- }
994
- }
995
-
996
- // INVERSE ORDER UPDATE
997
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
998
- const updateInverseOrderPromises = [];
999
- for (const relToDelete of relsToDelete) {
1000
- if (relToDelete[inverseOrderColumnName] !== null) {
1001
- const updatePromise = this.createQueryBuilder(joinTable.name)
1002
- .decrement(inverseOrderColumnName, 1)
1003
- .where({
1004
- [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name],
1005
- [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] },
1006
- })
1007
- .where(joinTable.on || {})
1008
- .execute();
1009
- updateInverseOrderPromises.push(updatePromise);
1010
- }
1011
- }
1012
-
1013
- await Promise.all(updateInverseOrderPromises);
1014
- }
1015
- }
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;
1016
905
  }
1017
906
 
1018
- // DELETE
1019
- await this.createQueryBuilder(joinTable.name)
1020
- .delete()
1021
- .where({
1022
- [joinColumn.name]: id,
1023
- [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove },
1024
- })
1025
- .where(joinTable.on || {})
1026
- .execute();
1027
-
1028
- const currentMovingRels = await this.createQueryBuilder(joinTable.name)
1029
- .select(select)
1030
- .where({
1031
- [joinColumn.name]: id,
1032
- [inverseJoinColumn.name]: { $in: relIdsToaddOrMove },
1033
- })
1034
- .where(joinTable.on || {})
1035
- .execute();
1036
- const currentMovingRelsMap = groupBy(inverseJoinColumn.name, currentMovingRels);
1037
-
1038
- let max = 0;
1039
- for (const relToAdd of relsToAdd) {
1040
- const currentRel = currentMovingRelsMap[relToAdd.id]?.[0];
1041
-
1042
- if (currentRel && isAnyToMany(attribute)) {
1043
- const update = { [orderColumnName]: max + 1 };
1044
- await this.createQueryBuilder(joinTable.name)
1045
- .update(update)
1046
- .where({
1047
- [joinColumn.name]: id,
1048
- [inverseJoinColumn.name]: relToAdd.id,
1049
- })
1050
- .where(joinTable.on || {})
1051
- .execute();
1052
- } else if (!currentRel) {
1053
- const insert = {
1054
- [joinColumn.name]: id,
1055
- [inverseJoinColumn.name]: relToAdd.id,
1056
- ...(relToAdd.__pivot || {}),
1057
- };
1058
-
1059
- if (isAnyToMany(attribute)) {
1060
- insert[orderColumnName] = max + 1;
1061
- }
1062
- // can be optimized in one query
1063
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
1064
- const { max: reverseMax } = await this.createQueryBuilder(joinTable.name)
1065
- .max(inverseOrderColumnName)
1066
- .where({ [inverseJoinColumn.name]: id })
1067
- .where(joinTable.on || {})
1068
- .first()
1069
- .execute();
1070
-
1071
- insert[inverseOrderColumnName] = reverseMax + 1;
1072
- }
1073
-
1074
- await this.createQueryBuilder(joinTable.name).insert(insert).execute();
1075
- }
1076
- max += 1;
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
+ });
1077
919
  }
1078
- }
1079
920
 
1080
- // Delete the previous relations for oneToAny relations
1081
- if (isBidirectional(attribute) && isOneToAny(attribute)) {
1082
- // update orders for previous oneToAny relations that will be deleted if it has order (oneToMany)
1083
- if (isAnyToMany(attribute)) {
1084
- const currentRelsToDelete = await this.createQueryBuilder(joinTable.name)
1085
- .select(select)
921
+ // add inv order value
922
+ if (hasInverseOrderColumn(attribute)) {
923
+ const existingRels = await this.createQueryBuilder(joinTable.name)
924
+ .select(inverseJoinColumn.name)
1086
925
  .where({
1087
- [inverseJoinColumn.name]: relIdsToaddOrMove,
1088
- [joinColumn.name]: { $ne: id },
926
+ [joinColumn.name]: id,
927
+ [inverseJoinColumn.name]: { $in: relIdsToaddOrMove },
1089
928
  })
1090
929
  .where(joinTable.on || {})
930
+ .transacting(trx)
1091
931
  .execute();
1092
932
 
1093
- currentRelsToDelete.sort((a, b) => b[orderColumnName] - a[orderColumnName]);
1094
-
1095
- for (const relToDelete of currentRelsToDelete) {
1096
- if (relToDelete[orderColumnName] !== null) {
1097
- await this.createQueryBuilder(joinTable.name)
1098
- .decrement(orderColumnName, 1)
1099
- .where({
1100
- [joinColumn.name]: relToDelete[joinColumn.name],
1101
- [orderColumnName]: { $gt: relToDelete[orderColumnName] },
1102
- })
1103
- .where(joinTable.on || {})
1104
- .execute();
1105
- }
1106
- }
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();
1107
968
  }
1108
969
 
1109
- // delete previous oneToAny relations
1110
- await this.createQueryBuilder(joinTable.name)
1111
- .delete()
1112
- .where({
1113
- [inverseJoinColumn.name]: relIdsToaddOrMove,
1114
- [joinColumn.name]: { $ne: id },
1115
- })
1116
- .where(joinTable.on || {})
1117
- .execute();
970
+ await query.execute();
971
+ }
972
+
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
+ });
1118
982
  }
1119
983
 
1120
984
  // Delete the previous relations for anyToOne relations
1121
985
  if (isBidirectional(attribute) && isAnyToOne(attribute)) {
1122
- // update orders for previous anyToOne relations that will be deleted if it has order (manyToOne)
1123
- if (isManyToAny(attribute)) {
1124
- const currentRelsToDelete = await this.createQueryBuilder(joinTable.name)
1125
- .select(select)
1126
- .where({
1127
- [joinColumn.name]: id,
1128
- [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove },
1129
- })
1130
- .where(joinTable.on || {})
1131
- .execute();
1132
-
1133
- for (const relToDelete of currentRelsToDelete) {
1134
- if (relToDelete[inverseOrderColumnName] !== null) {
1135
- await this.createQueryBuilder(joinTable.name)
1136
- .decrement(inverseOrderColumnName, 1)
1137
- .where({
1138
- [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name],
1139
- [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] },
1140
- })
1141
- .where(joinTable.on || {})
1142
- .execute();
1143
- }
1144
- }
1145
- }
1146
-
1147
- // delete previous oneToAny relations
1148
- await this.createQueryBuilder(joinTable.name)
1149
- .delete()
1150
- .where({
1151
- [joinColumn.name]: id,
1152
- [inverseJoinColumn.name]: { $notIn: relIdsToaddOrMove },
1153
- })
1154
- .where(joinTable.on || {})
1155
- .execute();
986
+ await deletePreviousAnyToOneRelations({
987
+ id,
988
+ attribute,
989
+ relIdToadd: relIdsToaddOrMove[0],
990
+ db,
991
+ transaction: trx,
992
+ });
1156
993
  }
1157
994
  }
1158
995
  }
@@ -1168,8 +1005,7 @@ const createEntityManager = (db) => {
1168
1005
  * @param {Metadata} metadata - model metadta
1169
1006
  * @param {ID} id - entity ID
1170
1007
  */
1171
- // TODO: wrap Transaction
1172
- async deleteRelations(uid, id) {
1008
+ async deleteRelations(uid, id, { transaction: trx }) {
1173
1009
  const { attributes } = db.metadata.get(uid);
1174
1010
 
1175
1011
  for (const attributeName of Object.keys(attributes)) {
@@ -1198,6 +1034,7 @@ const createEntityManager = (db) => {
1198
1034
  await this.createQueryBuilder(target)
1199
1035
  .update({ [idColumn.name]: null, [typeColumn.name]: null })
1200
1036
  .where({ [idColumn.name]: id, [typeColumn.name]: uid })
1037
+ .transacting(trx)
1201
1038
  .execute();
1202
1039
  } else if (targetAttribute.relation === 'morphToMany') {
1203
1040
  const { joinTable } = targetAttribute;
@@ -1213,6 +1050,7 @@ const createEntityManager = (db) => {
1213
1050
  ...(joinTable.on || {}),
1214
1051
  field: attributeName,
1215
1052
  })
1053
+ .transacting(trx)
1216
1054
  .execute();
1217
1055
  }
1218
1056
 
@@ -1241,6 +1079,7 @@ const createEntityManager = (db) => {
1241
1079
  [joinColumn.name]: id,
1242
1080
  ...(joinTable.on || {}),
1243
1081
  })
1082
+ .transacting(trx)
1244
1083
  .execute();
1245
1084
 
1246
1085
  continue;
@@ -1265,56 +1104,12 @@ const createEntityManager = (db) => {
1265
1104
  await this.createQueryBuilder(target)
1266
1105
  .where({ [attribute.joinColumn.referencedColumn]: id })
1267
1106
  .update({ [attribute.joinColumn.referencedColumn]: null })
1107
+ .transacting(trx)
1268
1108
  .execute();
1269
1109
  }
1270
1110
 
1271
1111
  if (attribute.joinTable) {
1272
- const { joinTable } = attribute;
1273
- const { joinColumn, inverseJoinColumn, inverseOrderColumnName } = joinTable;
1274
-
1275
- // INVERSE ORDER UPDATE
1276
- if (isBidirectional(attribute) && isManyToAny(attribute)) {
1277
- let lastId = 0;
1278
- let done = false;
1279
- const batchSize = 100;
1280
- while (!done) {
1281
- const relsToDelete = await this.createQueryBuilder(joinTable.name)
1282
- .select(inverseJoinColumn.name, inverseOrderColumnName)
1283
- .where({
1284
- [joinColumn.name]: id,
1285
- id: { $gt: lastId },
1286
- })
1287
- .where(joinTable.on || {})
1288
- .orderBy('id')
1289
- .limit(batchSize)
1290
- .execute();
1291
- done = relsToDelete.length < batchSize;
1292
- lastId = relsToDelete[relsToDelete.length - 1]?.id;
1293
-
1294
- const updateInverseOrderPromises = [];
1295
- for (const relToDelete of relsToDelete) {
1296
- if (relToDelete[inverseOrderColumnName] !== null) {
1297
- const updatePromise = this.createQueryBuilder(joinTable.name)
1298
- .decrement(inverseOrderColumnName, 1)
1299
- .where({
1300
- [inverseJoinColumn.name]: relToDelete[inverseJoinColumn.name],
1301
- [inverseOrderColumnName]: { $gt: relToDelete[inverseOrderColumnName] },
1302
- })
1303
- .where(joinTable.on || {})
1304
- .execute();
1305
- updateInverseOrderPromises.push(updatePromise);
1306
- }
1307
- }
1308
-
1309
- await Promise.all(updateInverseOrderPromises);
1310
- }
1311
- }
1312
-
1313
- await this.createQueryBuilder(joinTable.name)
1314
- .delete()
1315
- .where({ [joinColumn.name]: id })
1316
- .where(joinTable.on || {})
1317
- .execute();
1112
+ await deleteRelations({ id, attribute, db, relIdsToDelete: 'all', transaction: trx });
1318
1113
  }
1319
1114
  }
1320
1115
  },