@strapi/database 4.6.0-beta.2 → 4.6.1
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.
- package/lib/__tests__/index.test.js +93 -0
- package/lib/dialects/postgresql/index.js +10 -2
- package/lib/entity-manager/index.js +3 -3
- package/lib/entity-manager/regular-relations.js +133 -126
- package/lib/index.d.ts +9 -0
- package/lib/index.js +45 -5
- package/lib/metadata/relations.js +4 -1
- package/lib/query/helpers/where.js +10 -0
- package/lib/query/query-builder.js +10 -1
- package/lib/tests/transactions.test.api.js +308 -0
- package/lib/transaction-context.js +17 -0
- package/lib/validations/index.js +20 -0
- package/lib/validations/relations/bidirectional.js +89 -0
- package/lib/validations/relations/index.js +14 -0
- package/package.json +3 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { Database } = require('../index');
|
|
4
|
+
|
|
5
|
+
jest.mock('../connection', () =>
|
|
6
|
+
jest.fn(() => {
|
|
7
|
+
const trx = {
|
|
8
|
+
commit: jest.fn(),
|
|
9
|
+
rollback: jest.fn(),
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
...trx,
|
|
13
|
+
transaction: jest.fn(async () => trx),
|
|
14
|
+
};
|
|
15
|
+
})
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
jest.mock('../dialects', () => ({
|
|
19
|
+
getDialect: jest.fn(() => ({
|
|
20
|
+
configure: jest.fn(),
|
|
21
|
+
initialize: jest.fn(),
|
|
22
|
+
})),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
jest.mock('../migrations', () => ({
|
|
26
|
+
createMigrationsProvider: jest.fn(),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const config = {
|
|
30
|
+
models: [
|
|
31
|
+
{
|
|
32
|
+
tableName: 'strapi_core_store_settings',
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
connection: {
|
|
36
|
+
client: 'postgres',
|
|
37
|
+
connection: {
|
|
38
|
+
database: 'strapi',
|
|
39
|
+
user: 'strapi',
|
|
40
|
+
password: 'strapi',
|
|
41
|
+
port: 5432,
|
|
42
|
+
host: 'localhost',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
describe('Database', () => {
|
|
48
|
+
describe('constructor', () => {
|
|
49
|
+
it('should throw an error if no config is provided', async () => {
|
|
50
|
+
expect(async () => Database.init()).rejects.toThrowError();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('it should intialize if config is provided', async () => {
|
|
54
|
+
expect(() => Database.init(config)).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('Transaction', () => {
|
|
59
|
+
it('transaction should be defined', async () => {
|
|
60
|
+
const db = await Database.init(config);
|
|
61
|
+
expect(db.transaction).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return value if transaction is complete', async () => {
|
|
65
|
+
const db = await Database.init(config);
|
|
66
|
+
const result = await db.transaction(async () => 'test');
|
|
67
|
+
expect(result).toBe('test');
|
|
68
|
+
expect(db.connection.commit).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rollback is called incase of error', async () => {
|
|
72
|
+
const db = await Database.init(config);
|
|
73
|
+
try {
|
|
74
|
+
await db.transaction(async () => {
|
|
75
|
+
throw new Error('test');
|
|
76
|
+
});
|
|
77
|
+
} catch {
|
|
78
|
+
/* ignore */
|
|
79
|
+
}
|
|
80
|
+
expect(db.connection.rollback).toHaveBeenCalledTimes(1);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error', async () => {
|
|
84
|
+
const db = await Database.init(config);
|
|
85
|
+
|
|
86
|
+
expect(async () => {
|
|
87
|
+
await db.transaction(async () => {
|
|
88
|
+
throw new Error('test');
|
|
89
|
+
});
|
|
90
|
+
}).rejects.toThrowError('test');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -16,8 +16,16 @@ class PostgresDialect extends Dialect {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
async initialize() {
|
|
19
|
-
this.db.connection.client.driver.types.setTypeParser(
|
|
20
|
-
|
|
19
|
+
this.db.connection.client.driver.types.setTypeParser(
|
|
20
|
+
this.db.connection.client.driver.types.builtins.DATE,
|
|
21
|
+
'text',
|
|
22
|
+
(v) => v
|
|
23
|
+
); // Don't cast DATE string to Date()
|
|
24
|
+
this.db.connection.client.driver.types.setTypeParser(
|
|
25
|
+
this.db.connection.client.driver.types.builtins.NUMERIC,
|
|
26
|
+
'text',
|
|
27
|
+
parseFloat
|
|
28
|
+
);
|
|
21
29
|
}
|
|
22
30
|
|
|
23
31
|
usesForeignKeys() {
|
|
@@ -227,7 +227,7 @@ const createEntityManager = (db) => {
|
|
|
227
227
|
|
|
228
228
|
const trx = await strapi.db.transaction();
|
|
229
229
|
try {
|
|
230
|
-
await this.attachRelations(uid, id, data, { transaction: trx });
|
|
230
|
+
await this.attachRelations(uid, id, data, { transaction: trx.get() });
|
|
231
231
|
|
|
232
232
|
await trx.commit();
|
|
233
233
|
} catch (e) {
|
|
@@ -311,7 +311,7 @@ const createEntityManager = (db) => {
|
|
|
311
311
|
|
|
312
312
|
const trx = await strapi.db.transaction();
|
|
313
313
|
try {
|
|
314
|
-
await this.updateRelations(uid, id, data, { transaction: trx });
|
|
314
|
+
await this.updateRelations(uid, id, data, { transaction: trx.get() });
|
|
315
315
|
await trx.commit();
|
|
316
316
|
} catch (e) {
|
|
317
317
|
await trx.rollback();
|
|
@@ -382,7 +382,7 @@ const createEntityManager = (db) => {
|
|
|
382
382
|
|
|
383
383
|
const trx = await strapi.db.transaction();
|
|
384
384
|
try {
|
|
385
|
-
await this.deleteRelations(uid, id, { transaction: trx });
|
|
385
|
+
await this.deleteRelations(uid, id, { transaction: trx.get() });
|
|
386
386
|
|
|
387
387
|
await trx.commit();
|
|
388
388
|
} catch (e) {
|
|
@@ -198,38 +198,73 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
// Handle databases that don't support window function ROW_NUMBER (here it's MySQL 5)
|
|
202
|
+
if (!strapi.db.dialect.supportsWindowFunctions()) {
|
|
203
|
+
await cleanOrderColumnsForOldDatabases({ id, attribute, db, inverseRelIds, transaction: trx });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { joinTable } = attribute;
|
|
208
|
+
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
|
209
|
+
const update = [];
|
|
210
|
+
const updateBinding = [];
|
|
211
|
+
const select = ['??'];
|
|
212
|
+
const selectBinding = ['id'];
|
|
213
|
+
const where = [];
|
|
214
|
+
const whereBinding = [];
|
|
215
|
+
|
|
216
|
+
if (hasOrderColumn(attribute) && id) {
|
|
217
|
+
update.push('?? = b.src_order');
|
|
218
|
+
updateBinding.push(orderColumnName);
|
|
219
|
+
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
|
|
220
|
+
selectBinding.push(joinColumn.name, orderColumnName);
|
|
221
|
+
where.push('?? = ?');
|
|
222
|
+
whereBinding.push(joinColumn.name, id);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
|
226
|
+
update.push('?? = b.inv_order');
|
|
227
|
+
updateBinding.push(inverseOrderColumnName);
|
|
228
|
+
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
|
|
229
|
+
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
|
|
230
|
+
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
|
231
|
+
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
|
232
|
+
}
|
|
233
|
+
|
|
201
234
|
switch (strapi.db.dialect.client) {
|
|
202
235
|
case 'mysql':
|
|
203
|
-
|
|
236
|
+
// Here it's MariaDB and MySQL 8
|
|
237
|
+
await db
|
|
238
|
+
.getConnection()
|
|
239
|
+
.raw(
|
|
240
|
+
`UPDATE
|
|
241
|
+
?? as a,
|
|
242
|
+
(
|
|
243
|
+
SELECT ${select.join(', ')}
|
|
244
|
+
FROM ??
|
|
245
|
+
WHERE ${where.join(' OR ')}
|
|
246
|
+
) AS b
|
|
247
|
+
SET ${update.join(', ')}
|
|
248
|
+
WHERE b.id = a.id`,
|
|
249
|
+
[joinTable.name, ...selectBinding, joinTable.name, ...whereBinding, ...updateBinding]
|
|
250
|
+
)
|
|
251
|
+
.transacting(trx);
|
|
204
252
|
break;
|
|
253
|
+
/*
|
|
254
|
+
UPDATE
|
|
255
|
+
:joinTable: as a,
|
|
256
|
+
(
|
|
257
|
+
SELECT
|
|
258
|
+
id,
|
|
259
|
+
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
|
260
|
+
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
|
261
|
+
FROM :joinTable:
|
|
262
|
+
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
|
263
|
+
) AS b
|
|
264
|
+
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
|
265
|
+
WHERE b.id = a.id;
|
|
266
|
+
*/
|
|
205
267
|
default: {
|
|
206
|
-
const { joinTable } = attribute;
|
|
207
|
-
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
|
208
|
-
const update = [];
|
|
209
|
-
const updateBinding = [];
|
|
210
|
-
const select = ['??'];
|
|
211
|
-
const selectBinding = ['id'];
|
|
212
|
-
const where = [];
|
|
213
|
-
const whereBinding = [];
|
|
214
|
-
|
|
215
|
-
if (hasOrderColumn(attribute) && id) {
|
|
216
|
-
update.push('?? = b.src_order');
|
|
217
|
-
updateBinding.push(orderColumnName);
|
|
218
|
-
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS src_order');
|
|
219
|
-
selectBinding.push(joinColumn.name, orderColumnName);
|
|
220
|
-
where.push('?? = ?');
|
|
221
|
-
whereBinding.push(joinColumn.name, id);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
|
225
|
-
update.push('?? = b.inv_order');
|
|
226
|
-
updateBinding.push(inverseOrderColumnName);
|
|
227
|
-
select.push('ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ??) AS inv_order');
|
|
228
|
-
selectBinding.push(inverseJoinColumn.name, inverseOrderColumnName);
|
|
229
|
-
where.push(`?? IN (${inverseRelIds.map(() => '?').join(', ')})`);
|
|
230
|
-
whereBinding.push(inverseJoinColumn.name, ...inverseRelIds);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
268
|
const joinTableName = addSchema(joinTable.name);
|
|
234
269
|
|
|
235
270
|
// raw query as knex doesn't allow updating from a subquery
|
|
@@ -249,17 +284,17 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
|
|
249
284
|
.transacting(trx);
|
|
250
285
|
|
|
251
286
|
/*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
287
|
+
UPDATE :joinTable: as a
|
|
288
|
+
SET :orderColumn: = b.src_order, :inverseOrderColumn: = b.inv_order
|
|
289
|
+
FROM (
|
|
290
|
+
SELECT
|
|
291
|
+
id,
|
|
292
|
+
ROW_NUMBER() OVER ( PARTITION BY :joinColumn: ORDER BY :orderColumn:) AS src_order,
|
|
293
|
+
ROW_NUMBER() OVER ( PARTITION BY :inverseJoinColumn: ORDER BY :inverseOrderColumn:) AS inv_order
|
|
294
|
+
FROM :joinTable:
|
|
295
|
+
WHERE :joinColumn: = :id OR :inverseJoinColumn: IN (:inverseRelIds)
|
|
296
|
+
) AS b
|
|
297
|
+
WHERE b.id = a.id;
|
|
263
298
|
*/
|
|
264
299
|
}
|
|
265
300
|
}
|
|
@@ -267,9 +302,9 @@ const cleanOrderColumns = async ({ id, attribute, db, inverseRelIds, transaction
|
|
|
267
302
|
|
|
268
303
|
/*
|
|
269
304
|
* Ensure that orders are following a 1, 2, 3 sequence, without gap.
|
|
270
|
-
* The use of a
|
|
305
|
+
* The use of a session variable instead of a window function makes the query compatible with MySQL 5
|
|
271
306
|
*/
|
|
272
|
-
const
|
|
307
|
+
const cleanOrderColumnsForOldDatabases = async ({
|
|
273
308
|
id,
|
|
274
309
|
attribute,
|
|
275
310
|
db,
|
|
@@ -279,96 +314,68 @@ const cleanOrderColumnsForInnoDB = async ({
|
|
|
279
314
|
const { joinTable } = attribute;
|
|
280
315
|
const { joinColumn, inverseJoinColumn, orderColumnName, inverseOrderColumnName } = joinTable;
|
|
281
316
|
|
|
282
|
-
const
|
|
283
|
-
const randomHex = randomBytes(16).toString('hex');
|
|
317
|
+
const randomSuffix = `${new Date().valueOf()}_${randomBytes(16).toString('hex')}`;
|
|
284
318
|
|
|
285
319
|
if (hasOrderColumn(attribute) && id) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
)
|
|
310
|
-
.transacting(trx);
|
|
311
|
-
|
|
312
|
-
// raw query as knex doesn't allow updating from a subquery
|
|
313
|
-
// https://github.com/knex/knex/issues/2504
|
|
314
|
-
await db.connection
|
|
315
|
-
.raw(
|
|
316
|
-
`UPDATE ?? as a, (SELECT * FROM ??) AS b
|
|
317
|
-
SET ?? = b.src_order
|
|
318
|
-
WHERE a.id = b.id`,
|
|
319
|
-
[joinTable.name, tempOrderTableName, orderColumnName]
|
|
320
|
-
)
|
|
321
|
-
.transacting(trx);
|
|
322
|
-
} finally {
|
|
323
|
-
await db.connection.raw(`DROP TABLE IF EXISTS ??`, [tempOrderTableName]).transacting(trx);
|
|
324
|
-
}
|
|
320
|
+
// raw query as knex doesn't allow updating from a subquery
|
|
321
|
+
// https://github.com/knex/knex/issues/2504
|
|
322
|
+
const orderVar = `order_${randomSuffix}`;
|
|
323
|
+
await db.connection.raw(`SET @${orderVar} = 0;`).transacting(trx);
|
|
324
|
+
await db.connection
|
|
325
|
+
.raw(
|
|
326
|
+
`UPDATE :joinTableName: as a, (
|
|
327
|
+
SELECT id, (@${orderVar}:=@${orderVar} + 1) AS src_order
|
|
328
|
+
FROM :joinTableName:
|
|
329
|
+
WHERE :joinColumnName: = :id
|
|
330
|
+
ORDER BY :orderColumnName:
|
|
331
|
+
) AS b
|
|
332
|
+
SET :orderColumnName: = b.src_order
|
|
333
|
+
WHERE a.id = b.id
|
|
334
|
+
AND a.:joinColumnName: = :id`,
|
|
335
|
+
{
|
|
336
|
+
joinTableName: joinTable.name,
|
|
337
|
+
orderColumnName,
|
|
338
|
+
joinColumnName: joinColumn.name,
|
|
339
|
+
id,
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
.transacting(trx);
|
|
325
343
|
}
|
|
326
344
|
|
|
327
345
|
if (hasInverseOrderColumn(attribute) && !isEmpty(inverseRelIds)) {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
await db.connection
|
|
362
|
-
.raw(
|
|
363
|
-
`UPDATE ?? as a, (SELECT * FROM ??) AS b
|
|
364
|
-
SET ?? = b.inv_order
|
|
365
|
-
WHERE a.id = b.id`,
|
|
366
|
-
[joinTable.name, tempInvOrderTableName, inverseOrderColumnName]
|
|
367
|
-
)
|
|
368
|
-
.transacting(trx);
|
|
369
|
-
} finally {
|
|
370
|
-
await db.connection.raw(`DROP TABLE IF EXISTS ??`, [tempInvOrderTableName]).transacting(trx);
|
|
371
|
-
}
|
|
346
|
+
const orderVar = `order_${randomSuffix}`;
|
|
347
|
+
const columnVar = `col_${randomSuffix}`;
|
|
348
|
+
await db.connection.raw(`SET @${orderVar} = 0;`).transacting(trx);
|
|
349
|
+
await db.connection
|
|
350
|
+
.raw(
|
|
351
|
+
`UPDATE ?? as a, (
|
|
352
|
+
SELECT
|
|
353
|
+
id,
|
|
354
|
+
@${orderVar}:=CASE WHEN @${columnVar} = ?? THEN @${orderVar} + 1 ELSE 1 END AS inv_order,
|
|
355
|
+
@${columnVar}:=?? ??
|
|
356
|
+
FROM ?? a
|
|
357
|
+
WHERE ?? IN(${inverseRelIds.map(() => '?').join(', ')})
|
|
358
|
+
ORDER BY ??, ??
|
|
359
|
+
) AS b
|
|
360
|
+
SET ?? = b.inv_order
|
|
361
|
+
WHERE a.id = b.id
|
|
362
|
+
AND a.?? IN(${inverseRelIds.map(() => '?').join(', ')})`,
|
|
363
|
+
[
|
|
364
|
+
joinTable.name,
|
|
365
|
+
inverseJoinColumn.name,
|
|
366
|
+
inverseJoinColumn.name,
|
|
367
|
+
inverseJoinColumn.name,
|
|
368
|
+
joinTable.name,
|
|
369
|
+
inverseJoinColumn.name,
|
|
370
|
+
...inverseRelIds,
|
|
371
|
+
inverseJoinColumn.name,
|
|
372
|
+
joinColumn.name,
|
|
373
|
+
inverseOrderColumnName,
|
|
374
|
+
inverseJoinColumn.name,
|
|
375
|
+
...inverseRelIds,
|
|
376
|
+
]
|
|
377
|
+
)
|
|
378
|
+
.transacting(trx);
|
|
372
379
|
}
|
|
373
380
|
};
|
|
374
381
|
|
package/lib/index.d.ts
CHANGED
|
@@ -163,6 +163,15 @@ export interface Database {
|
|
|
163
163
|
connection: Knex;
|
|
164
164
|
|
|
165
165
|
query<T extends keyof AllTypes>(uid: T): QueryFromContentType<T>;
|
|
166
|
+
transaction(
|
|
167
|
+
cb?: (params: {
|
|
168
|
+
trx: Knex.Transaction;
|
|
169
|
+
rollback: () => Promise<void>;
|
|
170
|
+
commit: () => Promise<void>;
|
|
171
|
+
}) => Promise<unknown>
|
|
172
|
+
):
|
|
173
|
+
| Promise<unknown>
|
|
174
|
+
| { get: () => Knex.Transaction; rollback: () => Promise<void>; commit: () => Promise<void> };
|
|
166
175
|
}
|
|
167
176
|
export class Database implements Database {
|
|
168
177
|
static transformContentTypes(contentTypes: any[]): ModelConfig[];
|
package/lib/index.js
CHANGED
|
@@ -8,9 +8,11 @@ const { createMigrationsProvider } = require('./migrations');
|
|
|
8
8
|
const { createLifecyclesProvider } = require('./lifecycles');
|
|
9
9
|
const createConnection = require('./connection');
|
|
10
10
|
const errors = require('./errors');
|
|
11
|
+
const transactionCtx = require('./transaction-context');
|
|
11
12
|
|
|
12
13
|
// TODO: move back into strapi
|
|
13
14
|
const { transformContentTypes } = require('./utils/content-types');
|
|
15
|
+
const { validateDatabase } = require('./validations');
|
|
14
16
|
|
|
15
17
|
class Database {
|
|
16
18
|
constructor(config) {
|
|
@@ -49,6 +51,44 @@ class Database {
|
|
|
49
51
|
return this.entityManager.getRepository(uid);
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
async transaction(cb) {
|
|
55
|
+
const notNestedTransaction = !transactionCtx.get();
|
|
56
|
+
const trx = notNestedTransaction ? await this.connection.transaction() : transactionCtx.get();
|
|
57
|
+
|
|
58
|
+
async function commit() {
|
|
59
|
+
if (notNestedTransaction) {
|
|
60
|
+
await trx.commit();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function rollback() {
|
|
65
|
+
if (notNestedTransaction) {
|
|
66
|
+
await trx.rollback();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!cb) {
|
|
70
|
+
return {
|
|
71
|
+
commit,
|
|
72
|
+
rollback,
|
|
73
|
+
get() {
|
|
74
|
+
return trx;
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return transactionCtx.run(trx, async () => {
|
|
80
|
+
try {
|
|
81
|
+
const callbackParams = { trx, commit, rollback };
|
|
82
|
+
const res = await cb(callbackParams);
|
|
83
|
+
await commit();
|
|
84
|
+
return res;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await rollback();
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
getConnection(tableName) {
|
|
53
93
|
const schema = this.connection.getSchemaName();
|
|
54
94
|
const connection = tableName ? this.connection(tableName) : this.connection;
|
|
@@ -60,10 +100,6 @@ class Database {
|
|
|
60
100
|
return schema ? trx.schema.withSchema(schema) : trx.schema;
|
|
61
101
|
}
|
|
62
102
|
|
|
63
|
-
transaction() {
|
|
64
|
-
return this.connection.transaction();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
103
|
queryBuilder(uid) {
|
|
68
104
|
return this.entityManager.createQueryBuilder(uid);
|
|
69
105
|
}
|
|
@@ -76,7 +112,11 @@ class Database {
|
|
|
76
112
|
|
|
77
113
|
// TODO: move into strapi
|
|
78
114
|
Database.transformContentTypes = transformContentTypes;
|
|
79
|
-
Database.init = async (config) =>
|
|
115
|
+
Database.init = async (config) => {
|
|
116
|
+
const db = new Database(config);
|
|
117
|
+
await validateDatabase(db);
|
|
118
|
+
return db;
|
|
119
|
+
};
|
|
80
120
|
|
|
81
121
|
module.exports = {
|
|
82
122
|
Database,
|
|
@@ -16,6 +16,8 @@ const isAnyToMany = (attribute) => ['oneToMany', 'manyToMany'].includes(attribut
|
|
|
16
16
|
const isBidirectional = (attribute) => hasInversedBy(attribute) || hasMappedBy(attribute);
|
|
17
17
|
const isOwner = (attribute) => !isBidirectional(attribute) || hasInversedBy(attribute);
|
|
18
18
|
const shouldUseJoinTable = (attribute) => attribute.useJoinTable !== false;
|
|
19
|
+
const getJoinTableName = (tableName, attributeName) =>
|
|
20
|
+
_.snakeCase(`${tableName}_${attributeName}_links`);
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Creates a oneToOne relation metadata
|
|
@@ -397,7 +399,7 @@ const createJoinTable = (metadata, { attributeName, attribute, meta }) => {
|
|
|
397
399
|
throw new Error(`Unknown target ${attribute.target}`);
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
const joinTableName =
|
|
402
|
+
const joinTableName = getJoinTableName(meta.tableName, attributeName);
|
|
401
403
|
|
|
402
404
|
const joinColumnName = _.snakeCase(`${meta.singularName}_id`);
|
|
403
405
|
let inverseJoinColumnName = _.snakeCase(`${targetMeta.singularName}_id`);
|
|
@@ -560,4 +562,5 @@ module.exports = {
|
|
|
560
562
|
isAnyToMany,
|
|
561
563
|
hasOrderColumn,
|
|
562
564
|
hasInverseOrderColumn,
|
|
565
|
+
getJoinTableName,
|
|
563
566
|
};
|
|
@@ -25,6 +25,8 @@ const OPERATORS = [
|
|
|
25
25
|
'$between',
|
|
26
26
|
'$startsWith',
|
|
27
27
|
'$endsWith',
|
|
28
|
+
'$startsWithi',
|
|
29
|
+
'$endsWithi',
|
|
28
30
|
'$contains',
|
|
29
31
|
'$notContains',
|
|
30
32
|
'$containsi',
|
|
@@ -283,10 +285,18 @@ const applyOperator = (qb, column, operator, value) => {
|
|
|
283
285
|
qb.where(column, 'like', `${value}%`);
|
|
284
286
|
break;
|
|
285
287
|
}
|
|
288
|
+
case '$startsWithi': {
|
|
289
|
+
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [column, `${value}%`]);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
286
292
|
case '$endsWith': {
|
|
287
293
|
qb.where(column, 'like', `%${value}`);
|
|
288
294
|
break;
|
|
289
295
|
}
|
|
296
|
+
case '$endsWithi': {
|
|
297
|
+
qb.whereRaw(`${fieldLowerFn(qb)} LIKE LOWER(?)`, [column, `%${value}`]);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
290
300
|
case '$contains': {
|
|
291
301
|
qb.where(column, 'like', `%${value}%`);
|
|
292
302
|
break;
|
|
@@ -4,6 +4,7 @@ const _ = require('lodash/fp');
|
|
|
4
4
|
|
|
5
5
|
const { DatabaseError } = require('../errors');
|
|
6
6
|
const helpers = require('./helpers');
|
|
7
|
+
const transactionCtx = require('../transaction-context');
|
|
7
8
|
|
|
8
9
|
const createQueryBuilder = (uid, db, initialState = {}) => {
|
|
9
10
|
const meta = db.metadata.get(uid);
|
|
@@ -473,10 +474,18 @@ const createQueryBuilder = (uid, db, initialState = {}) => {
|
|
|
473
474
|
try {
|
|
474
475
|
const qb = this.getKnexQuery();
|
|
475
476
|
|
|
477
|
+
if (transactionCtx.get()) {
|
|
478
|
+
qb.transacting(transactionCtx.get());
|
|
479
|
+
}
|
|
480
|
+
|
|
476
481
|
const rows = await qb;
|
|
477
482
|
|
|
478
483
|
if (state.populate && !_.isNil(rows)) {
|
|
479
|
-
await helpers.applyPopulate(_.castArray(rows), state.populate, {
|
|
484
|
+
await helpers.applyPopulate(_.castArray(rows), state.populate, {
|
|
485
|
+
qb: this,
|
|
486
|
+
uid,
|
|
487
|
+
db,
|
|
488
|
+
});
|
|
480
489
|
}
|
|
481
490
|
|
|
482
491
|
let results = rows;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
|
4
|
+
|
|
5
|
+
let strapi;
|
|
6
|
+
|
|
7
|
+
describe('transactions', () => {
|
|
8
|
+
let original;
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
strapi = await createStrapiInstance();
|
|
11
|
+
original = await strapi.db
|
|
12
|
+
.queryBuilder('strapi::core-store')
|
|
13
|
+
.select(['*'])
|
|
14
|
+
.where({ id: 1 })
|
|
15
|
+
.execute();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterAll(async () => {
|
|
19
|
+
await strapi.destroy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
await strapi.db
|
|
24
|
+
.queryBuilder('strapi::core-store')
|
|
25
|
+
.update({
|
|
26
|
+
key: original[0].key,
|
|
27
|
+
})
|
|
28
|
+
.where({ id: 1 })
|
|
29
|
+
.execute();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('using a transaction method', () => {
|
|
33
|
+
test('commits successfully', async () => {
|
|
34
|
+
await strapi.db.transaction(async () => {
|
|
35
|
+
await strapi.db
|
|
36
|
+
.queryBuilder('strapi::core-store')
|
|
37
|
+
.update({
|
|
38
|
+
key: 'wrong key',
|
|
39
|
+
})
|
|
40
|
+
.where({ id: 1 })
|
|
41
|
+
.execute();
|
|
42
|
+
|
|
43
|
+
await strapi.db
|
|
44
|
+
.queryBuilder('strapi::core-store')
|
|
45
|
+
.update({
|
|
46
|
+
key: 'new key',
|
|
47
|
+
})
|
|
48
|
+
.where({ id: 1 })
|
|
49
|
+
.execute();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const end = await strapi.db
|
|
53
|
+
.queryBuilder('strapi::core-store')
|
|
54
|
+
.select(['*'])
|
|
55
|
+
.where({ id: 1 })
|
|
56
|
+
.execute();
|
|
57
|
+
|
|
58
|
+
expect(end[0].key).toEqual('new key');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('rollback successfully', async () => {
|
|
62
|
+
try {
|
|
63
|
+
await strapi.db.transaction(async () => {
|
|
64
|
+
// this is valid
|
|
65
|
+
await strapi.db
|
|
66
|
+
.queryBuilder('strapi::core-store')
|
|
67
|
+
.update({
|
|
68
|
+
key: 'wrong key',
|
|
69
|
+
})
|
|
70
|
+
.where({ id: 1 })
|
|
71
|
+
.execute();
|
|
72
|
+
|
|
73
|
+
// this throws
|
|
74
|
+
await strapi.db
|
|
75
|
+
.queryBuilder('invalid_uid')
|
|
76
|
+
.update({
|
|
77
|
+
key: 'bad key',
|
|
78
|
+
invalid_key: 'error',
|
|
79
|
+
})
|
|
80
|
+
.where({ id: 1 })
|
|
81
|
+
.execute();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect('this should not be reached').toBe(false);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// do nothing
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const end = await strapi.db
|
|
90
|
+
.queryBuilder('strapi::core-store')
|
|
91
|
+
.select(['*'])
|
|
92
|
+
.where({ id: 1 })
|
|
93
|
+
.execute();
|
|
94
|
+
|
|
95
|
+
expect(end[0].key).toEqual(original[0].key);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('nested rollback -> rollback works', async () => {
|
|
99
|
+
try {
|
|
100
|
+
await strapi.db.transaction(async () => {
|
|
101
|
+
// this is valid
|
|
102
|
+
await strapi.db
|
|
103
|
+
.queryBuilder('strapi::core-store')
|
|
104
|
+
.update({
|
|
105
|
+
key: 'changed key',
|
|
106
|
+
})
|
|
107
|
+
.where({ id: 1 })
|
|
108
|
+
.execute();
|
|
109
|
+
|
|
110
|
+
// here we'll make a nested transaction that throws and then confirm we still have "changed key" from above
|
|
111
|
+
try {
|
|
112
|
+
await strapi.db.transaction(async () => {
|
|
113
|
+
await strapi.db
|
|
114
|
+
.queryBuilder('strapi::core-store')
|
|
115
|
+
.update({
|
|
116
|
+
key: 'changed key - nested',
|
|
117
|
+
})
|
|
118
|
+
.where({ id: 1 })
|
|
119
|
+
.execute();
|
|
120
|
+
|
|
121
|
+
// this should throw and roll back
|
|
122
|
+
await strapi.db
|
|
123
|
+
.queryBuilder('invalid_uid')
|
|
124
|
+
.update({
|
|
125
|
+
invalid_key: 'error',
|
|
126
|
+
})
|
|
127
|
+
.where({ id: 1 })
|
|
128
|
+
.execute();
|
|
129
|
+
});
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// do nothing
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// should equal the result from above
|
|
135
|
+
const result = await strapi.db
|
|
136
|
+
.queryBuilder('strapi::core-store')
|
|
137
|
+
.select(['*'])
|
|
138
|
+
.where({ id: 1 })
|
|
139
|
+
.execute();
|
|
140
|
+
|
|
141
|
+
expect(result[0].key).toEqual('changed key');
|
|
142
|
+
|
|
143
|
+
// this throws
|
|
144
|
+
await strapi.db
|
|
145
|
+
.queryBuilder('invalid_uid')
|
|
146
|
+
.update({
|
|
147
|
+
key: original[0].key,
|
|
148
|
+
invalid_key: 'error',
|
|
149
|
+
})
|
|
150
|
+
.where({ id: 1 })
|
|
151
|
+
.execute();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect('this should not be reached').toBe(false);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// do nothing
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const end = await strapi.db
|
|
160
|
+
.queryBuilder('strapi::core-store')
|
|
161
|
+
.select(['*'])
|
|
162
|
+
.where({ id: 1 })
|
|
163
|
+
.execute();
|
|
164
|
+
|
|
165
|
+
expect(end[0].key).toEqual(original[0].key);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('nested commit -> rollback works', async () => {
|
|
169
|
+
try {
|
|
170
|
+
await strapi.db.transaction(async () => {
|
|
171
|
+
// this is valid
|
|
172
|
+
await strapi.db
|
|
173
|
+
.queryBuilder('strapi::core-store')
|
|
174
|
+
.update({
|
|
175
|
+
key: 'changed key',
|
|
176
|
+
})
|
|
177
|
+
.where({ id: 1 })
|
|
178
|
+
.execute();
|
|
179
|
+
|
|
180
|
+
// here we'll make a nested transaction that works, and then later we'll rollback the outer transaction
|
|
181
|
+
try {
|
|
182
|
+
await strapi.db.transaction(async () => {
|
|
183
|
+
await strapi.db
|
|
184
|
+
.queryBuilder('strapi::core-store')
|
|
185
|
+
.update({
|
|
186
|
+
key: 'changed key - nested',
|
|
187
|
+
})
|
|
188
|
+
.where({ id: 1 })
|
|
189
|
+
.execute();
|
|
190
|
+
});
|
|
191
|
+
} catch (e) {
|
|
192
|
+
// do nothing
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// should equal the result from above
|
|
196
|
+
const result = await strapi.db
|
|
197
|
+
.queryBuilder('strapi::core-store')
|
|
198
|
+
.select(['*'])
|
|
199
|
+
.where({ id: 1 })
|
|
200
|
+
.execute();
|
|
201
|
+
|
|
202
|
+
expect(result[0].key).toEqual('changed key - nested');
|
|
203
|
+
|
|
204
|
+
// this throws
|
|
205
|
+
await strapi.db
|
|
206
|
+
.queryBuilder('invalid_uid')
|
|
207
|
+
.update({
|
|
208
|
+
key: original[0].key,
|
|
209
|
+
invalid_key: 'error',
|
|
210
|
+
})
|
|
211
|
+
.where({ id: 1 })
|
|
212
|
+
.execute();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect('this should not be reached').toBe(false);
|
|
216
|
+
} catch (e) {
|
|
217
|
+
// do nothing
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const end = await strapi.db
|
|
221
|
+
.queryBuilder('strapi::core-store')
|
|
222
|
+
.select(['*'])
|
|
223
|
+
.where({ id: 1 })
|
|
224
|
+
.execute();
|
|
225
|
+
|
|
226
|
+
expect(end[0].key).toEqual(original[0].key);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('using a transaction object', () => {
|
|
231
|
+
test('commits successfully', async () => {
|
|
232
|
+
const trx = await strapi.db.transaction();
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await strapi.db
|
|
236
|
+
.queryBuilder('strapi::core-store')
|
|
237
|
+
.update({
|
|
238
|
+
key: 'wrong key',
|
|
239
|
+
})
|
|
240
|
+
.where({ id: 1 })
|
|
241
|
+
.transacting(trx.get())
|
|
242
|
+
.execute();
|
|
243
|
+
|
|
244
|
+
await strapi.db
|
|
245
|
+
.queryBuilder('strapi::core-store')
|
|
246
|
+
.update({
|
|
247
|
+
key: original[0].key,
|
|
248
|
+
})
|
|
249
|
+
.where({ id: 1 })
|
|
250
|
+
.transacting(trx.get())
|
|
251
|
+
.execute();
|
|
252
|
+
|
|
253
|
+
await trx.commit();
|
|
254
|
+
} catch (e) {
|
|
255
|
+
await trx.rollback();
|
|
256
|
+
console.log(e.message);
|
|
257
|
+
expect('this should not be reached').toBe(false);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const end = await strapi.db
|
|
261
|
+
.queryBuilder('strapi::core-store')
|
|
262
|
+
.select(['*'])
|
|
263
|
+
.where({ id: 1 })
|
|
264
|
+
.execute();
|
|
265
|
+
|
|
266
|
+
expect(end[0].key).toEqual('strapi_content_types_schema');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test('rollback successfully', async () => {
|
|
270
|
+
const trx = await strapi.db.transaction();
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
await strapi.db
|
|
274
|
+
.queryBuilder('strapi::core-store')
|
|
275
|
+
.update({
|
|
276
|
+
key: 'wrong key',
|
|
277
|
+
})
|
|
278
|
+
.where({ id: 1 })
|
|
279
|
+
.transacting(trx.get())
|
|
280
|
+
.execute();
|
|
281
|
+
|
|
282
|
+
// this query should throw because it has errors
|
|
283
|
+
await strapi.db
|
|
284
|
+
.queryBuilder('invalid_uid')
|
|
285
|
+
.update({
|
|
286
|
+
key: 123,
|
|
287
|
+
key_not_here: 'this should error',
|
|
288
|
+
})
|
|
289
|
+
.where({ id: 'this should error' })
|
|
290
|
+
.transacting(trx.get())
|
|
291
|
+
.execute();
|
|
292
|
+
|
|
293
|
+
await trx.commit();
|
|
294
|
+
expect('this should not be reached').toBe(false);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
await trx.rollback();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const end = await strapi.db
|
|
300
|
+
.queryBuilder('strapi::core-store')
|
|
301
|
+
.select(['*'])
|
|
302
|
+
.where({ id: 1 })
|
|
303
|
+
.execute();
|
|
304
|
+
|
|
305
|
+
expect(end[0].key).toEqual('strapi_content_types_schema');
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
4
|
+
|
|
5
|
+
const storage = new AsyncLocalStorage();
|
|
6
|
+
|
|
7
|
+
const transactionCtx = {
|
|
8
|
+
async run(store, cb) {
|
|
9
|
+
return storage.run(store, cb);
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
get() {
|
|
13
|
+
return storage.getStore();
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
module.exports = transactionCtx;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { validateRelations } = require('./relations');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validate if the database is in a valid state before starting the server.
|
|
7
|
+
*
|
|
8
|
+
* @param {*} db - Database instance
|
|
9
|
+
*/
|
|
10
|
+
async function validateDatabase(db) {
|
|
11
|
+
const relationErrors = await validateRelations(db);
|
|
12
|
+
const errorList = [...relationErrors];
|
|
13
|
+
|
|
14
|
+
if (errorList.length > 0) {
|
|
15
|
+
errorList.forEach((error) => strapi.log.error(error));
|
|
16
|
+
throw new Error('There are errors in some of your models. Please check the logs above.');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { validateDatabase };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const types = require('../../types');
|
|
4
|
+
const { getJoinTableName } = require('../../metadata/relations');
|
|
5
|
+
|
|
6
|
+
const getLinksWithoutMappedBy = (db) => {
|
|
7
|
+
const relationsToUpdate = {};
|
|
8
|
+
|
|
9
|
+
db.metadata.forEach((contentType) => {
|
|
10
|
+
const attributes = contentType.attributes;
|
|
11
|
+
|
|
12
|
+
// For each relation attribute, add the joinTable name to tablesToUpdate
|
|
13
|
+
Object.values(attributes).forEach((attribute) => {
|
|
14
|
+
if (!types.isRelation(attribute.type)) return;
|
|
15
|
+
|
|
16
|
+
if (attribute.inversedBy) {
|
|
17
|
+
const invRelation = db.metadata.get(attribute.target).attributes[attribute.inversedBy];
|
|
18
|
+
|
|
19
|
+
// Both relations use inversedBy.
|
|
20
|
+
if (invRelation.inversedBy) {
|
|
21
|
+
relationsToUpdate[attribute.joinTable.name] = {
|
|
22
|
+
relation: attribute,
|
|
23
|
+
invRelation,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return Object.values(relationsToUpdate);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const isLinkTableEmpty = async (db, linkTableName) => {
|
|
34
|
+
// If the table doesn't exist, it's empty
|
|
35
|
+
const exists = await db.getSchemaConnection().hasTable(linkTableName);
|
|
36
|
+
if (!exists) return true;
|
|
37
|
+
|
|
38
|
+
const result = await db.getConnection().count('* as count').from(linkTableName);
|
|
39
|
+
return Number(result[0].count) === 0;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates bidirectional relations before starting the server.
|
|
44
|
+
* - If both sides use inversedBy, one of the sides must switch to mappedBy.
|
|
45
|
+
* When this happens, two join tables exist in the database.
|
|
46
|
+
* This makes sure you switch the side which does not delete any data.
|
|
47
|
+
*
|
|
48
|
+
* @param {*} db
|
|
49
|
+
* @return {*}
|
|
50
|
+
*/
|
|
51
|
+
const validateBidirectionalRelations = async (db) => {
|
|
52
|
+
const invalidLinks = getLinksWithoutMappedBy(db);
|
|
53
|
+
const errorList = [];
|
|
54
|
+
|
|
55
|
+
for (const { relation, invRelation } of invalidLinks) {
|
|
56
|
+
const contentType = db.metadata.get(invRelation.target);
|
|
57
|
+
const invContentType = db.metadata.get(relation.target);
|
|
58
|
+
|
|
59
|
+
// Generate the join table name based on the relation target table and attribute name.
|
|
60
|
+
const joinTableName = getJoinTableName(contentType.tableName, invRelation.inversedBy);
|
|
61
|
+
const inverseJoinTableName = getJoinTableName(invContentType.tableName, relation.inversedBy);
|
|
62
|
+
|
|
63
|
+
const joinTableEmpty = await isLinkTableEmpty(db, joinTableName);
|
|
64
|
+
const inverseJoinTableEmpty = await isLinkTableEmpty(db, inverseJoinTableName);
|
|
65
|
+
|
|
66
|
+
if (joinTableEmpty) {
|
|
67
|
+
process.emitWarning(
|
|
68
|
+
`Error on attribute "${invRelation.inversedBy}" in model "${contentType.singularName}" (${contentType.uid}).` +
|
|
69
|
+
` Please modify your ${contentType.singularName} schema by renaming the key "inversedBy" to "mappedBy".` +
|
|
70
|
+
` Ex: { "inversedBy": "${relation.inversedBy}" } -> { "mappedBy": "${relation.inversedBy}" }`
|
|
71
|
+
);
|
|
72
|
+
} else if (inverseJoinTableEmpty) {
|
|
73
|
+
// Its safe to delete the inverse join table
|
|
74
|
+
process.emitWarning(
|
|
75
|
+
`Error on attribute "${relation.inversedBy}" in model "${invContentType.singularName}" (${invContentType.uid}).` +
|
|
76
|
+
` Please modify your ${invContentType.singularName} schema by renaming the key "inversedBy" to "mappedBy".` +
|
|
77
|
+
` Ex: { "inversedBy": "${invRelation.inversedBy}" } -> { "mappedBy": "${invRelation.inversedBy}" }`
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
// Both sides have data in the join table
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return errorList;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
validateBidirectionalRelations,
|
|
89
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { validateBidirectionalRelations } = require('./bidirectional');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates if relations data and tables are in a valid state before
|
|
7
|
+
* starting the server.
|
|
8
|
+
*/
|
|
9
|
+
const validateRelations = async (db) => {
|
|
10
|
+
const bidirectionalRelationsErrors = await validateBidirectionalRelations(db);
|
|
11
|
+
return [...bidirectionalRelationsErrors];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = { validateRelations };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@strapi/database",
|
|
3
|
-
"version": "4.6.
|
|
3
|
+
"version": "4.6.1",
|
|
4
4
|
"description": "Strapi's database layer",
|
|
5
5
|
"homepage": "https://strapi.io",
|
|
6
6
|
"bugs": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"date-fns": "2.29.3",
|
|
35
35
|
"debug": "4.3.4",
|
|
36
36
|
"fs-extra": "10.0.0",
|
|
37
|
-
"knex": "
|
|
37
|
+
"knex": "2.4.0",
|
|
38
38
|
"lodash": "4.17.21",
|
|
39
39
|
"semver": "7.3.8",
|
|
40
40
|
"umzug": "3.1.1"
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"node": ">=14.19.1 <=18.x.x",
|
|
44
44
|
"npm": ">=6.0.0"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "17a7845e3d453ea2e7911bda6ec25ed196dd5f16"
|
|
47
47
|
}
|