@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.
@@ -20,7 +20,7 @@ const getMorphToManyRowsLinkedToMorphOne = (rows, { uid, attributeName, typeColu
20
20
 
21
21
  const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (
22
22
  rows,
23
- { uid, attributeName, joinTable, db }
23
+ { uid, attributeName, joinTable, db, transaction: trx }
24
24
  ) => {
25
25
  const { morphColumn } = joinTable;
26
26
  const { idColumn, typeColumn } = morphColumn;
@@ -50,7 +50,11 @@ const deleteRelatedMorphOneRelationsAfterMorphToManyUpdate = async (
50
50
  }
51
51
 
52
52
  if (!isEmpty(orWhere)) {
53
- await createQueryBuilder(joinTable.name, db).delete().where({ $or: orWhere }).execute();
53
+ await createQueryBuilder(joinTable.name, db)
54
+ .delete()
55
+ .where({ $or: orWhere })
56
+ .transacting(trx)
57
+ .execute();
54
58
  }
55
59
  };
56
60
 
@@ -0,0 +1,283 @@
1
+ 'use strict';
2
+
3
+ const { map, isEmpty } = require('lodash/fp');
4
+ const {
5
+ isBidirectional,
6
+ isOneToAny,
7
+ isManyToAny,
8
+ isAnyToOne,
9
+ hasOrderColumn,
10
+ hasInverseOrderColumn,
11
+ } = require('../metadata/relations');
12
+ const { createQueryBuilder } = require('../query');
13
+
14
+ /**
15
+ * If some relations currently exist for this oneToX relation, on the one side, this function removes them and update the inverse order if needed.
16
+ * @param {Object} params
17
+ * @param {string} params.id - entity id on which the relations for entities relIdsToadd are created
18
+ * @param {string} params.attribute - attribute of the relation
19
+ * @param {string} params.inverseRelIds - entity ids of the inverse side for which the current relations will be deleted
20
+ * @param {string} params.db - database instance
21
+ */
22
+ const deletePreviousOneToAnyRelations = async ({
23
+ id,
24
+ attribute,
25
+ relIdsToadd,
26
+ db,
27
+ transaction: trx,
28
+ }) => {
29
+ if (!(isBidirectional(attribute) && isOneToAny(attribute))) {
30
+ throw new Error(
31
+ 'deletePreviousOneToAnyRelations can only be called for bidirectional oneToAny relations'
32
+ );
33
+ }
34
+ const { joinTable } = attribute;
35
+ const { joinColumn, inverseJoinColumn } = joinTable;
36
+
37
+ await createQueryBuilder(joinTable.name, db)
38
+ .delete()
39
+ .where({
40
+ [inverseJoinColumn.name]: relIdsToadd,
41
+ [joinColumn.name]: { $ne: id },
42
+ })
43
+ .where(joinTable.on || {})
44
+ .transacting(trx)
45
+ .execute();
46
+
47
+ await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToadd, transaction: trx });
48
+ };
49
+
50
+ /**
51
+ * If a relation currently exists for this xToOne relations, this function removes it and update the inverse order if needed.
52
+ * @param {Object} params
53
+ * @param {string} params.id - entity id on which the relation for entity relIdToadd is created
54
+ * @param {string} params.attribute - attribute of the relation
55
+ * @param {string} params.relIdToadd - entity id of the new relation
56
+ * @param {string} params.db - database instance
57
+ */
58
+ const deletePreviousAnyToOneRelations = async ({
59
+ id,
60
+ attribute,
61
+ relIdToadd,
62
+ db,
63
+ transaction: trx,
64
+ }) => {
65
+ const { joinTable } = attribute;
66
+ const { joinColumn, inverseJoinColumn } = joinTable;
67
+
68
+ if (!(isBidirectional(attribute) && isAnyToOne(attribute))) {
69
+ throw new Error(
70
+ 'deletePreviousAnyToOneRelations can only be called for bidirectional anyToOne relations'
71
+ );
72
+ }
73
+ // handling manyToOne
74
+ if (isManyToAny(attribute)) {
75
+ // if the database integrity was not broken relsToDelete is supposed to be of length 1
76
+ const relsToDelete = await createQueryBuilder(joinTable.name, db)
77
+ .select(inverseJoinColumn.name)
78
+ .where({
79
+ [joinColumn.name]: id,
80
+ [inverseJoinColumn.name]: { $ne: relIdToadd },
81
+ })
82
+ .where(joinTable.on || {})
83
+ .transacting(trx)
84
+ .execute();
85
+
86
+ const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete);
87
+
88
+ await createQueryBuilder(joinTable.name, db)
89
+ .delete()
90
+ .where({
91
+ [joinColumn.name]: id,
92
+ [inverseJoinColumn.name]: { $in: relIdsToDelete },
93
+ })
94
+ .where(joinTable.on || {})
95
+ .transacting(trx)
96
+ .execute();
97
+
98
+ await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete, transaction: trx });
99
+
100
+ // handling oneToOne
101
+ } else {
102
+ await createQueryBuilder(joinTable.name, db)
103
+ .delete()
104
+ .where({
105
+ [joinColumn.name]: id,
106
+ [inverseJoinColumn.name]: { $ne: relIdToadd },
107
+ })
108
+ .where(joinTable.on || {})
109
+ .transacting(trx)
110
+ .execute();
111
+ }
112
+ };
113
+
114
+ /**
115
+ * Delete all or some relations of entity field
116
+ * @param {Object} params
117
+ * @param {string} params.id - entity id for which the relations will be deleted
118
+ * @param {string} params.attribute - attribute of the relation
119
+ * @param {string} params.db - database instance
120
+ * @param {string} params.relIdsToDelete - ids of entities to remove from the relations. Also accepts 'all'
121
+ * @param {string} params.relIdsToNotDelete - ids of entities to not remove from the relation when relIdsToDelete equals 'all'
122
+ */
123
+ const deleteRelations = async ({
124
+ id,
125
+ attribute,
126
+ db,
127
+ relIdsToNotDelete = [],
128
+ relIdsToDelete = [],
129
+ transaction: trx,
130
+ }) => {
131
+ const { joinTable } = attribute;
132
+ const { joinColumn, inverseJoinColumn } = joinTable;
133
+ const all = relIdsToDelete === 'all';
134
+
135
+ if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
136
+ let lastId = 0;
137
+ let done = false;
138
+ const batchSize = 100;
139
+ while (!done) {
140
+ const batchToDelete = await createQueryBuilder(joinTable.name, db)
141
+ .select(inverseJoinColumn.name)
142
+ .where({
143
+ [joinColumn.name]: id,
144
+ id: { $gt: lastId },
145
+ [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete },
146
+ ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }),
147
+ })
148
+ .where(joinTable.on || {})
149
+ .orderBy('id')
150
+ .limit(batchSize)
151
+ .transacting(trx)
152
+ .execute();
153
+ done = batchToDelete.length < batchSize;
154
+ lastId = batchToDelete[batchToDelete.length - 1]?.id;
155
+
156
+ const batchIds = map(inverseJoinColumn.name, batchToDelete);
157
+
158
+ await createQueryBuilder(joinTable.name, db)
159
+ .delete()
160
+ .where({
161
+ [joinColumn.name]: id,
162
+ [inverseJoinColumn.name]: { $in: batchIds },
163
+ })
164
+ .where(joinTable.on || {})
165
+ .transacting(trx)
166
+ .execute();
167
+
168
+ await cleanOrderColumns({ attribute, db, id, inverseRelIds: batchIds, transaction: trx });
169
+ }
170
+ } else {
171
+ await createQueryBuilder(joinTable.name, db)
172
+ .delete()
173
+ .where({
174
+ [joinColumn.name]: id,
175
+ [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete },
176
+ ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }),
177
+ })
178
+ .where(joinTable.on || {})
179
+ .transacting(trx)
180
+ .execute();
181
+ }
182
+ };
183
+
184
+ /**
185
+ * Clean the order columns by ensuring the order value are continuous (ex: 1, 2, 3 and not 1, 5, 10)
186
+ * @param {Object} params
187
+ * @param {string} params.id - entity id for which the clean will be done
188
+ * @param {string} params.attribute - attribute of the relation
189
+ * @param {string} params.db - database instance
190
+ * @param {string} params.inverseRelIds - entity ids of the inverse side for which the clean will be done
191
+ */
192
+ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction: trx }) => {
193
+ if (
194
+ !(hasOrderColumn(attribute) && id) &&
195
+ !(hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds))
196
+ ) {
197
+ return;
198
+ }
199
+
200
+ const { joinTable } = attribute;
201
+ const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
202
+ const update = [];
203
+ const updateBinding = [];
204
+ const select = ['??'];
205
+ const selectBinding = ['id'];
206
+ const where = [];
207
+ const whereBinding = [];
208
+
209
+ if (hasOrderColumn(attribute) && id) {
210
+ update.push('?? = b.src_order');
211
+ updateBinding.push(orderColumnName);
212
+ select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
213
+ selectBinding.push(joinColumn.name, orderColumnName);
214
+ where.push('?? = ?');
215
+ whereBinding.push(joinColumn.name, id);
216
+ }
217
+
218
+ if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
219
+ update.push('?? = b.inv_order');
220
+ updateBinding.push(inverseOrderColumnName);
221
+ select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
222
+ selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
223
+ where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
224
+ whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
225
+ }
226
+
227
+ // raw query as knex doesn't allow updating from a subquery
228
+ // https://github.com/knex/knex/issues/2504
229
+ switch (strapi.db.dialect.client) {
230
+ case 'mysql':
231
+ await db
232
+ .getConnection()
233
+ .raw(
234
+ `UPDATE
235
+ ?? as a,
236
+ (
237
+ SELECT ${select.join(', ')}
238
+ FROM ??
239
+ WHERE ${where.join(' OR ')}
240
+ ) AS b
241
+ SET ${update.join(', ')}
242
+ WHERE b.id = a.id`,
243
+ [joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
244
+ )
245
+ .transacting(trx);
246
+ break;
247
+ default:
248
+ await db
249
+ .getConnection()
250
+ .raw(
251
+ `UPDATE ?? as a
252
+ SET ${update.join(', ')}
253
+ FROM (
254
+ SELECT ${select.join(', ')}
255
+ FROM ??
256
+ WHERE ${where.join(' OR ')}
257
+ ) AS b
258
+ WHERE b.id = a.id`,
259
+ [joinTable.name, ...updateBinding, ...selectBinding, joinTable.name, ...whereBinding]
260
+ )
261
+ .transacting(trx);
262
+ /*
263
+ `UPDATE :joinTable: as a
264
+ SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
265
+ FROM (
266
+ SELECT
267
+ id,
268
+ ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
269
+ ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
270
+ FROM :joinTable:
271
+ WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
272
+ ) AS b
273
+ WHERE b.id = a.id`,
274
+ */
275
+ }
276
+ };
277
+
278
+ module.exports = {
279
+ deletePreviousOneToAnyRelations,
280
+ deletePreviousAnyToOneRelations,
281
+ deleteRelations,
282
+ cleanOrderColumns,
283
+ };
@@ -123,7 +123,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
123
123
  type: 'integer',
124
124
  column: {
125
125
  unsigned: true,
126
- defaultTo: 0,
126
+ defaultTo: null,
127
127
  },
128
128
  },
129
129
  },
@@ -142,6 +142,11 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
142
142
  name: `${baseModelMeta.tableName}_entity_fk`,
143
143
  columns: ['entity_id'],
144
144
  },
145
+ {
146
+ name: `${baseModelMeta.tableName}_unique`,
147
+ columns: ['entity_id', 'component_id', 'field', 'component_type'],
148
+ type: 'unique',
149
+ },
145
150
  ],
146
151
  foreignKeys: [
147
152
  {
@@ -183,6 +188,7 @@ const createDynamicZone = (attributeName, attribute, meta) => {
183
188
  orderBy: {
184
189
  order: 'asc',
185
190
  },
191
+ pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
186
192
  },
187
193
  });
188
194
  };
@@ -205,9 +211,11 @@ const createComponent = (attributeName, attribute, meta) => {
205
211
  on: {
206
212
  field: attributeName,
207
213
  },
214
+ orderColumnName: 'order',
208
215
  orderBy: {
209
216
  order: 'asc',
210
217
  },
218
+ pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
211
219
  },
212
220
  });
213
221
  };
@@ -10,6 +10,9 @@ const hasInversedBy = _.has('inversedBy');
10
10
  const hasMappedBy = _.has('mappedBy');
11
11
 
12
12
  const isOneToAny = (attribute) => ['oneToOne', 'oneToMany'].includes(attribute.relation);
13
+ const isManyToAny = (attribute) => ['manyToMany', 'manyToOne'].includes(attribute.relation);
14
+ const isAnyToOne = (attribute) => ['oneToOne', 'manyToOne'].includes(attribute.relation);
15
+ const isAnyToMany = (attribute) => ['oneToMany', 'manyToMany'].includes(attribute.relation);
13
16
  const isBidirectional = (attribute) => hasInversedBy(attribute) || hasMappedBy(attribute);
14
17
  const isOwner = (attribute) => !isBidirectional(attribute) || hasInversedBy(attribute);
15
18
  const shouldUseJoinTable = (attribute) => attribute.useJoinTable !== false;
@@ -269,6 +272,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
269
272
  orderBy: {
270
273
  order: 'asc',
271
274
  },
275
+ pivotColumns: [joinColumnName, typeColumnName, idColumnName],
272
276
  };
273
277
 
274
278
  attribute.joinTable = joinTable;
@@ -398,12 +402,20 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
398
402
  const joinColumnName = _.snakeCase(`${meta.singularName}_id`);
399
403
  let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
400
404
 
401
- // if relation is slef referencing
405
+ // if relation is self referencing
402
406
  if (joinColumnName === inverseJoinColumnName) {
403
407
  inverseJoinColumnName = `inv_${inverseJoinColumnName}`;
404
408
  }
405
409
 
406
- metadata.add({
410
+ const orderColumnName = _.snakeCase(`${targetMeta.singularName}_order`);
411
+ let inverseOrderColumnName = _.snakeCase(`${meta.singularName}_order`);
412
+
413
+ // if relation is self referencing
414
+ if (attribute.relation === 'manyToMany' && joinColumnName === inverseJoinColumnName) {
415
+ inverseOrderColumnName = `inv_${inverseOrderColumnName}`;
416
+ }
417
+
418
+ const metadataSchema = {
407
419
  uid: joinTableName,
408
420
  tableName: joinTableName,
409
421
  attributes: {
@@ -433,6 +445,11 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
433
445
  name: `${joinTableName}_inv_fk`,
434
446
  columns: [inverseJoinColumnName],
435
447
  },
448
+ {
449
+ name: `${joinTableName}_unique`,
450
+ columns: [joinColumnName, inverseJoinColumnName],
451
+ type: 'unique',
452
+ },
436
453
  ],
437
454
  foreignKeys: [
438
455
  {
@@ -450,7 +467,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
450
467
  onDelete: 'CASCADE',
451
468
  },
452
469
  ],
453
- });
470
+ };
454
471
 
455
472
  const joinTable = {
456
473
  name: joinTableName,
@@ -462,8 +479,46 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
462
479
  name: inverseJoinColumnName,
463
480
  referencedColumn: 'id',
464
481
  },
482
+ pivotColumns: [joinColumnName, inverseJoinColumnName],
465
483
  };
466
484
 
485
+ // order
486
+ if (isAnyToMany(attribute)) {
487
+ metadataSchema.attributes[orderColumnName] = {
488
+ type: 'integer',
489
+ column: {
490
+ unsigned: true,
491
+ defaultTo: null,
492
+ },
493
+ };
494
+ metadataSchema.indexes.push({
495
+ name: `${joinTableName}_order_fk`,
496
+ columns: [orderColumnName],
497
+ });
498
+ joinTable.orderColumnName = orderColumnName;
499
+ joinTable.orderBy = { [orderColumnName]: 'asc' };
500
+ }
501
+
502
+ // inv order
503
+ if (isBidirectional(attribute) && isManyToAny(attribute)) {
504
+ metadataSchema.attributes[inverseOrderColumnName] = {
505
+ type: 'integer',
506
+ column: {
507
+ unsigned: true,
508
+ defaultTo: null,
509
+ },
510
+ };
511
+
512
+ metadataSchema.indexes.push({
513
+ name: `${joinTableName}_order_inv_fk`,
514
+ columns: [inverseOrderColumnName],
515
+ });
516
+
517
+ joinTable.inverseOrderColumnName = inverseOrderColumnName;
518
+ }
519
+
520
+ metadata.add(metadataSchema);
521
+
467
522
  attribute.joinTable = joinTable;
468
523
 
469
524
  if (isBidirectional(attribute)) {
@@ -479,13 +534,30 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
479
534
  name: joinTableName,
480
535
  joinColumn: joinTable.inverseJoinColumn,
481
536
  inverseJoinColumn: joinTable.joinColumn,
537
+ pivotColumns: joinTable.pivotColumns,
482
538
  };
539
+
540
+ if (isManyToAny(attribute)) {
541
+ inverseAttribute.joinTable.orderColumnName = inverseOrderColumnName;
542
+ inverseAttribute.joinTable.orderBy = { [inverseOrderColumnName]: 'asc' };
543
+ }
544
+ if (isAnyToMany(attribute)) {
545
+ inverseAttribute.joinTable.inverseOrderColumnName = orderColumnName;
546
+ }
483
547
  }
484
548
  };
485
549
 
550
+ const hasOrderColumn = (attribute) => isAnyToMany(attribute);
551
+ const hasInverseOrderColumn = (attribute) => isBidirectional(attribute) && isManyToAny(attribute);
552
+
486
553
  module.exports = {
487
554
  createRelation,
488
555
 
489
556
  isBidirectional,
490
557
  isOneToAny,
558
+ isManyToAny,
559
+ isAnyToOne,
560
+ isAnyToMany,
561
+ hasOrderColumn,
562
+ hasInverseOrderColumn,
491
563
  };
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
- const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
3
+ const createPivotJoin = (ctx, { alias, refAlias, joinTable, targetMeta }) => {
4
+ const { qb } = ctx;
4
5
  const joinAlias = qb.getAlias();
5
6
  qb.join({
6
7
  alias: joinAlias,
@@ -11,10 +12,10 @@ const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
11
12
  on: joinTable.on,
12
13
  });
13
14
 
14
- const subAlias = qb.getAlias();
15
+ const subAlias = refAlias || qb.getAlias();
15
16
  qb.join({
16
17
  alias: subAlias,
17
- referencedTable: tragetMeta.tableName,
18
+ referencedTable: targetMeta.tableName,
18
19
  referencedColumn: joinTable.inverseJoinColumn.referencedColumn,
19
20
  rootColumn: joinTable.inverseJoinColumn.name,
20
21
  rootTable: joinAlias,
@@ -23,22 +24,22 @@ const createPivotJoin = (qb, joinTable, alias, tragetMeta) => {
23
24
  return subAlias;
24
25
  };
25
26
 
26
- const createJoin = (ctx, { alias, attributeName, attribute }) => {
27
+ const createJoin = (ctx, { alias, refAlias, attributeName, attribute }) => {
27
28
  const { db, qb } = ctx;
28
29
 
29
30
  if (attribute.type !== 'relation') {
30
31
  throw new Error(`Cannot join on non relational field ${attributeName}`);
31
32
  }
32
33
 
33
- const tragetMeta = db.metadata.get(attribute.target);
34
+ const targetMeta = db.metadata.get(attribute.target);
34
35
 
35
36
  const { joinColumn } = attribute;
36
37
 
37
38
  if (joinColumn) {
38
- const subAlias = qb.getAlias();
39
+ const subAlias = refAlias || qb.getAlias();
39
40
  qb.join({
40
41
  alias: subAlias,
41
- referencedTable: tragetMeta.tableName,
42
+ referencedTable: targetMeta.tableName,
42
43
  referencedColumn: joinColumn.referencedColumn,
43
44
  rootColumn: joinColumn.name,
44
45
  rootTable: alias,
@@ -48,7 +49,7 @@ const createJoin = (ctx, { alias, attributeName, attribute }) => {
48
49
 
49
50
  const { joinTable } = attribute;
50
51
  if (joinTable) {
51
- return createPivotJoin(qb, joinTable, alias, tragetMeta);
52
+ return createPivotJoin(ctx, { alias, refAlias, joinTable, targetMeta });
52
53
  }
53
54
 
54
55
  return alias;