-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15217 from strapi/fix/duplicate-join-tables
- Loading branch information
Showing
5 changed files
with
133 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
'use strict'; | ||
|
||
const { validateRelations } = require('./relations'); | ||
|
||
/** | ||
* Validate if the database is in a valid state before starting the server. | ||
* | ||
* @param {*} db - Database instance | ||
*/ | ||
async function validateDatabase(db) { | ||
const relationErrors = await validateRelations(db); | ||
const errorList = [...relationErrors]; | ||
|
||
if (errorList.length > 0) { | ||
errorList.forEach((error) => strapi.log.error(error)); | ||
throw new Error('There are errors in some of your models. Please check the logs above.'); | ||
} | ||
} | ||
|
||
module.exports = { validateDatabase }; |
89 changes: 89 additions & 0 deletions
89
packages/core/database/lib/validations/relations/bidirectional.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
'use strict'; | ||
|
||
const types = require('../../types'); | ||
const { getJoinTableName } = require('../../metadata/relations'); | ||
|
||
const getLinksWithoutMappedBy = (db) => { | ||
const relationsToUpdate = {}; | ||
|
||
db.metadata.forEach((contentType) => { | ||
const attributes = contentType.attributes; | ||
|
||
// For each relation attribute, add the joinTable name to tablesToUpdate | ||
Object.values(attributes).forEach((attribute) => { | ||
if (!types.isRelation(attribute.type)) return; | ||
|
||
if (attribute.inversedBy) { | ||
const invRelation = db.metadata.get(attribute.target).attributes[attribute.inversedBy]; | ||
|
||
// Both relations use inversedBy. | ||
if (invRelation.inversedBy) { | ||
relationsToUpdate[attribute.joinTable.name] = { | ||
relation: attribute, | ||
invRelation, | ||
}; | ||
} | ||
} | ||
}); | ||
}); | ||
|
||
return Object.values(relationsToUpdate); | ||
}; | ||
|
||
const isLinkTableEmpty = async (db, linkTableName) => { | ||
// If the table doesn't exist, it's empty | ||
const exists = await db.getConnection().schema.hasTable(linkTableName); | ||
if (!exists) return true; | ||
|
||
const result = await db.getConnection().count('* as count').from(linkTableName); | ||
return Number(result[0].count) === 0; | ||
}; | ||
|
||
/** | ||
* Validates bidirectional relations before starting the server. | ||
* - If both sides use inversedBy, one of the sides must switch to mappedBy. | ||
* When this happens, two join tables exist in the database. | ||
* This makes sure you switch the side which does not delete any data. | ||
* | ||
* @param {*} db | ||
* @return {*} | ||
*/ | ||
const validateBidirectionalRelations = async (db) => { | ||
const invalidLinks = getLinksWithoutMappedBy(db); | ||
const errorList = []; | ||
|
||
for (const { relation, invRelation } of invalidLinks) { | ||
const contentType = db.metadata.get(invRelation.target); | ||
const invContentType = db.metadata.get(relation.target); | ||
|
||
// Generate the join table name based on the relation target table and attribute name. | ||
const joinTableName = getJoinTableName(contentType.tableName, invRelation.inversedBy); | ||
const inverseJoinTableName = getJoinTableName(invContentType.tableName, relation.inversedBy); | ||
|
||
const joinTableEmpty = await isLinkTableEmpty(db, joinTableName); | ||
const inverseJoinTableEmpty = await isLinkTableEmpty(db, inverseJoinTableName); | ||
|
||
if (joinTableEmpty) { | ||
process.emitWarning( | ||
`Error on attribute "${invRelation.inversedBy}" in model "${contentType.singularName}" (${contentType.uid}).` + | ||
` Please modify your ${contentType.singularName} schema by renaming the key "inversedBy" to "mappedBy".` + | ||
` Ex: { "inversedBy": "${relation.inversedBy}" } -> { "mappedBy": "${relation.inversedBy}" }` | ||
); | ||
} else if (inverseJoinTableEmpty) { | ||
// Its safe to delete the inverse join table | ||
process.emitWarning( | ||
`Error on attribute "${relation.inversedBy}" in model "${invContentType.singularName}" (${invContentType.uid}).` + | ||
` Please modify your ${invContentType.singularName} schema by renaming the key "inversedBy" to "mappedBy".` + | ||
` Ex: { "inversedBy": "${invRelation.inversedBy}" } -> { "mappedBy": "${invRelation.inversedBy}" }` | ||
); | ||
} else { | ||
// Both sides have data in the join table | ||
} | ||
} | ||
|
||
return errorList; | ||
}; | ||
|
||
module.exports = { | ||
validateBidirectionalRelations, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
'use strict'; | ||
|
||
const { validateBidirectionalRelations } = require('./bidirectional'); | ||
|
||
/** | ||
* Validates if relations data and tables are in a valid state before | ||
* starting the server. | ||
*/ | ||
const validateRelations = async (db) => { | ||
const bidirectionalRelationsErrors = await validateBidirectionalRelations(db); | ||
return [...bidirectionalRelationsErrors]; | ||
}; | ||
|
||
module.exports = { validateRelations }; |