mirror of
https://github.com/strapi/strapi.git
synced 2025-10-29 17:04:13 +00:00
add validation folder in database
This commit is contained in:
parent
189a67b237
commit
74c6ec1f61
@ -11,8 +11,7 @@ const errors = require('./errors');
|
|||||||
|
|
||||||
// TODO: move back into strapi
|
// TODO: move back into strapi
|
||||||
const { transformContentTypes } = require('./utils/content-types');
|
const { transformContentTypes } = require('./utils/content-types');
|
||||||
const { getJoinTableName } = require('./metadata/relations');
|
const { validateDatabase } = require('./validations');
|
||||||
const types = require('./types');
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
constructor(config) {
|
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
|
// TODO: move into strapi
|
||||||
Database.transformContentTypes = transformContentTypes;
|
Database.transformContentTypes = transformContentTypes;
|
||||||
Database.init = async (config) => {
|
Database.init = async (config) => {
|
||||||
const db = new Database(config);
|
const db = new Database(config);
|
||||||
|
await validateDatabase(db);
|
||||||
// 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.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
20
packages/core/database/lib/validations/index.js
Normal file
20
packages/core/database/lib/validations/index.js
Normal 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 };
|
||||||
@ -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,
|
||||||
|
};
|
||||||
14
packages/core/database/lib/validations/relations/index.js
Normal file
14
packages/core/database/lib/validations/relations/index.js
Normal 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 };
|
||||||
Loading…
x
Reference in New Issue
Block a user