@strapi/database 4.5.0-alpha.0 → 4.5.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,281 @@
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 (!isAnyToOne(attribute)) {
69
+ throw new Error('deletePreviousAnyToOneRelations can only be called for anyToOne relations');
70
+ }
71
+ // handling manyToOne
72
+ if (isManyToAny(attribute)) {
73
+ // if the database integrity was not broken relsToDelete is supposed to be of length 1
74
+ const relsToDelete = await createQueryBuilder(joinTable.name, db)
75
+ .select(inverseJoinColumn.name)
76
+ .where({
77
+ [joinColumn.name]: id,
78
+ [inverseJoinColumn.name]: { $ne: relIdToadd },
79
+ })
80
+ .where(joinTable.on || {})
81
+ .transacting(trx)
82
+ .execute();
83
+
84
+ const relIdsToDelete = map(inverseJoinColumn.name, relsToDelete);
85
+
86
+ await createQueryBuilder(joinTable.name, db)
87
+ .delete()
88
+ .where({
89
+ [joinColumn.name]: id,
90
+ [inverseJoinColumn.name]: { $in: relIdsToDelete },
91
+ })
92
+ .where(joinTable.on || {})
93
+ .transacting(trx)
94
+ .execute();
95
+
96
+ await cleanOrderColumns({ attribute, db, inverseRelIds: relIdsToDelete, transaction: trx });
97
+
98
+ // handling oneToOne
99
+ } else {
100
+ await createQueryBuilder(joinTable.name, db)
101
+ .delete()
102
+ .where({
103
+ [joinColumn.name]: id,
104
+ [inverseJoinColumn.name]: { $ne: relIdToadd },
105
+ })
106
+ .where(joinTable.on || {})
107
+ .transacting(trx)
108
+ .execute();
109
+ }
110
+ };
111
+
112
+ /**
113
+ * Delete all or some relations of entity field
114
+ * @param {Object} params
115
+ * @param {string} params.id - entity id for which the relations will be deleted
116
+ * @param {string} params.attribute - attribute of the relation
117
+ * @param {string} params.db - database instance
118
+ * @param {string} params.relIdsToDelete - ids of entities to remove from the relations. Also accepts 'all'
119
+ * @param {string} params.relIdsToNotDelete - ids of entities to not remove from the relation when relIdsToDelete equals 'all'
120
+ */
121
+ const deleteRelations = async ({
122
+ id,
123
+ attribute,
124
+ db,
125
+ relIdsToNotDelete = [],
126
+ relIdsToDelete = [],
127
+ transaction: trx,
128
+ }) => {
129
+ const { joinTable } = attribute;
130
+ const { joinColumn, inverseJoinColumn } = joinTable;
131
+ const all = relIdsToDelete === 'all';
132
+
133
+ if (hasOrderColumn(attribute) || hasInverseOrderColumn(attribute)) {
134
+ let lastId = 0;
135
+ let done = false;
136
+ const batchSize = 100;
137
+ while (!done) {
138
+ const batchToDelete = await createQueryBuilder(joinTable.name, db)
139
+ .select(inverseJoinColumn.name)
140
+ .where({
141
+ [joinColumn.name]: id,
142
+ id: { $gt: lastId },
143
+ [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete },
144
+ ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }),
145
+ })
146
+ .where(joinTable.on || {})
147
+ .orderBy('id')
148
+ .limit(batchSize)
149
+ .transacting(trx)
150
+ .execute();
151
+ done = batchToDelete.length < batchSize;
152
+ lastId = batchToDelete[batchToDelete.length - 1]?.id;
153
+
154
+ const batchIds = map(inverseJoinColumn.name, batchToDelete);
155
+
156
+ await createQueryBuilder(joinTable.name, db)
157
+ .delete()
158
+ .where({
159
+ [joinColumn.name]: id,
160
+ [inverseJoinColumn.name]: { $in: batchIds },
161
+ })
162
+ .where(joinTable.on || {})
163
+ .transacting(trx)
164
+ .execute();
165
+
166
+ await cleanOrderColumns({ attribute, db, id, inverseRelIds: batchIds, transaction: trx });
167
+ }
168
+ } else {
169
+ await createQueryBuilder(joinTable.name, db)
170
+ .delete()
171
+ .where({
172
+ [joinColumn.name]: id,
173
+ [inverseJoinColumn.name]: { $notIn: relIdsToNotDelete },
174
+ ...(all ? {} : { [inverseJoinColumn.name]: { $in: relIdsToDelete } }),
175
+ })
176
+ .where(joinTable.on || {})
177
+ .transacting(trx)
178
+ .execute();
179
+ }
180
+ };
181
+
182
+ /**
183
+ * Clean the order columns by ensuring the order value are continuous (ex: 1, 2, 3 and not 1, 5, 10)
184
+ * @param {Object} params
185
+ * @param {string} params.id - entity id for which the clean will be done
186
+ * @param {string} params.attribute - attribute of the relation
187
+ * @param {string} params.db - database instance
188
+ * @param {string} params.inverseRelIds - entity ids of the inverse side for which the clean will be done
189
+ */
190
+ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction: trx }) => {
191
+ if (
192
+ !(hasOrderColumn(attribute) && id) &&
193
+ !(hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds))
194
+ ) {
195
+ return;
196
+ }
197
+
198
+ const { joinTable } = attribute;
199
+ const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
200
+ const update = [];
201
+ const updateBinding = [];
202
+ const select = ['??'];
203
+ const selectBinding = ['id'];
204
+ const where = [];
205
+ const whereBinding = [];
206
+
207
+ if (hasOrderColumn(attribute) && id) {
208
+ update.push('?? = b.src_order');
209
+ updateBinding.push(orderColumnName);
210
+ select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
211
+ selectBinding.push(joinColumn.name, orderColumnName);
212
+ where.push('?? = ?');
213
+ whereBinding.push(joinColumn.name, id);
214
+ }
215
+
216
+ if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
217
+ update.push('?? = b.inv_order');
218
+ updateBinding.push(inverseOrderColumnName);
219
+ select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
220
+ selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
221
+ where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
222
+ whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
223
+ }
224
+
225
+ // raw query as knex doesn't allow updating from a subquery
226
+ // https://github.com/knex/knex/issues/2504
227
+ switch (strapi.db.dialect.client) {
228
+ case 'mysql':
229
+ await db
230
+ .getConnection()
231
+ .raw(
232
+ `UPDATE
233
+ ?? as a,
234
+ (
235
+ SELECT ${select.join(', ')}
236
+ FROM ??
237
+ WHERE ${where.join(' OR ')}
238
+ ) AS b
239
+ SET ${update.join(', ')}
240
+ WHERE b.id = a.id`,
241
+ [joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
242
+ )
243
+ .transacting(trx);
244
+ break;
245
+ default:
246
+ await db
247
+ .getConnection()
248
+ .raw(
249
+ `UPDATE ?? as a
250
+ SET ${update.join(', ')}
251
+ FROM (
252
+ SELECT ${select.join(', ')}
253
+ FROM ??
254
+ WHERE ${where.join(' OR ')}
255
+ ) AS b
256
+ WHERE b.id = a.id`,
257
+ [joinTable.name, ...updateBinding, ...selectBinding, joinTable.name, ...whereBinding]
258
+ )
259
+ .transacting(trx);
260
+ /*
261
+ `UPDATE :joinTable: as a
262
+ SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
263
+ FROM (
264
+ SELECT
265
+ id,
266
+ ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
267
+ ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
268
+ FROM :joinTable:
269
+ WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
270
+ ) AS b
271
+ WHERE b.id = a.id`,
272
+ */
273
+ }
274
+ };
275
+
276
+ module.exports = {
277
+ deletePreviousOneToAnyRelations,
278
+ deletePreviousAnyToOneRelations,
279
+ deleteRelations,
280
+ cleanOrderColumns,
281
+ };
@@ -9,6 +9,21 @@ class Metadata extends Map {
9
9
  add(meta) {
10
10
  return this.set(meta.uid, meta);
11
11
  }
12
+
13
+ /**
14
+ * Validate the DB metadata, throwing an error if a duplicate DB table name is detected
15
+ */
16
+ validate() {
17
+ const seenTables = new Map();
18
+ for (const meta of this.values()) {
19
+ if (seenTables.get(meta.tableName)) {
20
+ throw new Error(
21
+ `DB table "${meta.tableName}" already exists. Change the collectionName of the related content type.`
22
+ );
23
+ }
24
+ seenTables.set(meta.tableName, true);
25
+ }
26
+ }
12
27
  }
13
28
 
14
29
  // TODO: check if there isn't an attribute with an id already
@@ -81,6 +96,7 @@ const createMetadata = (models = []) => {
81
96
  meta.columnToAttribute = columnToAttribute;
82
97
  }
83
98
 
99
+ metadata.validate();
84
100
  return metadata;
85
101
  };
86
102
 
@@ -123,7 +139,7 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
123
139
  type: 'integer',
124
140
  column: {
125
141
  unsigned: true,
126
- defaultTo: 0,
142
+ defaultTo: null,
127
143
  },
128
144
  },
129
145
  },
@@ -142,6 +158,11 @@ const createCompoLinkModelMeta = (baseModelMeta) => {
142
158
  name: `${baseModelMeta.tableName}_entity_fk`,
143
159
  columns: ['entity_id'],
144
160
  },
161
+ {
162
+ name: `${baseModelMeta.tableName}_unique`,
163
+ columns: ['entity_id', 'component_id', 'field', 'component_type'],
164
+ type: 'unique',
165
+ },
145
166
  ],
146
167
  foreignKeys: [
147
168
  {
@@ -183,6 +204,7 @@ const createDynamicZone = (attributeName, attribute, meta) => {
183
204
  orderBy: {
184
205
  order: 'asc',
185
206
  },
207
+ pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
186
208
  },
187
209
  });
188
210
  };
@@ -205,10 +227,11 @@ const createComponent = (attributeName, attribute, meta) => {
205
227
  on: {
206
228
  field: attributeName,
207
229
  },
230
+ orderColumnName: 'order',
208
231
  orderBy: {
209
232
  order: 'asc',
210
233
  },
211
- ...(attribute.repeatable === true ? { orderColumnName: 'order' } : {}),
234
+ pivotColumns: ['entity_id', 'component_id', 'field', 'component_type'],
212
235
  },
213
236
  });
214
237
  };
@@ -272,6 +272,7 @@ const createMorphToMany = (attributeName, attribute, meta, metadata) => {
272
272
  orderBy: {
273
273
  order: 'asc',
274
274
  },
275
+ pivotColumns: [joinColumnName, typeColumnName, idColumnName],
275
276
  };
276
277
 
277
278
  attribute.joinTable = joinTable;
@@ -406,8 +407,8 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
406
407
  inverseJoinColumnName = `inv_${inverseJoinColumnName}`;
407
408
  }
408
409
 
409
- const orderColumnName = _.snakeCase(`${meta.singularName}_order`);
410
- let inverseOrderColumnName = _.snakeCase(`${targetMeta.singularName}_order`);
410
+ const orderColumnName = _.snakeCase(`${targetMeta.singularName}_order`);
411
+ let inverseOrderColumnName = _.snakeCase(`${meta.singularName}_order`);
411
412
 
412
413
  // if relation is self referencing
413
414
  if (attribute.relation === 'manyToMany' && joinColumnName === inverseJoinColumnName) {
@@ -444,6 +445,11 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
444
445
  name: `${joinTableName}_inv_fk`,
445
446
  columns: [inverseJoinColumnName],
446
447
  },
448
+ {
449
+ name: `${joinTableName}_unique`,
450
+ columns: [joinColumnName, inverseJoinColumnName],
451
+ type: 'unique',
452
+ },
447
453
  ],
448
454
  foreignKeys: [
449
455
  {
@@ -473,6 +479,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
473
479
  name: inverseJoinColumnName,
474
480
  referencedColumn: 'id',
475
481
  },
482
+ pivotColumns: [joinColumnName, inverseJoinColumnName],
476
483
  };
477
484
 
478
485
  // order
@@ -527,6 +534,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
527
534
  name: joinTableName,
528
535
  joinColumn: joinTable.inverseJoinColumn,
529
536
  inverseJoinColumn: joinTable.joinColumn,
537
+ pivotColumns: joinTable.pivotColumns,
530
538
  };
531
539
 
532
540
  if (isManyToAny(attribute)) {
@@ -539,6 +547,9 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
539
547
  }
540
548
  };
541
549
 
550
+ const hasOrderColumn = (attribute) => isAnyToMany(attribute);
551
+ const hasInverseOrderColumn = (attribute) => isBidirectional(attribute) && isManyToAny(attribute);
552
+
542
553
  module.exports = {
543
554
  createRelation,
544
555
 
@@ -547,4 +558,6 @@ module.exports = {
547
558
  isManyToAny,
548
559
  isAnyToOne,
549
560
  isAnyToMany,
561
+ hasOrderColumn,
562
+ hasInverseOrderColumn,
550
563
  };