378 lines
15 KiB
JavaScript
378 lines
15 KiB
JavaScript
import _ from 'lodash/fp';
|
|
|
|
// TODO: get that list dynamically instead
|
|
const RESERVED_TABLE_NAMES = [
|
|
'strapi_migrations',
|
|
'strapi_migrations_internal',
|
|
'strapi_database_schema'
|
|
];
|
|
const statuses = {
|
|
CHANGED: 'CHANGED',
|
|
UNCHANGED: 'UNCHANGED'
|
|
};
|
|
// NOTE:We could move the schema to use maps of tables & columns instead of arrays to make it easier to diff
|
|
// => this will make the creation a bit more complicated (ordering, Object.values(tables | columns)) -> not a big pbl
|
|
const helpers = {
|
|
hasTable (schema, tableName) {
|
|
return schema.tables.findIndex((table)=>table.name === tableName) !== -1;
|
|
},
|
|
findTable (schema, tableName) {
|
|
return schema.tables.find((table)=>table.name === tableName);
|
|
},
|
|
hasColumn (table, columnName) {
|
|
return table.columns.findIndex((column)=>column.name === columnName) !== -1;
|
|
},
|
|
findColumn (table, columnName) {
|
|
return table.columns.find((column)=>column.name === columnName);
|
|
},
|
|
hasIndex (table, columnName) {
|
|
return table.indexes.findIndex((column)=>column.name === columnName) !== -1;
|
|
},
|
|
findIndex (table, columnName) {
|
|
return table.indexes.find((column)=>column.name === columnName);
|
|
},
|
|
hasForeignKey (table, columnName) {
|
|
return table.foreignKeys.findIndex((column)=>column.name === columnName) !== -1;
|
|
},
|
|
findForeignKey (table, columnName) {
|
|
return table.foreignKeys.find((column)=>column.name === columnName);
|
|
}
|
|
};
|
|
var createSchemaDiff = ((db)=>{
|
|
const hasChangedStatus = (diff)=>diff.status === statuses.CHANGED;
|
|
/**
|
|
* Compares two indexes info
|
|
* @param {Object} oldIndex - index info read from DB
|
|
* @param {Object} index - newly generate index info
|
|
*/ const diffIndexes = (oldIndex, index)=>{
|
|
const changes = [];
|
|
// use xor to avoid differences in order
|
|
if (_.xor(oldIndex.columns, index.columns).length > 0) {
|
|
changes.push('columns');
|
|
}
|
|
if (oldIndex.type && index.type && _.toLower(oldIndex.type) !== _.toLower(index.type)) {
|
|
changes.push('type');
|
|
}
|
|
return {
|
|
status: changes.length > 0 ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
name: index.name,
|
|
object: index
|
|
}
|
|
};
|
|
};
|
|
/**
|
|
* Compares two foreign keys info
|
|
* @param {Object} oldForeignKey - foreignKey info read from DB
|
|
* @param {Object} foreignKey - newly generate foreignKey info
|
|
*/ const diffForeignKeys = (oldForeignKey, foreignKey)=>{
|
|
const changes = [];
|
|
if (_.difference(oldForeignKey.columns, foreignKey.columns).length > 0) {
|
|
changes.push('columns');
|
|
}
|
|
if (_.difference(oldForeignKey.referencedColumns, foreignKey.referencedColumns).length > 0) {
|
|
changes.push('referencedColumns');
|
|
}
|
|
if (oldForeignKey.referencedTable !== foreignKey.referencedTable) {
|
|
changes.push('referencedTable');
|
|
}
|
|
if (_.isNil(oldForeignKey.onDelete) || _.toUpper(oldForeignKey.onDelete) === 'NO ACTION') {
|
|
if (!_.isNil(foreignKey.onDelete) && _.toUpper(oldForeignKey.onDelete ?? '') !== 'NO ACTION') {
|
|
changes.push('onDelete');
|
|
}
|
|
} else if (_.toUpper(oldForeignKey.onDelete) !== _.toUpper(foreignKey.onDelete ?? '')) {
|
|
changes.push('onDelete');
|
|
}
|
|
if (_.isNil(oldForeignKey.onUpdate) || _.toUpper(oldForeignKey.onUpdate) === 'NO ACTION') {
|
|
if (!_.isNil(foreignKey.onUpdate) && _.toUpper(oldForeignKey.onUpdate ?? '') !== 'NO ACTION') {
|
|
changes.push('onUpdate');
|
|
}
|
|
} else if (_.toUpper(oldForeignKey.onUpdate) !== _.toUpper(foreignKey.onUpdate ?? '')) {
|
|
changes.push('onUpdate');
|
|
}
|
|
return {
|
|
status: changes.length > 0 ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
name: foreignKey.name,
|
|
object: foreignKey
|
|
}
|
|
};
|
|
};
|
|
const diffDefault = (oldColumn, column)=>{
|
|
const oldDefaultTo = oldColumn.defaultTo;
|
|
const { defaultTo } = column;
|
|
if (oldDefaultTo === null || _.toLower(oldDefaultTo) === 'null') {
|
|
return _.isNil(defaultTo) || _.toLower(defaultTo) === 'null';
|
|
}
|
|
return _.toLower(oldDefaultTo) === _.toLower(column.defaultTo) || _.toLower(oldDefaultTo) === _.toLower(`'${column.defaultTo}'`);
|
|
};
|
|
/**
|
|
* Compares two columns info
|
|
* @param {Object} oldColumn - column info read from DB
|
|
* @param {Object} column - newly generate column info
|
|
*/ const diffColumns = (oldColumn, column)=>{
|
|
const changes = [];
|
|
const isIgnoredType = [
|
|
'increments'
|
|
].includes(column.type);
|
|
const oldType = oldColumn.type;
|
|
const type = db.dialect.getSqlType(column.type);
|
|
if (oldType !== type && !isIgnoredType) {
|
|
changes.push('type');
|
|
}
|
|
// NOTE: compare args at some point and split them into specific properties instead
|
|
if (oldColumn.notNullable !== column.notNullable) {
|
|
changes.push('notNullable');
|
|
}
|
|
const hasSameDefault = diffDefault(oldColumn, column);
|
|
if (!hasSameDefault) {
|
|
changes.push('defaultTo');
|
|
}
|
|
if (oldColumn.unsigned !== column.unsigned && db.dialect.supportsUnsigned()) {
|
|
changes.push('unsigned');
|
|
}
|
|
return {
|
|
status: changes.length > 0 ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
name: column.name,
|
|
object: column
|
|
}
|
|
};
|
|
};
|
|
const diffTableColumns = (diffCtx)=>{
|
|
const { databaseTable, userSchemaTable, previousTable } = diffCtx;
|
|
const addedColumns = [];
|
|
const updatedColumns = [];
|
|
const unchangedColumns = [];
|
|
const removedColumns = [];
|
|
for (const userSchemaColumn of userSchemaTable.columns){
|
|
const databaseColumn = helpers.findColumn(databaseTable, userSchemaColumn.name);
|
|
if (databaseColumn) {
|
|
const { status, diff } = diffColumns(databaseColumn, userSchemaColumn);
|
|
if (status === statuses.CHANGED) {
|
|
updatedColumns.push(diff);
|
|
} else {
|
|
unchangedColumns.push(databaseColumn);
|
|
}
|
|
} else {
|
|
addedColumns.push(userSchemaColumn);
|
|
}
|
|
}
|
|
for (const databaseColumn of databaseTable.columns){
|
|
if (!helpers.hasColumn(userSchemaTable, databaseColumn.name) && previousTable && helpers.hasColumn(previousTable, databaseColumn.name)) {
|
|
removedColumns.push(databaseColumn);
|
|
}
|
|
}
|
|
const hasChanged = [
|
|
addedColumns,
|
|
updatedColumns,
|
|
removedColumns
|
|
].some((arr)=>arr.length > 0);
|
|
return {
|
|
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
added: addedColumns,
|
|
updated: updatedColumns,
|
|
unchanged: unchangedColumns,
|
|
removed: removedColumns
|
|
}
|
|
};
|
|
};
|
|
const diffTableIndexes = (diffCtx)=>{
|
|
const { databaseTable, userSchemaTable, previousTable } = diffCtx;
|
|
const addedIndexes = [];
|
|
const updatedIndexes = [];
|
|
const unchangedIndexes = [];
|
|
const removedIndexes = [];
|
|
for (const userSchemaIndex of userSchemaTable.indexes){
|
|
const databaseIndex = helpers.findIndex(databaseTable, userSchemaIndex.name);
|
|
if (databaseIndex) {
|
|
const { status, diff } = diffIndexes(databaseIndex, userSchemaIndex);
|
|
if (status === statuses.CHANGED) {
|
|
updatedIndexes.push(diff);
|
|
} else {
|
|
unchangedIndexes.push(databaseIndex);
|
|
}
|
|
} else {
|
|
addedIndexes.push(userSchemaIndex);
|
|
}
|
|
}
|
|
for (const databaseIndex of databaseTable.indexes){
|
|
if (!helpers.hasIndex(userSchemaTable, databaseIndex.name) && previousTable && helpers.hasIndex(previousTable, databaseIndex.name)) {
|
|
removedIndexes.push(databaseIndex);
|
|
}
|
|
}
|
|
const hasChanged = [
|
|
addedIndexes,
|
|
updatedIndexes,
|
|
removedIndexes
|
|
].some((arr)=>arr.length > 0);
|
|
return {
|
|
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
added: addedIndexes,
|
|
updated: updatedIndexes,
|
|
unchanged: unchangedIndexes,
|
|
removed: removedIndexes
|
|
}
|
|
};
|
|
};
|
|
const diffTableForeignKeys = (diffCtx)=>{
|
|
const { databaseTable, userSchemaTable, previousTable } = diffCtx;
|
|
const addedForeignKeys = [];
|
|
const updatedForeignKeys = [];
|
|
const unchangedForeignKeys = [];
|
|
const removedForeignKeys = [];
|
|
if (!db.dialect.usesForeignKeys()) {
|
|
return {
|
|
status: statuses.UNCHANGED,
|
|
diff: {
|
|
added: addedForeignKeys,
|
|
updated: updatedForeignKeys,
|
|
unchanged: unchangedForeignKeys,
|
|
removed: removedForeignKeys
|
|
}
|
|
};
|
|
}
|
|
for (const userSchemaForeignKeys of userSchemaTable.foreignKeys){
|
|
const databaseForeignKeys = helpers.findForeignKey(databaseTable, userSchemaForeignKeys.name);
|
|
if (databaseForeignKeys) {
|
|
const { status, diff } = diffForeignKeys(databaseForeignKeys, userSchemaForeignKeys);
|
|
if (status === statuses.CHANGED) {
|
|
updatedForeignKeys.push(diff);
|
|
} else {
|
|
unchangedForeignKeys.push(databaseForeignKeys);
|
|
}
|
|
} else {
|
|
addedForeignKeys.push(userSchemaForeignKeys);
|
|
}
|
|
}
|
|
for (const databaseForeignKeys of databaseTable.foreignKeys){
|
|
if (!helpers.hasForeignKey(userSchemaTable, databaseForeignKeys.name) && previousTable && helpers.hasForeignKey(previousTable, databaseForeignKeys.name)) {
|
|
removedForeignKeys.push(databaseForeignKeys);
|
|
}
|
|
}
|
|
const hasChanged = [
|
|
addedForeignKeys,
|
|
updatedForeignKeys,
|
|
removedForeignKeys
|
|
].some((arr)=>arr.length > 0);
|
|
return {
|
|
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
added: addedForeignKeys,
|
|
updated: updatedForeignKeys,
|
|
unchanged: unchangedForeignKeys,
|
|
removed: removedForeignKeys
|
|
}
|
|
};
|
|
};
|
|
const diffTables = (diffCtx)=>{
|
|
const { databaseTable } = diffCtx;
|
|
const columnsDiff = diffTableColumns(diffCtx);
|
|
const indexesDiff = diffTableIndexes(diffCtx);
|
|
const foreignKeysDiff = diffTableForeignKeys(diffCtx);
|
|
const hasChanged = [
|
|
columnsDiff,
|
|
indexesDiff,
|
|
foreignKeysDiff
|
|
].some(hasChangedStatus);
|
|
return {
|
|
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
name: databaseTable.name,
|
|
indexes: indexesDiff.diff,
|
|
foreignKeys: foreignKeysDiff.diff,
|
|
columns: columnsDiff.diff
|
|
}
|
|
};
|
|
};
|
|
const diffSchemas = async (schemaDiffCtx)=>{
|
|
const { previousSchema, databaseSchema, userSchema } = schemaDiffCtx;
|
|
const addedTables = [];
|
|
const updatedTables = [];
|
|
const unchangedTables = [];
|
|
const removedTables = [];
|
|
// for each table in the user schema, check if it already exists in the database schema
|
|
for (const userSchemaTable of userSchema.tables){
|
|
const databaseTable = helpers.findTable(databaseSchema, userSchemaTable.name);
|
|
const previousTable = previousSchema && helpers.findTable(previousSchema, userSchemaTable.name);
|
|
if (databaseTable) {
|
|
const { status, diff } = diffTables({
|
|
previousTable,
|
|
databaseTable,
|
|
userSchemaTable
|
|
});
|
|
if (status === statuses.CHANGED) {
|
|
updatedTables.push(diff);
|
|
} else {
|
|
unchangedTables.push(databaseTable);
|
|
}
|
|
} else {
|
|
addedTables.push(userSchemaTable);
|
|
}
|
|
}
|
|
// maintain audit logs table from EE -> CE
|
|
const parsePersistedTable = (persistedTable)=>{
|
|
if (typeof persistedTable === 'string') {
|
|
return persistedTable;
|
|
}
|
|
return persistedTable.name;
|
|
};
|
|
const persistedTables = helpers.hasTable(databaseSchema, 'strapi_core_store_settings') ? await strapi.store.get({
|
|
type: 'core',
|
|
key: 'persisted_tables'
|
|
}) ?? [] : [];
|
|
const reservedTables = [
|
|
...RESERVED_TABLE_NAMES,
|
|
...persistedTables.map(parsePersistedTable)
|
|
];
|
|
// for all tables in the database schema, check if they are not in the user schema
|
|
for (const databaseTable of databaseSchema.tables){
|
|
const isInUserSchema = helpers.hasTable(userSchema, databaseTable.name);
|
|
const wasTracked = previousSchema && helpers.hasTable(previousSchema, databaseTable.name);
|
|
const isReserved = reservedTables.includes(databaseTable.name);
|
|
// NOTE: if db table is not in the user schema and is not in the previous stored schema leave it alone. it is a user custom table that we should not touch
|
|
if (!isInUserSchema && !wasTracked) {
|
|
continue;
|
|
}
|
|
// if a db table is not in the user schema I want to delete it
|
|
if (!isInUserSchema && wasTracked && !isReserved) {
|
|
const dependencies = persistedTables.filter((table)=>{
|
|
const dependsOn = table?.dependsOn;
|
|
if (!_.isArray(dependsOn)) {
|
|
return;
|
|
}
|
|
return dependsOn.some((table)=>table.name === databaseTable.name);
|
|
}).map((dependsOnTable)=>{
|
|
return databaseSchema.tables.find((databaseTable)=>databaseTable.name === dependsOnTable.name);
|
|
})// In case the table is not found, filter undefined values
|
|
.filter((table)=>!_.isNil(table));
|
|
removedTables.push(databaseTable, ...dependencies);
|
|
}
|
|
}
|
|
const hasChanged = [
|
|
addedTables,
|
|
updatedTables,
|
|
removedTables
|
|
].some((arr)=>arr.length > 0);
|
|
return {
|
|
status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED,
|
|
diff: {
|
|
tables: {
|
|
added: addedTables,
|
|
updated: updatedTables,
|
|
unchanged: unchangedTables,
|
|
removed: removedTables
|
|
}
|
|
}
|
|
};
|
|
};
|
|
return {
|
|
diff: diffSchemas
|
|
};
|
|
});
|
|
|
|
export { createSchemaDiff as default };
|
|
//# sourceMappingURL=diff.mjs.map
|