379 lines
11 KiB
TypeScript
Raw Normal View History

2023-07-19 16:35:50 +02:00
import { isNil, prop, omit, castArray } from 'lodash/fp';
import createDebug from 'debug';
2021-06-02 15:53:22 +02:00
2023-07-19 16:35:50 +02:00
import type { Knex } from 'knex';
import type { Database } from '..';
import type { Schema, Table, SchemaDiff, TableDiff, ForeignKey, Index, Column } from './types';
2021-06-02 15:53:22 +02:00
2023-07-19 16:35:50 +02:00
const debug = createDebug('strapi::database');
export default (db: Database) => {
const helpers = createHelpers(db);
return {
/**
* Returns a knex schema builder instance
* @param {string} table - table name
*/
2023-07-19 16:35:50 +02:00
getSchemaBuilder(trx: Knex.Transaction) {
return db.getSchemaConnection(trx);
},
/**
* Creates schema in DB
*/
2023-07-19 16:35:50 +02:00
async createSchema(schema: Schema) {
2022-08-08 23:33:39 +02:00
await db.connection.transaction(async (trx) => {
await this.createTables(schema.tables, trx);
});
},
/**
* Creates a list of tables in a schema
* @param {KnexInstance} trx
* @param {Table[]} tables
*/
2023-07-19 16:35:50 +02:00
async createTables(tables: Table[], trx: Knex.Transaction) {
for (const table of tables) {
debug(`Creating table: ${table.name}`);
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.createTable(schemaBuilder, table);
}
2021-06-02 15:53:22 +02:00
// create FKs once all the tables exist
for (const table of tables) {
debug(`Creating table foreign keys: ${table.name}`);
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.createTableForeignKeys(schemaBuilder, table);
}
},
/**
* Drops schema from DB
*/
2023-07-19 16:35:50 +02:00
async dropSchema(schema: Schema, { dropDatabase = false } = {}) {
if (dropDatabase) {
// TODO: drop database & return as it will drop everything
return;
}
2021-06-02 15:53:22 +02:00
2022-08-08 23:33:39 +02:00
await db.connection.transaction(async (trx) => {
for (const table of schema.tables.reverse()) {
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.dropTable(schemaBuilder, table);
}
});
},
/**
* Applies a schema diff update in the DB
* @param {*} schemaDiff
*/
// TODO: implement force option to disable removal in DB
2023-09-12 21:47:42 +02:00
async updateSchema(schemaDiff: SchemaDiff['diff']) {
2023-07-19 16:35:50 +02:00
const forceMigration = db.config.settings?.forceMigration;
await db.dialect.startSchemaUpdate();
2022-08-08 23:33:39 +02:00
await db.connection.transaction(async (trx) => {
await this.createTables(schemaDiff.tables.added, trx);
if (forceMigration) {
// drop all delete table foreign keys then delete the tables
for (const table of schemaDiff.tables.removed) {
debug(`Removing table foreign keys: ${table.name}`);
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.dropTableForeignKeys(schemaBuilder, table);
}
for (const table of schemaDiff.tables.removed) {
debug(`Removing table: ${table.name}`);
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.dropTable(schemaBuilder, table);
}
}
for (const table of schemaDiff.tables.updated) {
debug(`Updating table: ${table.name}`);
// alter table
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.alterTable(schemaBuilder, table);
}
});
await db.dialect.endSchemaUpdate();
},
};
};
2021-06-02 15:53:22 +02:00
2023-07-19 16:35:50 +02:00
const createHelpers = (db: Database) => {
2021-06-17 16:17:15 +02:00
/**
* Creates a foreign key on a table
2021-06-17 16:17:15 +02:00
*/
2023-07-19 16:35:50 +02:00
const createForeignKey = (tableBuilder: Knex.TableBuilder, foreignKey: ForeignKey) => {
const { name, columns, referencedColumns, referencedTable, onDelete, onUpdate } = foreignKey;
const constraint = tableBuilder
.foreign(columns, name)
.references(referencedColumns)
2023-07-19 16:35:50 +02:00
.inTable(db.getSchemaName() ? `${db.getSchemaName()}.${referencedTable}` : referencedTable);
if (onDelete) {
constraint.onDelete(onDelete);
2021-06-02 15:53:22 +02:00
}
if (onUpdate) {
constraint.onUpdate(onUpdate);
2021-06-02 15:53:22 +02:00
}
};
2021-06-17 16:17:15 +02:00
/**
* Drops a foreign key from a table
2021-06-17 16:17:15 +02:00
*/
2023-07-19 16:35:50 +02:00
const dropForeignKey = (tableBuilder: Knex.TableBuilder, foreignKey: ForeignKey) => {
const { name, columns } = foreignKey;
2021-06-02 15:53:22 +02:00
tableBuilder.dropForeign(columns, name);
};
2021-06-02 15:53:22 +02:00
2021-06-17 16:17:15 +02:00
/**
* Creates an index on a table
2021-06-17 16:17:15 +02:00
*/
2023-07-19 16:35:50 +02:00
const createIndex = (tableBuilder: Knex.TableBuilder, index: Index) => {
const { type, columns, name } = index;
switch (type) {
case 'primary': {
return tableBuilder.primary(columns, name);
}
case 'unique': {
return tableBuilder.unique(columns, name);
}
default: {
return tableBuilder.index(columns, name, type);
2021-06-02 15:53:22 +02:00
}
}
};
2021-06-02 15:53:22 +02:00
/**
* Drops an index from table
* @param {Knex.TableBuilder} tableBuilder
* @param {Index} index
*/
2023-07-19 16:35:50 +02:00
const dropIndex = (tableBuilder: Knex.TableBuilder, index: Index) => {
if (!db.config.settings?.forceMigration) {
return;
}
const { type, columns, name } = index;
2021-06-02 15:53:22 +02:00
switch (type) {
case 'primary': {
return tableBuilder.dropPrimary(name);
2021-06-17 16:17:15 +02:00
}
case 'unique': {
return tableBuilder.dropUnique(columns, name);
}
default: {
return tableBuilder.dropIndex(columns, name);
}
}
};
2021-06-02 15:53:22 +02:00
/**
* Creates a column in a table
*/
2023-07-19 16:35:50 +02:00
const createColumn = (tableBuilder: Knex.TableBuilder, column: Column) => {
const { type, name, args = [], defaultTo, unsigned, notNullable } = column;
2021-06-17 16:17:15 +02:00
2023-09-12 21:47:42 +02:00
const col = (tableBuilder[type as keyof Knex.TableBuilder] as any)(name, ...args);
2021-06-17 16:17:15 +02:00
2021-09-15 12:25:09 +02:00
if (unsigned === true) {
col.unsigned();
2021-06-17 16:17:15 +02:00
}
2021-06-02 15:53:22 +02:00
2021-09-15 12:25:09 +02:00
if (!isNil(defaultTo)) {
const [value, opts] = castArray(defaultTo);
if (prop('isRaw', opts)) {
col.defaultTo(db.connection.raw(value), omit('isRaw', opts));
} else {
col.defaultTo(value, opts);
}
2021-06-17 16:17:15 +02:00
}
2021-09-15 12:25:09 +02:00
if (notNullable === true) {
col.notNullable();
} else {
col.nullable();
2021-06-02 15:53:22 +02:00
}
2021-06-17 16:17:15 +02:00
return col;
};
2021-06-17 16:17:15 +02:00
/**
* Drops a column from a table
*/
2023-07-19 16:35:50 +02:00
const dropColumn = (tableBuilder: Knex.TableBuilder, column: Column) => {
if (!db.config.settings?.forceMigration) {
return;
}
return tableBuilder.dropColumn(column.name);
};
2021-06-17 16:17:15 +02:00
/**
* Creates a table in a database
*/
2023-07-19 16:35:50 +02:00
const createTable = async (schemaBuilder: Knex.SchemaBuilder, table: Table) => {
2022-08-08 23:33:39 +02:00
await schemaBuilder.createTable(table.name, (tableBuilder) => {
// columns
2022-08-08 23:33:39 +02:00
(table.columns || []).forEach((column) => createColumn(tableBuilder, column));
2021-06-17 16:17:15 +02:00
// indexes
2022-08-08 23:33:39 +02:00
(table.indexes || []).forEach((index) => createIndex(tableBuilder, index));
2021-06-17 16:17:15 +02:00
// foreign keys
2021-06-17 16:17:15 +02:00
2021-09-15 12:25:09 +02:00
if (!db.dialect.canAlterConstraints()) {
2022-08-08 23:33:39 +02:00
(table.foreignKeys || []).forEach((foreignKey) =>
createForeignKey(tableBuilder, foreignKey)
);
}
});
};
2021-06-17 16:17:15 +02:00
2023-09-12 21:47:42 +02:00
const alterTable = async (schemaBuilder: Knex.SchemaBuilder, table: TableDiff['diff']) => {
2022-08-08 23:33:39 +02:00
await schemaBuilder.alterTable(table.name, (tableBuilder) => {
// Delete indexes / fks / columns
2021-08-10 09:36:02 +02:00
// Drop foreign keys first to avoid foreign key errors in the following steps
for (const removedForeignKey of table.foreignKeys.removed) {
debug(`Dropping foreign key ${removedForeignKey.name} on ${table.name}`);
dropForeignKey(tableBuilder, removedForeignKey);
}
2021-08-10 09:36:02 +02:00
for (const updatedForeignKey of table.foreignKeys.updated) {
debug(`Dropping updated foreign key ${updatedForeignKey.name} on ${table.name}`);
2021-09-15 12:25:09 +02:00
dropForeignKey(tableBuilder, updatedForeignKey.object);
}
2021-08-10 09:36:02 +02:00
for (const removedColumn of table.columns.removed) {
debug(`Dropping column ${removedColumn.name} on ${table.name}`);
dropColumn(tableBuilder, removedColumn);
}
2021-08-10 09:36:02 +02:00
// for mysql only, dropForeignKey also removes the index, so don't drop it twice
const isMySQL = db.config.connection.client === 'mysql';
const ignoreForeignKeyNames = isMySQL
? [
...table.foreignKeys.removed.map((fk) => fk.name),
...table.foreignKeys.updated.map((fk) => fk.name),
]
: [];
for (const removedIndex of table.indexes.removed) {
if (!ignoreForeignKeyNames.includes(removedIndex.name)) {
debug(`Dropping index ${removedIndex.name} on ${table.name}`);
dropIndex(tableBuilder, removedIndex);
}
}
for (const updatedIndex of table.indexes.updated) {
if (!ignoreForeignKeyNames.includes(updatedIndex.name)) {
debug(`Dropping updated index ${updatedIndex.name} on ${table.name}`);
dropIndex(tableBuilder, updatedIndex.object);
}
}
// Update existing columns / foreign keys / indexes
for (const updatedColumn of table.columns.updated) {
debug(`Updating column ${updatedColumn.name} on ${table.name}`);
2021-08-10 09:36:02 +02:00
const { object } = updatedColumn;
2021-08-10 09:36:02 +02:00
if (object.type === 'increments') {
createColumn(tableBuilder, { ...object, type: 'integer' }).alter();
} else {
createColumn(tableBuilder, object).alter();
}
}
2021-08-10 09:36:02 +02:00
for (const updatedForeignKey of table.foreignKeys.updated) {
debug(`Recreating updated foreign key ${updatedForeignKey.name} on ${table.name}`);
2021-09-15 12:25:09 +02:00
createForeignKey(tableBuilder, updatedForeignKey.object);
}
2021-08-10 09:36:02 +02:00
for (const updatedIndex of table.indexes.updated) {
debug(`Recreating updated index ${updatedIndex.name} on ${table.name}`);
2021-09-15 12:25:09 +02:00
createIndex(tableBuilder, updatedIndex.object);
}
2021-08-10 09:36:02 +02:00
for (const addedColumn of table.columns.added) {
debug(`Creating column ${addedColumn.name} on ${table.name}`);
if (addedColumn.type === 'increments' && !db.dialect.canAddIncrements()) {
tableBuilder.integer(addedColumn.name).unsigned();
2023-09-12 21:47:42 +02:00
tableBuilder.primary([addedColumn.name]);
} else {
createColumn(tableBuilder, addedColumn);
}
}
2021-08-10 09:36:02 +02:00
for (const addedForeignKey of table.foreignKeys.added) {
debug(`Creating foreign keys ${addedForeignKey.name} on ${table.name}`);
createForeignKey(tableBuilder, addedForeignKey);
}
2021-08-10 09:36:02 +02:00
for (const addedIndex of table.indexes.added) {
debug(`Creating index ${addedIndex.name} on ${table.name}`);
createIndex(tableBuilder, addedIndex);
}
});
};
2021-08-10 09:36:02 +02:00
/**
* Drops a table from a database
*/
2023-09-12 21:47:42 +02:00
const dropTable = (schemaBuilder: Knex.SchemaBuilder, table: Table) => {
if (!db.config.settings.forceMigration) {
return;
}
return schemaBuilder.dropTableIfExists(table.name);
};
2021-06-17 16:17:15 +02:00
/**
* Creates a table foreign keys constraints
*/
2023-09-12 21:47:42 +02:00
const createTableForeignKeys = async (schemaBuilder: Knex.SchemaBuilder, table: Table) => {
// foreign keys
2022-08-08 23:33:39 +02:00
await schemaBuilder.table(table.name, (tableBuilder) => {
(table.foreignKeys || []).forEach((foreignKey) => createForeignKey(tableBuilder, foreignKey));
});
};
/**
* Drops a table foreign keys constraints
*/
2023-09-12 21:47:42 +02:00
const dropTableForeignKeys = async (schemaBuilder: Knex.SchemaBuilder, table: Table) => {
if (!db.config.settings.forceMigration) {
return;
}
// foreign keys
2022-08-08 23:33:39 +02:00
await schemaBuilder.table(table.name, (tableBuilder) => {
(table.foreignKeys || []).forEach((foreignKey) => dropForeignKey(tableBuilder, foreignKey));
});
};
return {
createTable,
alterTable,
dropTable,
createTableForeignKeys,
dropTableForeignKeys,
};
};