2021-06-02 15:53:22 +02:00
|
|
|
'use strict';
|
|
|
|
|
2021-08-10 09:36:02 +02:00
|
|
|
const { isUndefined } = require('lodash/fp');
|
2021-06-17 16:17:15 +02:00
|
|
|
const debug = require('debug')('@strapi/database');
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
module.exports = db => ({
|
|
|
|
/**
|
|
|
|
* Returns a knex schema builder instance
|
|
|
|
* @param {string} table - table name
|
|
|
|
*/
|
|
|
|
getSchemaBuilder(table, trx = db.connection) {
|
|
|
|
return table.schema ? trx.schema.withSchema(table.schema) : trx.schema;
|
|
|
|
},
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
/**
|
|
|
|
* Creates schema in DB
|
|
|
|
* @param {Schema} schema - database schema
|
|
|
|
*/
|
|
|
|
async createSchema(schema) {
|
|
|
|
// TODO: ensure database exists;
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
await db.connection.transaction(async trx => {
|
|
|
|
// create tables without FKs first do avoid ordering issues
|
|
|
|
await this.createTables(schema.tables, trx);
|
|
|
|
});
|
|
|
|
},
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
/**
|
|
|
|
* Creates a list of tables in a schema
|
|
|
|
* @param {KnexInstance} trx
|
|
|
|
* @param {Table[]} tables
|
|
|
|
*/
|
|
|
|
async createTables(tables, trx) {
|
|
|
|
for (const table of tables) {
|
|
|
|
debug(`Creating table: ${table.name}`);
|
|
|
|
const schemaBuilder = this.getSchemaBuilder(table, trx);
|
|
|
|
await createTable(schemaBuilder, table);
|
2021-06-02 15:53:22 +02:00
|
|
|
}
|
|
|
|
|
2021-06-17 16:17:15 +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(table, trx);
|
|
|
|
await createTableForeignKeys(schemaBuilder, table);
|
2021-06-02 15:53:22 +02:00
|
|
|
}
|
2021-06-17 16:17:15 +02:00
|
|
|
},
|
|
|
|
/**
|
|
|
|
* Drops schema from DB
|
|
|
|
* @param {Schema} schema - database schema
|
|
|
|
* @param {object} opts
|
|
|
|
* @param {boolean} opts.dropDatabase - weather to drop the entire database or simply drop the tables
|
|
|
|
*/
|
|
|
|
async dropSchema(schema, { dropDatabase = false } = {}) {
|
|
|
|
if (dropDatabase) {
|
|
|
|
// TODO: drop database & return as it will drop everything
|
|
|
|
return;
|
2021-06-02 15:53:22 +02:00
|
|
|
}
|
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
await db.connection.transaction(async trx => {
|
|
|
|
for (const table of schema.tables.reverse()) {
|
|
|
|
const schemaBuilder = this.getSchemaBuilder(table, trx);
|
|
|
|
await dropTable(schemaBuilder, table);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
/**
|
|
|
|
* Applies a schema diff update in the DB
|
|
|
|
* @param {*} schemaDiff
|
|
|
|
*/
|
|
|
|
// TODO: implement force option to disable removal in DB
|
|
|
|
async updateSchema(schemaDiff) {
|
|
|
|
await db.connection.transaction(async trx => {
|
|
|
|
await this.createTables(schemaDiff.tables.added, trx);
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-08-06 11:19:17 +02:00
|
|
|
// 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(table, trx);
|
|
|
|
await dropTableForeignKeys(schemaBuilder, table);
|
|
|
|
}
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
for (const table of schemaDiff.tables.removed) {
|
|
|
|
debug(`Removing table: ${table.name}`);
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
const schemaBuilder = this.getSchemaBuilder(table, trx);
|
|
|
|
await dropTable(schemaBuilder, table);
|
2021-06-02 15:53:22 +02:00
|
|
|
}
|
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
for (const table of schemaDiff.tables.updated) {
|
|
|
|
debug(`Updating table: ${table.name}`);
|
|
|
|
// alter table
|
|
|
|
const schemaBuilder = this.getSchemaBuilder(table, trx);
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-08-10 09:36:02 +02:00
|
|
|
await alterTable(schemaBuilder, table);
|
2021-06-17 16:17:15 +02:00
|
|
|
}
|
2021-06-02 15:53:22 +02:00
|
|
|
});
|
2021-06-17 16:17:15 +02:00
|
|
|
},
|
|
|
|
});
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
/**
|
|
|
|
* Creates a foreign key on a table
|
|
|
|
* @param {Knex.TableBuilder} tableBuilder
|
|
|
|
* @param {ForeignKey} foreignKey
|
|
|
|
*/
|
|
|
|
const createForeignKey = (tableBuilder, foreignKey) => {
|
|
|
|
const { name, columns, referencedColumns, referencedTable, onDelete, onUpdate } = foreignKey;
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
const constraint = tableBuilder
|
|
|
|
.foreign(columns, name)
|
|
|
|
.references(referencedColumns)
|
|
|
|
.inTable(referencedTable);
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
if (onDelete) {
|
|
|
|
constraint.onDelete(onDelete);
|
|
|
|
}
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +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
|
|
|
|
* @param {Knex.TableBuilder} tableBuilder
|
|
|
|
* @param {ForeignKey} foreignKey
|
|
|
|
*/
|
|
|
|
const dropForeignKey = (tableBuilder, foreignKey) => {
|
|
|
|
const { name, columns } = foreignKey;
|
|
|
|
|
|
|
|
tableBuilder.dropForeign(columns, name);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates an index on a table
|
|
|
|
* @param {Knex.TableBuilder} tableBuilder
|
|
|
|
* @param {Index} index
|
|
|
|
*/
|
|
|
|
const createIndex = (tableBuilder, index) => {
|
|
|
|
const { type, columns, name } = index;
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 'primary': {
|
|
|
|
return tableBuilder.primary(columns, name);
|
|
|
|
}
|
|
|
|
case 'unique': {
|
|
|
|
return tableBuilder.unique(columns, name);
|
2021-06-02 15:53:22 +02:00
|
|
|
}
|
2021-06-17 16:17:15 +02:00
|
|
|
default: {
|
|
|
|
return tableBuilder.index(columns, name, type);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
/**
|
|
|
|
* Drops an index from table
|
|
|
|
* @param {Knex.TableBuilder} tableBuilder
|
|
|
|
* @param {Index} index
|
|
|
|
*/
|
|
|
|
const dropIndex = (tableBuilder, index) => {
|
|
|
|
const { type, columns, name } = index;
|
2021-06-02 15:53:22 +02:00
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
switch (type) {
|
|
|
|
case 'primary': {
|
|
|
|
return tableBuilder.dropPrimary(name);
|
|
|
|
}
|
|
|
|
case 'unique': {
|
|
|
|
return tableBuilder.dropUnique(columns, name);
|
|
|
|
}
|
|
|
|
default: {
|
|
|
|
return tableBuilder.dropIndex(columns, name);
|
2021-06-02 15:53:22 +02:00
|
|
|
}
|
2021-06-17 16:17:15 +02:00
|
|
|
}
|
2021-06-02 15:53:22 +02:00
|
|
|
};
|
2021-06-17 16:17:15 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a column in a table
|
|
|
|
* @param {Knex.TableBuilder} tableBuilder
|
|
|
|
* @param {Column} column
|
|
|
|
*/
|
|
|
|
const createColumn = (tableBuilder, column) => {
|
|
|
|
const { type, name, args = [], defaultTo, unsigned, notNullable } = column;
|
|
|
|
|
|
|
|
const col = tableBuilder[type](name, ...args);
|
|
|
|
|
|
|
|
if (unsigned) {
|
|
|
|
col.unsigned();
|
|
|
|
}
|
|
|
|
|
2021-08-10 09:36:02 +02:00
|
|
|
if (!isUndefined(defaultTo)) {
|
2021-06-17 16:17:15 +02:00
|
|
|
// TODO: allow some raw default values
|
|
|
|
col.defaultTo(...[].concat(defaultTo));
|
|
|
|
}
|
|
|
|
|
|
|
|
if (notNullable) {
|
|
|
|
col.notNullable();
|
|
|
|
} else {
|
|
|
|
col.nullable();
|
|
|
|
}
|
|
|
|
|
|
|
|
return col;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Drops a column from a table
|
|
|
|
* @param {Knex.TableBuilder} tableBuilder
|
|
|
|
* @param {Column} column
|
|
|
|
*/
|
|
|
|
const dropColumn = (tableBuilder, column) => {
|
|
|
|
tableBuilder.dropColumn(column.name);
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Creates a table in a database
|
|
|
|
* @param {SchemaBuilder} schemaBuilder
|
|
|
|
* @param {Table} table
|
|
|
|
*/
|
|
|
|
const createTable = async (schemaBuilder, table) => {
|
|
|
|
if (await schemaBuilder.hasTable(table.name)) {
|
2021-08-10 09:36:02 +02:00
|
|
|
debug(`Table ${table.name} already exists trying to alter it`);
|
2021-08-06 11:19:17 +02:00
|
|
|
|
2021-08-10 09:36:02 +02:00
|
|
|
// TODO: implement a DB sync at some point
|
|
|
|
return;
|
2021-06-17 16:17:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
await schemaBuilder.createTable(table.name, tableBuilder => {
|
|
|
|
// columns
|
|
|
|
(table.columns || []).forEach(column => createColumn(tableBuilder, column));
|
|
|
|
|
|
|
|
// indexes
|
|
|
|
(table.indexes || []).forEach(index => createIndex(tableBuilder, index));
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-08-10 09:36:02 +02:00
|
|
|
const alterTable = async (schemaBuilder, table) => {
|
|
|
|
await schemaBuilder.alterTable(table.name, tableBuilder => {
|
|
|
|
// Delete indexes / fks / columns
|
|
|
|
|
|
|
|
for (const removedIndex of table.indexes.removed) {
|
|
|
|
debug(`Dropping index ${removedIndex.name}`);
|
|
|
|
dropIndex(tableBuilder, removedIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const updateddIndex of table.indexes.updated) {
|
|
|
|
debug(`Dropping updated index ${updateddIndex.name}`);
|
|
|
|
dropIndex(tableBuilder, updateddIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const removedForeignKey of table.foreignKeys.removed) {
|
|
|
|
debug(`Dropping foreign key ${removedForeignKey.name}`);
|
|
|
|
dropForeignKey(tableBuilder, removedForeignKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const updatedForeignKey of table.foreignKeys.updated) {
|
|
|
|
debug(`Dropping updated foreign key ${updatedForeignKey.name}`);
|
|
|
|
dropForeignKey(tableBuilder, updatedForeignKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const removedColumn of table.columns.removed) {
|
|
|
|
debug(`Dropping column ${removedColumn.name}`);
|
|
|
|
dropColumn(tableBuilder, removedColumn);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update existing columns / foreign keys / indexes
|
|
|
|
|
|
|
|
for (const updatedColumn of table.columns.updated) {
|
|
|
|
debug(`Updating column ${updatedColumn.name}`);
|
|
|
|
|
|
|
|
// TODO: cleanup diffs for columns
|
|
|
|
const { object } = updatedColumn;
|
|
|
|
|
|
|
|
/*
|
|
|
|
type -> recreate the type
|
|
|
|
args -> recreate the type
|
|
|
|
unsigned
|
|
|
|
if changed then recreate the type
|
|
|
|
if removed then check if old value was true -> recreate the type else do nothing
|
|
|
|
defaultTo
|
|
|
|
reapply the default to previous data
|
|
|
|
notNullable
|
|
|
|
if null to not null we need a default value to migrate the data
|
|
|
|
*/
|
|
|
|
|
|
|
|
createColumn(tableBuilder, object).alter();
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const updatedForeignKey of table.foreignKeys.updated) {
|
|
|
|
debug(`Recreating updated foreign key ${updatedForeignKey.name}`);
|
|
|
|
createForeignKey(tableBuilder, updatedForeignKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const updatedIndex of table.indexes.updated) {
|
|
|
|
debug(`Recreating updated index ${updatedIndex.name}`);
|
|
|
|
createIndex(tableBuilder, updatedIndex);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const addedColumn of table.columns.added) {
|
|
|
|
debug(`Creating column ${addedColumn.name}`);
|
|
|
|
createColumn(tableBuilder, addedColumn);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const addedForeignKey of table.foreignKeys.added) {
|
|
|
|
debug(`Creating foreign keys ${addedForeignKey.name}`);
|
|
|
|
createForeignKey(tableBuilder, addedForeignKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const addedIndex of table.indexes.added) {
|
|
|
|
debug(`Creating index ${addedIndex.name}`);
|
|
|
|
createIndex(tableBuilder, addedIndex);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2021-08-06 11:19:17 +02:00
|
|
|
/**
|
|
|
|
* Drops a table from a database
|
|
|
|
* @param {Knex.SchemaBuilder} schemaBuilder
|
|
|
|
* @param {Table} table
|
|
|
|
*/
|
|
|
|
const dropTable = (schemaBuilder, table) => schemaBuilder.dropTableIfExists(table.name);
|
|
|
|
|
2021-06-17 16:17:15 +02:00
|
|
|
/**
|
|
|
|
* Creates a table foreign keys constraints
|
|
|
|
* @param {SchemaBuilder} schemaBuilder
|
|
|
|
* @param {Table} table
|
|
|
|
*/
|
|
|
|
const createTableForeignKeys = async (schemaBuilder, table) => {
|
|
|
|
// foreign keys
|
|
|
|
await schemaBuilder.table(table.name, tableBuilder => {
|
|
|
|
(table.foreignKeys || []).forEach(foreignKey => createForeignKey(tableBuilder, foreignKey));
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
2021-08-06 11:19:17 +02:00
|
|
|
* Drops a table foreign keys constraints
|
|
|
|
* @param {SchemaBuilder} schemaBuilder
|
2021-06-17 16:17:15 +02:00
|
|
|
* @param {Table} table
|
|
|
|
*/
|
2021-08-06 11:19:17 +02:00
|
|
|
const dropTableForeignKeys = async (schemaBuilder, table) => {
|
|
|
|
// foreign keys
|
|
|
|
await schemaBuilder.table(table.name, tableBuilder => {
|
|
|
|
(table.foreignKeys || []).forEach(foreignKey => dropForeignKey(tableBuilder, foreignKey));
|
|
|
|
});
|
|
|
|
};
|