'use strict'; const _ = require('lodash/fp'); const RESERVED_TABLE_NAMES = ['strapi_migrations', '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); }, }; module.exports = (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 = []; if (!_.isEqual(oldIndex.columns, index.columns)) { changes.push('columns'); } if (_.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 = (srcTable, destTable) => { const addedColumns = []; const updatedColumns = []; const unchangedColumns = []; const removedColumns = []; for (const destColumn of destTable.columns) { if (!helpers.hasColumn(srcTable, destColumn.name)) { addedColumns.push(destColumn); continue; } const srcColumn = helpers.findColumn(srcTable, destColumn.name); const { status, diff } = diffColumns(srcColumn, destColumn); if (status === statuses.CHANGED) { updatedColumns.push(diff); } else { unchangedColumns.push(srcColumn); } } for (const srcColumn of srcTable.columns) { if (!helpers.hasColumn(destTable, srcColumn.name)) { removedColumns.push(srcColumn); } } 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 = (srcTable, destTable) => { const addedIndexes = []; const updatedIndexes = []; const unchangedIndexes = []; const removedIndexes = []; for (const destIndex of destTable.indexes) { if (helpers.hasIndex(srcTable, destIndex.name)) { const srcIndex = helpers.findIndex(srcTable, destIndex.name); const { status, diff } = diffIndexes(srcIndex, destIndex); if (status === statuses.CHANGED) { updatedIndexes.push(diff); } else { unchangedIndexes.push(srcIndex); } } else { addedIndexes.push(destIndex); } } for (const srcIndex of srcTable.indexes) { if (!helpers.hasIndex(destTable, srcIndex.name)) { removedIndexes.push(srcIndex); } } 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 = (srcTable, destTable) => { 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 destForeignKey of destTable.foreignKeys) { if (helpers.hasForeignKey(srcTable, destForeignKey.name)) { const srcForeignKey = helpers.findForeignKey(srcTable, destForeignKey.name); const { status, diff } = diffForeignKeys(srcForeignKey, destForeignKey); if (status === statuses.CHANGED) { updatedForeignKeys.push(diff); } else { unchangedForeignKeys.push(srcForeignKey); } } else { addedForeignKeys.push(destForeignKey); } } for (const srcForeignKey of srcTable.foreignKeys) { if (!helpers.hasForeignKey(destTable, srcForeignKey.name)) { removedForeignKeys.push(srcForeignKey); } } 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 = (srcTable, destTable) => { const columnsDiff = diffTableColumns(srcTable, destTable); const indexesDiff = diffTableIndexes(srcTable, destTable); const foreignKeysDiff = diffTableForeignKeys(srcTable, destTable); const hasChanged = [columnsDiff, indexesDiff, foreignKeysDiff].some(hasChangedStatus); return { status: hasChanged ? statuses.CHANGED : statuses.UNCHANGED, diff: { name: srcTable.name, indexes: indexesDiff.diff, foreignKeys: foreignKeysDiff.diff, columns: columnsDiff.diff, }, }; }; const diffSchemas = async (srcSchema, destSchema) => { const addedTables = []; const updatedTables = []; const unchangedTables = []; const removedTables = []; for (const destTable of destSchema.tables) { if (helpers.hasTable(srcSchema, destTable.name)) { const srcTable = helpers.findTable(srcSchema, destTable.name); const { status, diff } = diffTables(srcTable, destTable); if (status === statuses.CHANGED) { updatedTables.push(diff); } else { unchangedTables.push(srcTable); } } else { addedTables.push(destTable); } } const persistedTables = helpers.hasTable(srcSchema, 'strapi_core_store_settings') ? (await strapi.store.get({ type: 'core', key: 'persisted_tables', })) ?? [] : []; for (const srcTable of srcSchema.tables) { if ( !helpers.hasTable(destSchema, srcTable.name) && ![...RESERVED_TABLE_NAMES, ...persistedTables].includes(srcTable.name) ) { removedTables.push(srcTable); } } 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, }; };