add validation folder in database

This commit is contained in:
Marc-Roig 2023-01-02 11:28:48 +01:00
parent 189a67b237
commit 74c6ec1f61
4 changed files with 135 additions and 72 deletions

View File

@ -11,8 +11,7 @@ const errors = require('./errors');
// TODO: move back into strapi
const { transformContentTypes } = require('./utils/content-types');
const { getJoinTableName } = require('./metadata/relations');
const types = require('./types');
const { validateDatabase } = require('./validations');
class Database {
constructor(config) {
@ -74,80 +73,11 @@ class Database {
}
}
const getLinks = ({ db }) => {
const relationsToUpdate = {};
db.metadata.forEach((contentType) => {
const attributes = contentType.attributes;
// For each relation type, 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);
};
// TODO: move into strapi
Database.transformContentTypes = transformContentTypes;
Database.init = async (config) => {
const db = new Database(config);
// TODO: Create validations folder for this.
const links = getLinks({ db });
const errorList = [];
for (const { relation, invRelation } of links) {
// Generate the join table name based on the relation target
// table and attribute name.
const joinTableName = getJoinTableName(
db.metadata.get(relation.target).tableName,
relation.inversedBy
);
const contentType = db.metadata.get(invRelation.target);
const invContentType = db.metadata.get(relation.target);
// If both sides use inversedBy
if (relation.inversedBy && invRelation.inversedBy) {
// If the generated join table name is the same as the one assigned in relation.joinTable,
// relation is on the inversed side of the bidirectional relation.
// and the other is on the owner side.
if (joinTableName === relation.joinTable.name) {
errorList.push(
`Error on attribute "${invRelation.inversedBy}" in model "${invContentType.tableName}"(${invContentType.uid}):` +
` One of the sides of the relationship must be the owning side. You should use mappedBy` +
` instead of inversedBy in the relation "${invRelation.inversedBy}".`
);
} else {
errorList.push(
`Error on attribute "${relation.inversedBy}" in model "${contentType.tableName}"(${contentType.uid}):` +
` One of the sides of the relationship must be the owning side. You should use mappedBy` +
` instead of inversedBy in the relation "${relation.inversedBy}".`
);
}
}
}
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.');
}
await validateDatabase(db);
return db;
};

View File

@ -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 };

View File

@ -0,0 +1,99 @@
'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 result.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) {
// Generate the join table name based on the relation target
// table and attribute name.
const inverseJoinTableName = getJoinTableName(
db.metadata.get(relation.target).tableName,
relation.inversedBy
);
const joinTableName = getJoinTableName(
db.metadata.get(invRelation.target).tableName,
invRelation.inversedBy
);
const contentType = db.metadata.get(invRelation.target);
const invContentType = db.metadata.get(relation.target);
// If both sides use inversedBy
if (relation.inversedBy && invRelation.inversedBy) {
const linkTableEmpty = await isLinkTableEmpty(db, joinTableName);
const inverseLinkTableEmpty = await isLinkTableEmpty(db, inverseJoinTableName);
if (linkTableEmpty) {
errorList.push(
`Error on attribute "${relation.inversedBy}" in model "${contentType.tableName}"(${contentType.uid}):` +
` One of the sides of the relationship must be the owning side. You should use mappedBy` +
` instead of inversedBy in the relation "${relation.inversedBy}".`
);
} else if (inverseLinkTableEmpty) {
// Its safe to delete the inverse join table
errorList.push(
`Error on attribute "${invRelation.inversedBy}" in model "${invContentType.tableName}"(${invContentType.uid}):` +
` One of the sides of the relationship must be the owning side. You should use mappedBy` +
` instead of inversedBy in the relation "${invRelation.inversedBy}".`
);
} else {
// Both sides have data in the join table
}
}
}
return errorList;
};
module.exports = {
validateBidirectionalRelations,
};

View File

@ -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 };