node_modules ignore

This commit is contained in:
2025-05-08 23:43:47 +02:00
parent e19d52f172
commit 4574544c9f
65041 changed files with 10593536 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import type { Knex } from 'knex';
import type { Database } from '..';
import type { Schema, Table, SchemaDiff } from './types';
declare const _default: (db: Database) => {
/**
* Returns a knex schema builder instance
* @param {string} table - table name
*/
getSchemaBuilder(trx: Knex.Transaction): Knex.SchemaBuilder;
/**
* Creates schema in DB
*/
createSchema(schema: Schema): Promise<void>;
/**
* Creates a list of tables in a schema
* @param {KnexInstance} trx
* @param {Table[]} tables
*/
createTables(tables: Table[], trx: Knex.Transaction): Promise<void>;
/**
* Drops schema from DB
*/
dropSchema(schema: Schema, { dropDatabase }?: {
dropDatabase?: boolean | undefined;
}): Promise<void>;
/**
* Applies a schema diff update in the DB
* @param {*} schemaDiff
*/
updateSchema(schemaDiff: SchemaDiff['diff']): Promise<void>;
};
export default _default;
//# sourceMappingURL=builder.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"builder.d.ts","sourceRoot":"","sources":["../../src/schema/builder.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AACjC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAwC,MAAM,SAAS,CAAC;6BAI3E,QAAQ;IAIxB;;;OAGG;0BACmB,KAAK,WAAW;IAItC;;OAEG;yBACwB,MAAM;IAMjC;;;;OAIG;yBACwB,KAAK,EAAE,OAAO,KAAK,WAAW;IAczD;;OAEG;uBACsB,MAAM;;;IAc/B;;;OAGG;6BAE4B,UAAU,CAAC,MAAM,CAAC;;AA9DrD,wBA6GE"}

View File

@@ -0,0 +1,354 @@
'use strict';
var _ = require('lodash/fp');
var createDebug = require('debug');
const debug = createDebug('strapi::database');
var createSchemaBuilder = ((db)=>{
const helpers = createHelpers(db);
return {
/**
* Returns a knex schema builder instance
* @param {string} table - table name
*/ getSchemaBuilder (trx) {
return db.getSchemaConnection(trx);
},
/**
* Creates schema in DB
*/ async createSchema (schema) {
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
*/ async createTables (tables, trx) {
for (const table of tables){
debug(`Creating table: ${table.name}`);
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.createTable(schemaBuilder, table);
}
// 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
*/ async dropSchema (schema, { dropDatabase = false } = {}) {
if (dropDatabase) {
// TODO: drop database & return as it will drop everything
return;
}
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
async updateSchema (schemaDiff) {
const forceMigration = db.config.settings?.forceMigration;
await db.dialect.startSchemaUpdate();
// Pre-fetch metadata for all updated tables
const existingMetadata = {};
for (const table of schemaDiff.tables.updated){
existingMetadata[table.name] = {
indexes: await db.dialect.schemaInspector.getIndexes(table.name),
foreignKeys: await db.dialect.schemaInspector.getForeignKeys(table.name)
};
}
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);
const { indexes, foreignKeys } = existingMetadata[table.name];
await helpers.alterTable(schemaBuilder, table, {
indexes,
foreignKeys
});
}
});
await db.dialect.endSchemaUpdate();
}
};
});
const createHelpers = (db)=>{
/**
* Creates a foreign key on a table
*/ const createForeignKey = (tableBuilder, foreignKey)=>{
const { name, columns, referencedColumns, referencedTable, onDelete, onUpdate } = foreignKey;
const constraint = tableBuilder.foreign(columns, name).references(referencedColumns).inTable(db.getSchemaName() ? `${db.getSchemaName()}.${referencedTable}` : referencedTable);
if (onDelete) {
constraint.onDelete(onDelete);
}
if (onUpdate) {
constraint.onUpdate(onUpdate);
}
};
/**
* Drops a foreign key from a table
*/ const dropForeignKey = (tableBuilder, foreignKey, existingForeignKeys)=>{
const { name, columns } = foreignKey;
// Check if the index exists in existingIndexes, and return early if it doesn't
if (existingForeignKeys && !existingForeignKeys.some((existingIndex)=>existingIndex?.name === name)) {
debug(`Foreign Key ${name} not found in existing foreign keys. Skipping drop.`);
return;
}
tableBuilder.dropForeign(columns, name);
};
/**
* Creates an index on a table
*/ const createIndex = (tableBuilder, index)=>{
const { type, columns, name } = index;
switch(type){
case 'primary':
{
return tableBuilder.primary(columns, {
constraintName: name
});
}
case 'unique':
{
return tableBuilder.unique(columns, {
indexName: name
});
}
default:
{
return tableBuilder.index(columns, name, type);
}
}
};
/**
* Drops an index from table
* @param {Knex.TableBuilder} tableBuilder
* @param {Index} index
*/ const dropIndex = (tableBuilder, index, existingIndexes)=>{
if (!db.config.settings?.forceMigration) {
return;
}
const { type, columns, name } = index;
// Check if the index exists in existingIndexes, and return early if it doesn't
if (existingIndexes && !existingIndexes.some((existingIndex)=>existingIndex?.name === name)) {
debug(`Index ${index.name} not found in existingIndexes. Skipping drop.`);
return;
}
switch(type){
case 'primary':
{
return tableBuilder.dropPrimary(name);
}
case 'unique':
{
return tableBuilder.dropUnique(columns, name);
}
default:
{
return tableBuilder.dropIndex(columns, name);
}
}
};
/**
* Creates a column in a table
*/ const createColumn = (tableBuilder, column)=>{
const { type, name, args = [], defaultTo, unsigned, notNullable } = column;
const col = tableBuilder[type](name, ...args);
if (unsigned === true) {
col.unsigned();
}
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);
}
}
if (notNullable === true) {
col.notNullable();
} else {
col.nullable();
}
return col;
};
/**
* Drops a column from a table
*/ const dropColumn = (tableBuilder, column)=>{
if (!db.config.settings?.forceMigration) {
return;
}
return tableBuilder.dropColumn(column.name);
};
/**
* Creates a table in a database
*/ const createTable = async (schemaBuilder, table)=>{
await schemaBuilder.createTable(table.name, (tableBuilder)=>{
// columns
(table.columns || []).forEach((column)=>createColumn(tableBuilder, column));
// indexes
(table.indexes || []).forEach((index)=>createIndex(tableBuilder, index));
// foreign keys
if (!db.dialect.canAlterConstraints()) {
(table.foreignKeys || []).forEach((foreignKey)=>createForeignKey(tableBuilder, foreignKey));
}
});
};
/**
* Alters a database table by applying a set of schema changes including updates to columns, indexes, and foreign keys.
* This function ensures proper ordering of operations to avoid conflicts (e.g., foreign key errors) and handles
* MySQL-specific quirks where dropping a foreign key can implicitly drop an associated index.
*
* @param {Knex.SchemaBuilder} schemaBuilder - Knex SchemaBuilder instance to perform schema operations.
* @param {TableDiff['diff']} table - A diff object representing the schema changes to be applied to the table.
* @param {{ indexes: Index[]; foreignKeys: ForeignKey[] }} existingMetadata - Metadata about existing indexes and
* foreign keys in the table. Used to ensure safe operations and avoid unnecessary modifications.
* - indexes: Array of existing index definitions.
* - foreignKeys: Array of existing foreign key definitions.
*/ const alterTable = async (schemaBuilder, table, existingMetadata = {
indexes: [],
foreignKeys: []
})=>{
let existingIndexes = [
...existingMetadata.indexes
];
const existingForeignKeys = [
...existingMetadata.foreignKeys
];
// Track dropped foreign keys
const droppedForeignKeyNames = [];
await schemaBuilder.alterTable(table.name, async (tableBuilder)=>{
// 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, existingForeignKeys);
droppedForeignKeyNames.push(removedForeignKey.name);
}
for (const updatedForeignKey of table.foreignKeys.updated){
debug(`Dropping updated foreign key ${updatedForeignKey.name} on ${table.name}`);
dropForeignKey(tableBuilder, updatedForeignKey.object, existingForeignKeys);
droppedForeignKeyNames.push(updatedForeignKey.object.name);
}
// In MySQL, dropping a foreign key can also implicitly drop an index with the same name
// Remove dropped foreign keys from existingIndexes for MySQL
if (db.config.connection.client === 'mysql') {
existingIndexes = existingIndexes.filter((index)=>!droppedForeignKeyNames.includes(index.name));
}
for (const removedIndex of table.indexes.removed){
debug(`Dropping index ${removedIndex.name} on ${table.name}`);
dropIndex(tableBuilder, removedIndex, existingIndexes);
}
for (const updatedIndex of table.indexes.updated){
debug(`Dropping updated index ${updatedIndex.name} on ${table.name}`);
dropIndex(tableBuilder, updatedIndex.object, existingIndexes);
}
// Drop columns after FKs have been removed to avoid FK errors
for (const removedColumn of table.columns.removed){
debug(`Dropping column ${removedColumn.name} on ${table.name}`);
dropColumn(tableBuilder, removedColumn);
}
// Update existing columns
for (const updatedColumn of table.columns.updated){
debug(`Updating column ${updatedColumn.name} on ${table.name}`);
const { object } = updatedColumn;
if (object.type === 'increments') {
createColumn(tableBuilder, {
...object,
type: 'integer'
}).alter();
} else {
createColumn(tableBuilder, object).alter();
}
}
// Add any new columns
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();
tableBuilder.primary([
addedColumn.name
]);
} else {
createColumn(tableBuilder, addedColumn);
}
}
// once the columns have all been updated, we can create indexes again
for (const updatedForeignKey of table.foreignKeys.updated){
debug(`Recreating updated foreign key ${updatedForeignKey.name} on ${table.name}`);
createForeignKey(tableBuilder, updatedForeignKey.object);
}
for (const updatedIndex of table.indexes.updated){
debug(`Recreating updated index ${updatedIndex.name} on ${table.name}`);
createIndex(tableBuilder, updatedIndex.object);
}
for (const addedForeignKey of table.foreignKeys.added){
debug(`Creating foreign key ${addedForeignKey.name} on ${table.name}`);
createForeignKey(tableBuilder, addedForeignKey);
}
for (const addedIndex of table.indexes.added){
debug(`Creating index ${addedIndex.name} on ${table.name}`);
createIndex(tableBuilder, addedIndex);
}
});
};
/**
* Drops a table from a database
*/ const dropTable = (schemaBuilder, table)=>{
if (!db.config.settings.forceMigration) {
return;
}
return schemaBuilder.dropTableIfExists(table.name);
};
/**
* Creates a table foreign keys constraints
*/ const createTableForeignKeys = async (schemaBuilder, table)=>{
// foreign keys
await schemaBuilder.table(table.name, (tableBuilder)=>{
(table.foreignKeys || []).forEach((foreignKey)=>createForeignKey(tableBuilder, foreignKey));
});
};
/**
* Drops a table foreign keys constraints
*/ const dropTableForeignKeys = async (schemaBuilder, table)=>{
if (!db.config.settings.forceMigration) {
return;
}
// foreign keys
await schemaBuilder.table(table.name, (tableBuilder)=>{
(table.foreignKeys || []).forEach((foreignKey)=>dropForeignKey(tableBuilder, foreignKey));
});
};
return {
createTable,
alterTable,
dropTable,
createTableForeignKeys,
dropTableForeignKeys
};
};
module.exports = createSchemaBuilder;
//# sourceMappingURL=builder.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,352 @@
import { isNil, castArray, prop, omit } from 'lodash/fp';
import createDebug from 'debug';
const debug = createDebug('strapi::database');
var createSchemaBuilder = ((db)=>{
const helpers = createHelpers(db);
return {
/**
* Returns a knex schema builder instance
* @param {string} table - table name
*/ getSchemaBuilder (trx) {
return db.getSchemaConnection(trx);
},
/**
* Creates schema in DB
*/ async createSchema (schema) {
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
*/ async createTables (tables, trx) {
for (const table of tables){
debug(`Creating table: ${table.name}`);
const schemaBuilder = this.getSchemaBuilder(trx);
await helpers.createTable(schemaBuilder, table);
}
// 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
*/ async dropSchema (schema, { dropDatabase = false } = {}) {
if (dropDatabase) {
// TODO: drop database & return as it will drop everything
return;
}
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
async updateSchema (schemaDiff) {
const forceMigration = db.config.settings?.forceMigration;
await db.dialect.startSchemaUpdate();
// Pre-fetch metadata for all updated tables
const existingMetadata = {};
for (const table of schemaDiff.tables.updated){
existingMetadata[table.name] = {
indexes: await db.dialect.schemaInspector.getIndexes(table.name),
foreignKeys: await db.dialect.schemaInspector.getForeignKeys(table.name)
};
}
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);
const { indexes, foreignKeys } = existingMetadata[table.name];
await helpers.alterTable(schemaBuilder, table, {
indexes,
foreignKeys
});
}
});
await db.dialect.endSchemaUpdate();
}
};
});
const createHelpers = (db)=>{
/**
* Creates a foreign key on a table
*/ const createForeignKey = (tableBuilder, foreignKey)=>{
const { name, columns, referencedColumns, referencedTable, onDelete, onUpdate } = foreignKey;
const constraint = tableBuilder.foreign(columns, name).references(referencedColumns).inTable(db.getSchemaName() ? `${db.getSchemaName()}.${referencedTable}` : referencedTable);
if (onDelete) {
constraint.onDelete(onDelete);
}
if (onUpdate) {
constraint.onUpdate(onUpdate);
}
};
/**
* Drops a foreign key from a table
*/ const dropForeignKey = (tableBuilder, foreignKey, existingForeignKeys)=>{
const { name, columns } = foreignKey;
// Check if the index exists in existingIndexes, and return early if it doesn't
if (existingForeignKeys && !existingForeignKeys.some((existingIndex)=>existingIndex?.name === name)) {
debug(`Foreign Key ${name} not found in existing foreign keys. Skipping drop.`);
return;
}
tableBuilder.dropForeign(columns, name);
};
/**
* Creates an index on a table
*/ const createIndex = (tableBuilder, index)=>{
const { type, columns, name } = index;
switch(type){
case 'primary':
{
return tableBuilder.primary(columns, {
constraintName: name
});
}
case 'unique':
{
return tableBuilder.unique(columns, {
indexName: name
});
}
default:
{
return tableBuilder.index(columns, name, type);
}
}
};
/**
* Drops an index from table
* @param {Knex.TableBuilder} tableBuilder
* @param {Index} index
*/ const dropIndex = (tableBuilder, index, existingIndexes)=>{
if (!db.config.settings?.forceMigration) {
return;
}
const { type, columns, name } = index;
// Check if the index exists in existingIndexes, and return early if it doesn't
if (existingIndexes && !existingIndexes.some((existingIndex)=>existingIndex?.name === name)) {
debug(`Index ${index.name} not found in existingIndexes. Skipping drop.`);
return;
}
switch(type){
case 'primary':
{
return tableBuilder.dropPrimary(name);
}
case 'unique':
{
return tableBuilder.dropUnique(columns, name);
}
default:
{
return tableBuilder.dropIndex(columns, name);
}
}
};
/**
* Creates a column in a table
*/ const createColumn = (tableBuilder, column)=>{
const { type, name, args = [], defaultTo, unsigned, notNullable } = column;
const col = tableBuilder[type](name, ...args);
if (unsigned === true) {
col.unsigned();
}
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);
}
}
if (notNullable === true) {
col.notNullable();
} else {
col.nullable();
}
return col;
};
/**
* Drops a column from a table
*/ const dropColumn = (tableBuilder, column)=>{
if (!db.config.settings?.forceMigration) {
return;
}
return tableBuilder.dropColumn(column.name);
};
/**
* Creates a table in a database
*/ const createTable = async (schemaBuilder, table)=>{
await schemaBuilder.createTable(table.name, (tableBuilder)=>{
// columns
(table.columns || []).forEach((column)=>createColumn(tableBuilder, column));
// indexes
(table.indexes || []).forEach((index)=>createIndex(tableBuilder, index));
// foreign keys
if (!db.dialect.canAlterConstraints()) {
(table.foreignKeys || []).forEach((foreignKey)=>createForeignKey(tableBuilder, foreignKey));
}
});
};
/**
* Alters a database table by applying a set of schema changes including updates to columns, indexes, and foreign keys.
* This function ensures proper ordering of operations to avoid conflicts (e.g., foreign key errors) and handles
* MySQL-specific quirks where dropping a foreign key can implicitly drop an associated index.
*
* @param {Knex.SchemaBuilder} schemaBuilder - Knex SchemaBuilder instance to perform schema operations.
* @param {TableDiff['diff']} table - A diff object representing the schema changes to be applied to the table.
* @param {{ indexes: Index[]; foreignKeys: ForeignKey[] }} existingMetadata - Metadata about existing indexes and
* foreign keys in the table. Used to ensure safe operations and avoid unnecessary modifications.
* - indexes: Array of existing index definitions.
* - foreignKeys: Array of existing foreign key definitions.
*/ const alterTable = async (schemaBuilder, table, existingMetadata = {
indexes: [],
foreignKeys: []
})=>{
let existingIndexes = [
...existingMetadata.indexes
];
const existingForeignKeys = [
...existingMetadata.foreignKeys
];
// Track dropped foreign keys
const droppedForeignKeyNames = [];
await schemaBuilder.alterTable(table.name, async (tableBuilder)=>{
// 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, existingForeignKeys);
droppedForeignKeyNames.push(removedForeignKey.name);
}
for (const updatedForeignKey of table.foreignKeys.updated){
debug(`Dropping updated foreign key ${updatedForeignKey.name} on ${table.name}`);
dropForeignKey(tableBuilder, updatedForeignKey.object, existingForeignKeys);
droppedForeignKeyNames.push(updatedForeignKey.object.name);
}
// In MySQL, dropping a foreign key can also implicitly drop an index with the same name
// Remove dropped foreign keys from existingIndexes for MySQL
if (db.config.connection.client === 'mysql') {
existingIndexes = existingIndexes.filter((index)=>!droppedForeignKeyNames.includes(index.name));
}
for (const removedIndex of table.indexes.removed){
debug(`Dropping index ${removedIndex.name} on ${table.name}`);
dropIndex(tableBuilder, removedIndex, existingIndexes);
}
for (const updatedIndex of table.indexes.updated){
debug(`Dropping updated index ${updatedIndex.name} on ${table.name}`);
dropIndex(tableBuilder, updatedIndex.object, existingIndexes);
}
// Drop columns after FKs have been removed to avoid FK errors
for (const removedColumn of table.columns.removed){
debug(`Dropping column ${removedColumn.name} on ${table.name}`);
dropColumn(tableBuilder, removedColumn);
}
// Update existing columns
for (const updatedColumn of table.columns.updated){
debug(`Updating column ${updatedColumn.name} on ${table.name}`);
const { object } = updatedColumn;
if (object.type === 'increments') {
createColumn(tableBuilder, {
...object,
type: 'integer'
}).alter();
} else {
createColumn(tableBuilder, object).alter();
}
}
// Add any new columns
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();
tableBuilder.primary([
addedColumn.name
]);
} else {
createColumn(tableBuilder, addedColumn);
}
}
// once the columns have all been updated, we can create indexes again
for (const updatedForeignKey of table.foreignKeys.updated){
debug(`Recreating updated foreign key ${updatedForeignKey.name} on ${table.name}`);
createForeignKey(tableBuilder, updatedForeignKey.object);
}
for (const updatedIndex of table.indexes.updated){
debug(`Recreating updated index ${updatedIndex.name} on ${table.name}`);
createIndex(tableBuilder, updatedIndex.object);
}
for (const addedForeignKey of table.foreignKeys.added){
debug(`Creating foreign key ${addedForeignKey.name} on ${table.name}`);
createForeignKey(tableBuilder, addedForeignKey);
}
for (const addedIndex of table.indexes.added){
debug(`Creating index ${addedIndex.name} on ${table.name}`);
createIndex(tableBuilder, addedIndex);
}
});
};
/**
* Drops a table from a database
*/ const dropTable = (schemaBuilder, table)=>{
if (!db.config.settings.forceMigration) {
return;
}
return schemaBuilder.dropTableIfExists(table.name);
};
/**
* Creates a table foreign keys constraints
*/ const createTableForeignKeys = async (schemaBuilder, table)=>{
// foreign keys
await schemaBuilder.table(table.name, (tableBuilder)=>{
(table.foreignKeys || []).forEach((foreignKey)=>createForeignKey(tableBuilder, foreignKey));
});
};
/**
* Drops a table foreign keys constraints
*/ const dropTableForeignKeys = async (schemaBuilder, table)=>{
if (!db.config.settings.forceMigration) {
return;
}
// foreign keys
await schemaBuilder.table(table.name, (tableBuilder)=>{
(table.foreignKeys || []).forEach((foreignKey)=>dropForeignKey(tableBuilder, foreignKey));
});
};
return {
createTable,
alterTable,
dropTable,
createTableForeignKeys,
dropTableForeignKeys
};
};
export { createSchemaBuilder as default };
//# sourceMappingURL=builder.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
import type { Schema, SchemaDiff } from './types';
import type { Database } from '..';
type SchemaDiffContext = {
previousSchema?: Schema;
databaseSchema: Schema;
userSchema: Schema;
};
declare const _default: (db: Database) => {
diff: (schemaDiffCtx: SchemaDiffContext) => Promise<SchemaDiff>;
};
export default _default;
//# sourceMappingURL=diff.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/schema/diff.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,MAAM,EAEN,UAAU,EAWX,MAAM,SAAS,CAAC;AACjB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAanC,KAAK,iBAAiB,GAAG;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;6BA8CkB,QAAQ;0BAoTgB,iBAAiB,KAAG,QAAQ,UAAU,CAAC;;AApTnF,wBA0ZE"}

View File

@@ -0,0 +1,379 @@
'use strict';
var _ = require('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
};
});
module.exports = createSchemaDiff;
//# sourceMappingURL=diff.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,377 @@
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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
import createSchemaBuilder from './builder';
import createSchemaDiff from './diff';
import createSchemaStorage from './storage';
import type { Schema, SchemaDiff } from './types';
import type { Database } from '..';
export type * from './types';
export interface SchemaProvider {
builder: ReturnType<typeof createSchemaBuilder>;
schemaDiff: ReturnType<typeof createSchemaDiff>;
schemaStorage: ReturnType<typeof createSchemaStorage>;
sync(): Promise<SchemaDiff['status']>;
syncSchema(): Promise<SchemaDiff['status']>;
reset(): Promise<void>;
create(): Promise<void>;
drop(): Promise<void>;
schema: Schema;
}
export declare const createSchemaProvider: (db: Database) => SchemaProvider;
//# sourceMappingURL=index.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/schema/index.ts"],"names":[],"mappings":"AAEA,OAAO,mBAAmB,MAAM,WAAW,CAAC;AAC5C,OAAO,gBAAgB,MAAM,QAAQ,CAAC;AACtC,OAAO,mBAAmB,MAAM,WAAW,CAAC;AAG5C,OAAO,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAEnC,mBAAmB,SAAS,CAAC;AAI7B,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;IAChD,UAAU,EAAE,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;IAChD,aAAa,EAAE,UAAU,CAAC,OAAO,mBAAmB,CAAC,CAAC;IACtD,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IACtC,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACxB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,eAAO,MAAM,oBAAoB,OAAQ,QAAQ,KAAG,cA0GnD,CAAC"}

View File

@@ -0,0 +1,93 @@
'use strict';
var createDebug = require('debug');
var builder = require('./builder.js');
var diff = require('./diff.js');
var storage = require('./storage.js');
var schema = require('./schema.js');
const debug = createDebug('strapi::database');
const createSchemaProvider = (db)=>{
const state = {};
return {
get schema () {
if (!state.schema) {
debug('Converting metadata to database schema');
state.schema = schema.metadataToSchema(db.metadata);
}
return state.schema;
},
builder: builder(db),
schemaDiff: diff(db),
schemaStorage: storage(db),
/**
* Drops the database schema
*/ async drop () {
debug('Dropping database schema');
const DBSchema = await db.dialect.schemaInspector.getSchema();
await this.builder.dropSchema(DBSchema);
},
/**
* Creates the database schema
*/ async create () {
debug('Created database schema');
await this.builder.createSchema(this.schema);
},
/**
* Resets the database schema
*/ async reset () {
debug('Resetting database schema');
await this.drop();
await this.create();
},
async syncSchema () {
debug('Synchronizing database schema');
const databaseSchema = await db.dialect.schemaInspector.getSchema();
const storedSchema = await this.schemaStorage.read();
/*
3way diff - DB schema / previous metadataSchema / new metadataSchema
- When something doesn't exist in the previous metadataSchema -> It's not tracked by us and should be ignored
- If no previous metadataSchema => use new metadataSchema so we start tracking them and ignore everything else
- Apply this logic to Tables / Columns / Indexes / FKs ...
- Handle errors (indexes or fks on incompatible stuff ...)
*/ const { status, diff } = await this.schemaDiff.diff({
previousSchema: storedSchema?.schema,
databaseSchema,
userSchema: this.schema
});
if (status === 'CHANGED') {
await this.builder.updateSchema(diff);
}
await this.schemaStorage.add(this.schema);
return status;
},
// TODO: support options to migrate softly or forcefully
// TODO: support option to disable auto migration & run a CLI command instead to avoid doing it at startup
// TODO: Allow keeping extra indexes / extra tables / extra columns (globally or on a per table basis)
async sync () {
if (await db.migrations.shouldRun()) {
debug('Found migrations to run');
await db.migrations.up();
return this.syncSchema();
}
const oldSchema = await this.schemaStorage.read();
if (!oldSchema) {
debug('Schema not persisted yet');
return this.syncSchema();
}
const { hash: oldHash } = oldSchema;
const hash = await this.schemaStorage.hashSchema(this.schema);
if (oldHash !== hash) {
debug('Schema changed');
return this.syncSchema();
}
debug('Schema unchanged');
return 'UNCHANGED';
}
};
};
exports.createSchemaProvider = createSchemaProvider;
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,91 @@
import createDebug from 'debug';
import createSchemaBuilder from './builder.mjs';
import createSchemaDiff from './diff.mjs';
import createSchemaStorage from './storage.mjs';
import { metadataToSchema } from './schema.mjs';
const debug = createDebug('strapi::database');
const createSchemaProvider = (db)=>{
const state = {};
return {
get schema () {
if (!state.schema) {
debug('Converting metadata to database schema');
state.schema = metadataToSchema(db.metadata);
}
return state.schema;
},
builder: createSchemaBuilder(db),
schemaDiff: createSchemaDiff(db),
schemaStorage: createSchemaStorage(db),
/**
* Drops the database schema
*/ async drop () {
debug('Dropping database schema');
const DBSchema = await db.dialect.schemaInspector.getSchema();
await this.builder.dropSchema(DBSchema);
},
/**
* Creates the database schema
*/ async create () {
debug('Created database schema');
await this.builder.createSchema(this.schema);
},
/**
* Resets the database schema
*/ async reset () {
debug('Resetting database schema');
await this.drop();
await this.create();
},
async syncSchema () {
debug('Synchronizing database schema');
const databaseSchema = await db.dialect.schemaInspector.getSchema();
const storedSchema = await this.schemaStorage.read();
/*
3way diff - DB schema / previous metadataSchema / new metadataSchema
- When something doesn't exist in the previous metadataSchema -> It's not tracked by us and should be ignored
- If no previous metadataSchema => use new metadataSchema so we start tracking them and ignore everything else
- Apply this logic to Tables / Columns / Indexes / FKs ...
- Handle errors (indexes or fks on incompatible stuff ...)
*/ const { status, diff } = await this.schemaDiff.diff({
previousSchema: storedSchema?.schema,
databaseSchema,
userSchema: this.schema
});
if (status === 'CHANGED') {
await this.builder.updateSchema(diff);
}
await this.schemaStorage.add(this.schema);
return status;
},
// TODO: support options to migrate softly or forcefully
// TODO: support option to disable auto migration & run a CLI command instead to avoid doing it at startup
// TODO: Allow keeping extra indexes / extra tables / extra columns (globally or on a per table basis)
async sync () {
if (await db.migrations.shouldRun()) {
debug('Found migrations to run');
await db.migrations.up();
return this.syncSchema();
}
const oldSchema = await this.schemaStorage.read();
if (!oldSchema) {
debug('Schema not persisted yet');
return this.syncSchema();
}
const { hash: oldHash } = oldSchema;
const hash = await this.schemaStorage.hashSchema(this.schema);
if (oldHash !== hash) {
debug('Schema changed');
return this.syncSchema();
}
debug('Schema unchanged');
return 'UNCHANGED';
}
};
};
export { createSchemaProvider };
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,4 @@
import type { Metadata } from '../metadata';
import type { Schema } from './types';
export declare const metadataToSchema: (metadata: Metadata) => Schema;
//# sourceMappingURL=schema.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/schema/schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAQ,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAU,MAAM,EAAS,MAAM,SAAS,CAAC;AA0NrD,eAAO,MAAM,gBAAgB,aAAc,QAAQ,KAAG,MAUrD,CAAC"}

View File

@@ -0,0 +1,266 @@
'use strict';
var types = require('../utils/types.js');
var index = require('../utils/identifiers/index.js');
/**
* TODO: This needs to be refactored to support incoming names such as
* (column, table, index) that are of the form string | NameToken[] so
* that pieces can be passed through and shortened here.
*
* Currently, we are potentially shortening twice, although in reality
* that won't happen since the shortened attribute column names will
* fit here because they are already shortened to the max identifier
* length
*
* That is the reason we use getName() here and not getColumnName();
* we just want the exact shortened name for the value without doing
* any other potential manipulation to it
* */ const createColumn = (name, attribute)=>{
const { type, args = [], ...opts } = getColumnType(attribute);
return {
name: index.identifiers.getName(name),
type,
args,
defaultTo: null,
notNullable: false,
unsigned: false,
...opts,
...'column' in attribute ? attribute.column ?? {} : {}
};
};
const createTable = (meta)=>{
const table = {
name: meta.tableName,
indexes: meta.indexes || [],
foreignKeys: meta.foreignKeys || [],
columns: []
};
for (const key of Object.keys(meta.attributes)){
const attribute = meta.attributes[key];
// if (types.isRelation(attribute.type)) {
if (attribute.type === 'relation') {
if ('morphColumn' in attribute && attribute.morphColumn && attribute.owner) {
const { idColumn, typeColumn } = attribute.morphColumn;
const idColumnName = index.identifiers.getName(idColumn.name);
const typeColumnName = index.identifiers.getName(typeColumn.name);
table.columns.push(createColumn(idColumnName, {
type: 'integer',
column: {
unsigned: true
}
}));
table.columns.push(createColumn(typeColumnName, {
type: 'string'
}));
} else if ('joinColumn' in attribute && attribute.joinColumn && attribute.owner && attribute.joinColumn.referencedTable) {
// NOTE: we could pass uniquness for oneToOne to avoid creating more than one to one
const { name: columnNameFull, referencedColumn, referencedTable, columnType = 'integer' } = attribute.joinColumn;
const columnName = index.identifiers.getName(columnNameFull);
const column = createColumn(columnName, {
// TODO: find the column type automatically, or allow passing all the column params
type: columnType,
column: {
unsigned: true
}
});
table.columns.push(column);
const fkName = index.identifiers.getFkIndexName([
table.name,
columnName
]);
table.foreignKeys.push({
name: fkName,
columns: [
column.name
],
referencedTable,
referencedColumns: [
referencedColumn
],
// NOTE: could allow configuration
onDelete: 'SET NULL'
});
table.indexes.push({
name: fkName,
columns: [
column.name
]
});
}
} else if (types.isScalarAttribute(attribute)) {
const columnName = index.identifiers.getName(attribute.columnName || key);
const column = createColumn(columnName, attribute);
if (column.unique) {
table.indexes.push({
type: 'unique',
name: index.identifiers.getUniqueIndexName([
table.name,
column.name
]),
columns: [
columnName
]
});
}
if (column.primary) {
table.indexes.push({
type: 'primary',
name: index.identifiers.getPrimaryIndexName([
table.name,
column.name
]),
columns: [
columnName
]
});
}
table.columns.push(column);
}
}
return table;
};
const getColumnType = (attribute)=>{
if ('columnType' in attribute && attribute.columnType) {
return attribute.columnType;
}
switch(attribute.type){
case 'increments':
{
return {
type: 'increments',
args: [
{
primary: true,
primaryKey: true
}
],
notNullable: true
};
}
// We might want to convert email/password to string types before going into the orm with specific validators & transformers
case 'password':
case 'email':
case 'string':
case 'enumeration':
{
return {
type: 'string'
};
}
case 'uid':
{
return {
type: 'string'
};
}
case 'richtext':
case 'text':
{
return {
type: 'text',
args: [
'longtext'
]
};
}
case 'blocks':
case 'json':
{
return {
type: 'jsonb'
};
}
case 'integer':
{
return {
type: 'integer'
};
}
case 'biginteger':
{
return {
type: 'bigInteger'
};
}
case 'float':
{
return {
type: 'double'
};
}
case 'decimal':
{
return {
type: 'decimal',
args: [
10,
2
]
};
}
case 'date':
{
return {
type: 'date'
};
}
case 'time':
{
return {
type: 'time',
args: [
{
precision: 3
}
]
};
}
case 'datetime':
{
return {
type: 'datetime',
args: [
{
useTz: false,
precision: 6
}
]
};
}
case 'timestamp':
{
return {
type: 'timestamp',
args: [
{
useTz: false,
precision: 6
}
]
};
}
case 'boolean':
{
return {
type: 'boolean'
};
}
default:
{
throw new Error(`Unknown type ${attribute.type}`);
}
}
};
const metadataToSchema = (metadata)=>{
const schema = {
tables: []
};
metadata.forEach((metadata)=>{
schema.tables.push(createTable(metadata));
});
return schema;
};
exports.metadataToSchema = metadataToSchema;
//# sourceMappingURL=schema.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,264 @@
import { isScalarAttribute } from '../utils/types.mjs';
import { identifiers } from '../utils/identifiers/index.mjs';
/**
* TODO: This needs to be refactored to support incoming names such as
* (column, table, index) that are of the form string | NameToken[] so
* that pieces can be passed through and shortened here.
*
* Currently, we are potentially shortening twice, although in reality
* that won't happen since the shortened attribute column names will
* fit here because they are already shortened to the max identifier
* length
*
* That is the reason we use getName() here and not getColumnName();
* we just want the exact shortened name for the value without doing
* any other potential manipulation to it
* */ const createColumn = (name, attribute)=>{
const { type, args = [], ...opts } = getColumnType(attribute);
return {
name: identifiers.getName(name),
type,
args,
defaultTo: null,
notNullable: false,
unsigned: false,
...opts,
...'column' in attribute ? attribute.column ?? {} : {}
};
};
const createTable = (meta)=>{
const table = {
name: meta.tableName,
indexes: meta.indexes || [],
foreignKeys: meta.foreignKeys || [],
columns: []
};
for (const key of Object.keys(meta.attributes)){
const attribute = meta.attributes[key];
// if (types.isRelation(attribute.type)) {
if (attribute.type === 'relation') {
if ('morphColumn' in attribute && attribute.morphColumn && attribute.owner) {
const { idColumn, typeColumn } = attribute.morphColumn;
const idColumnName = identifiers.getName(idColumn.name);
const typeColumnName = identifiers.getName(typeColumn.name);
table.columns.push(createColumn(idColumnName, {
type: 'integer',
column: {
unsigned: true
}
}));
table.columns.push(createColumn(typeColumnName, {
type: 'string'
}));
} else if ('joinColumn' in attribute && attribute.joinColumn && attribute.owner && attribute.joinColumn.referencedTable) {
// NOTE: we could pass uniquness for oneToOne to avoid creating more than one to one
const { name: columnNameFull, referencedColumn, referencedTable, columnType = 'integer' } = attribute.joinColumn;
const columnName = identifiers.getName(columnNameFull);
const column = createColumn(columnName, {
// TODO: find the column type automatically, or allow passing all the column params
type: columnType,
column: {
unsigned: true
}
});
table.columns.push(column);
const fkName = identifiers.getFkIndexName([
table.name,
columnName
]);
table.foreignKeys.push({
name: fkName,
columns: [
column.name
],
referencedTable,
referencedColumns: [
referencedColumn
],
// NOTE: could allow configuration
onDelete: 'SET NULL'
});
table.indexes.push({
name: fkName,
columns: [
column.name
]
});
}
} else if (isScalarAttribute(attribute)) {
const columnName = identifiers.getName(attribute.columnName || key);
const column = createColumn(columnName, attribute);
if (column.unique) {
table.indexes.push({
type: 'unique',
name: identifiers.getUniqueIndexName([
table.name,
column.name
]),
columns: [
columnName
]
});
}
if (column.primary) {
table.indexes.push({
type: 'primary',
name: identifiers.getPrimaryIndexName([
table.name,
column.name
]),
columns: [
columnName
]
});
}
table.columns.push(column);
}
}
return table;
};
const getColumnType = (attribute)=>{
if ('columnType' in attribute && attribute.columnType) {
return attribute.columnType;
}
switch(attribute.type){
case 'increments':
{
return {
type: 'increments',
args: [
{
primary: true,
primaryKey: true
}
],
notNullable: true
};
}
// We might want to convert email/password to string types before going into the orm with specific validators & transformers
case 'password':
case 'email':
case 'string':
case 'enumeration':
{
return {
type: 'string'
};
}
case 'uid':
{
return {
type: 'string'
};
}
case 'richtext':
case 'text':
{
return {
type: 'text',
args: [
'longtext'
]
};
}
case 'blocks':
case 'json':
{
return {
type: 'jsonb'
};
}
case 'integer':
{
return {
type: 'integer'
};
}
case 'biginteger':
{
return {
type: 'bigInteger'
};
}
case 'float':
{
return {
type: 'double'
};
}
case 'decimal':
{
return {
type: 'decimal',
args: [
10,
2
]
};
}
case 'date':
{
return {
type: 'date'
};
}
case 'time':
{
return {
type: 'time',
args: [
{
precision: 3
}
]
};
}
case 'datetime':
{
return {
type: 'datetime',
args: [
{
useTz: false,
precision: 6
}
]
};
}
case 'timestamp':
{
return {
type: 'timestamp',
args: [
{
useTz: false,
precision: 6
}
]
};
}
case 'boolean':
{
return {
type: 'boolean'
};
}
default:
{
throw new Error(`Unknown type ${attribute.type}`);
}
}
};
const metadataToSchema = (metadata)=>{
const schema = {
tables: []
};
metadata.forEach((metadata)=>{
schema.tables.push(createTable(metadata));
});
return schema;
};
export { metadataToSchema };
//# sourceMappingURL=schema.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
import type { Database } from '..';
import type { Schema } from './types';
declare const _default: (db: Database) => {
read(): Promise<{
id: number;
time: Date;
hash: string;
schema: Schema;
} | null>;
hashSchema(schema: Schema): string;
add(schema: Schema): Promise<void>;
clear(): Promise<void>;
};
export default _default;
//# sourceMappingURL=storage.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/schema/storage.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AACnC,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;6BAIlB,QAAQ;YAmBV,QAAQ;QACpB,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,IAAI,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;KAChB,GAAG,IAAI,CAAC;uBAmCU,MAAM;gBAIP,MAAM;;;AA/D5B,wBAuFE"}

View File

@@ -0,0 +1,66 @@
'use strict';
var crypto = require('crypto');
const TABLE_NAME = 'strapi_database_schema';
var createSchemaStorage = ((db)=>{
const hasSchemaTable = ()=>db.getSchemaConnection().hasTable(TABLE_NAME);
const createSchemaTable = ()=>{
return db.getSchemaConnection().createTable(TABLE_NAME, (t)=>{
t.increments('id');
t.json('schema');
t.datetime('time', {
useTz: false
});
t.string('hash');
});
};
const checkTableExists = async ()=>{
if (!await hasSchemaTable()) {
await createSchemaTable();
}
};
return {
async read () {
await checkTableExists();
// NOTE: We get the ID first before fetching the exact entry for performance on MySQL/MariaDB
// See: https://github.com/strapi/strapi/issues/20312
const getSchemaID = await db.getConnection().select('id').from(TABLE_NAME).orderBy('time', 'DESC').first();
if (!getSchemaID) {
return null;
}
const res = await db.getConnection().select('*').from(TABLE_NAME).where({
id: getSchemaID.id
}).first();
if (!res) {
return null;
}
const parsedSchema = typeof res.schema === 'object' ? res.schema : JSON.parse(res.schema);
return {
...res,
schema: parsedSchema
};
},
hashSchema (schema) {
return crypto.createHash('md5').update(JSON.stringify(schema)).digest('hex');
},
async add (schema) {
await checkTableExists();
// NOTE: we can remove this to add history
await db.getConnection(TABLE_NAME).delete();
const time = new Date();
await db.getConnection().insert({
schema: JSON.stringify(schema),
hash: this.hashSchema(schema),
time
}).into(TABLE_NAME);
},
async clear () {
await checkTableExists();
await db.getConnection(TABLE_NAME).truncate();
}
};
});
module.exports = createSchemaStorage;
//# sourceMappingURL=storage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"storage.js","sources":["../../src/schema/storage.ts"],"sourcesContent":["import crypto from 'crypto';\n\nimport type { Database } from '..';\nimport type { Schema } from './types';\n\nconst TABLE_NAME = 'strapi_database_schema';\n\nexport default (db: Database) => {\n const hasSchemaTable = () => db.getSchemaConnection().hasTable(TABLE_NAME);\n\n const createSchemaTable = () => {\n return db.getSchemaConnection().createTable(TABLE_NAME, (t) => {\n t.increments('id');\n t.json('schema');\n t.datetime('time', { useTz: false });\n t.string('hash');\n });\n };\n\n const checkTableExists = async () => {\n if (!(await hasSchemaTable())) {\n await createSchemaTable();\n }\n };\n\n return {\n async read(): Promise<{\n id: number;\n time: Date;\n hash: string;\n schema: Schema;\n } | null> {\n await checkTableExists();\n\n // NOTE: We get the ID first before fetching the exact entry for performance on MySQL/MariaDB\n // See: https://github.com/strapi/strapi/issues/20312\n const getSchemaID = await db\n .getConnection()\n .select('id')\n .from(TABLE_NAME)\n .orderBy('time', 'DESC')\n .first();\n\n if (!getSchemaID) {\n return null;\n }\n\n const res = await db\n .getConnection()\n .select('*')\n .from(TABLE_NAME)\n .where({ id: getSchemaID.id })\n .first();\n\n if (!res) {\n return null;\n }\n\n const parsedSchema = typeof res.schema === 'object' ? res.schema : JSON.parse(res.schema);\n\n return {\n ...res,\n schema: parsedSchema,\n };\n },\n\n hashSchema(schema: Schema) {\n return crypto.createHash('md5').update(JSON.stringify(schema)).digest('hex');\n },\n\n async add(schema: Schema) {\n await checkTableExists();\n\n // NOTE: we can remove this to add history\n await db.getConnection(TABLE_NAME).delete();\n\n const time = new Date();\n\n await db\n .getConnection()\n .insert({\n schema: JSON.stringify(schema),\n hash: this.hashSchema(schema),\n time,\n })\n .into(TABLE_NAME);\n },\n\n async clear() {\n await checkTableExists();\n\n await db.getConnection(TABLE_NAME).truncate();\n },\n };\n};\n"],"names":["TABLE_NAME","db","hasSchemaTable","getSchemaConnection","hasTable","createSchemaTable","createTable","t","increments","json","datetime","useTz","string","checkTableExists","read","getSchemaID","getConnection","select","from","orderBy","first","res","where","id","parsedSchema","schema","JSON","parse","hashSchema","crypto","createHash","update","stringify","digest","add","delete","time","Date","insert","hash","into","clear","truncate"],"mappings":"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"}

View File

@@ -0,0 +1,64 @@
import crypto from 'crypto';
const TABLE_NAME = 'strapi_database_schema';
var createSchemaStorage = ((db)=>{
const hasSchemaTable = ()=>db.getSchemaConnection().hasTable(TABLE_NAME);
const createSchemaTable = ()=>{
return db.getSchemaConnection().createTable(TABLE_NAME, (t)=>{
t.increments('id');
t.json('schema');
t.datetime('time', {
useTz: false
});
t.string('hash');
});
};
const checkTableExists = async ()=>{
if (!await hasSchemaTable()) {
await createSchemaTable();
}
};
return {
async read () {
await checkTableExists();
// NOTE: We get the ID first before fetching the exact entry for performance on MySQL/MariaDB
// See: https://github.com/strapi/strapi/issues/20312
const getSchemaID = await db.getConnection().select('id').from(TABLE_NAME).orderBy('time', 'DESC').first();
if (!getSchemaID) {
return null;
}
const res = await db.getConnection().select('*').from(TABLE_NAME).where({
id: getSchemaID.id
}).first();
if (!res) {
return null;
}
const parsedSchema = typeof res.schema === 'object' ? res.schema : JSON.parse(res.schema);
return {
...res,
schema: parsedSchema
};
},
hashSchema (schema) {
return crypto.createHash('md5').update(JSON.stringify(schema)).digest('hex');
},
async add (schema) {
await checkTableExists();
// NOTE: we can remove this to add history
await db.getConnection(TABLE_NAME).delete();
const time = new Date();
await db.getConnection().insert({
schema: JSON.stringify(schema),
hash: this.hashSchema(schema),
time
}).into(TABLE_NAME);
},
async clear () {
await checkTableExists();
await db.getConnection(TABLE_NAME).truncate();
}
};
});
export { createSchemaStorage as default };
//# sourceMappingURL=storage.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"storage.mjs","sources":["../../src/schema/storage.ts"],"sourcesContent":["import crypto from 'crypto';\n\nimport type { Database } from '..';\nimport type { Schema } from './types';\n\nconst TABLE_NAME = 'strapi_database_schema';\n\nexport default (db: Database) => {\n const hasSchemaTable = () => db.getSchemaConnection().hasTable(TABLE_NAME);\n\n const createSchemaTable = () => {\n return db.getSchemaConnection().createTable(TABLE_NAME, (t) => {\n t.increments('id');\n t.json('schema');\n t.datetime('time', { useTz: false });\n t.string('hash');\n });\n };\n\n const checkTableExists = async () => {\n if (!(await hasSchemaTable())) {\n await createSchemaTable();\n }\n };\n\n return {\n async read(): Promise<{\n id: number;\n time: Date;\n hash: string;\n schema: Schema;\n } | null> {\n await checkTableExists();\n\n // NOTE: We get the ID first before fetching the exact entry for performance on MySQL/MariaDB\n // See: https://github.com/strapi/strapi/issues/20312\n const getSchemaID = await db\n .getConnection()\n .select('id')\n .from(TABLE_NAME)\n .orderBy('time', 'DESC')\n .first();\n\n if (!getSchemaID) {\n return null;\n }\n\n const res = await db\n .getConnection()\n .select('*')\n .from(TABLE_NAME)\n .where({ id: getSchemaID.id })\n .first();\n\n if (!res) {\n return null;\n }\n\n const parsedSchema = typeof res.schema === 'object' ? res.schema : JSON.parse(res.schema);\n\n return {\n ...res,\n schema: parsedSchema,\n };\n },\n\n hashSchema(schema: Schema) {\n return crypto.createHash('md5').update(JSON.stringify(schema)).digest('hex');\n },\n\n async add(schema: Schema) {\n await checkTableExists();\n\n // NOTE: we can remove this to add history\n await db.getConnection(TABLE_NAME).delete();\n\n const time = new Date();\n\n await db\n .getConnection()\n .insert({\n schema: JSON.stringify(schema),\n hash: this.hashSchema(schema),\n time,\n })\n .into(TABLE_NAME);\n },\n\n async clear() {\n await checkTableExists();\n\n await db.getConnection(TABLE_NAME).truncate();\n },\n };\n};\n"],"names":["TABLE_NAME","db","hasSchemaTable","getSchemaConnection","hasTable","createSchemaTable","createTable","t","increments","json","datetime","useTz","string","checkTableExists","read","getSchemaID","getConnection","select","from","orderBy","first","res","where","id","parsedSchema","schema","JSON","parse","hashSchema","crypto","createHash","update","stringify","digest","add","delete","time","Date","insert","hash","into","clear","truncate"],"mappings":"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"}

View File

@@ -0,0 +1,103 @@
export interface Schema {
tables: Table[];
}
export interface Table {
name: string;
columns: Column[];
indexes: Index[];
foreignKeys: ForeignKey[];
}
export interface Column {
type: string;
name: string;
args?: unknown[];
defaultTo?: any;
notNullable?: boolean | null;
unsigned?: boolean;
unique?: boolean;
primary?: boolean;
}
export type IndexType = 'primary' | 'unique';
export interface Index {
columns: string[];
name: string;
type?: IndexType;
}
export interface ForeignKey {
name: string;
columns: string[];
referencedColumns: string[];
referencedTable: string;
onUpdate?: string | null;
onDelete?: string | null;
}
export interface IndexDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
name: string;
object: Index;
};
}
export interface ColumnDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
name: string;
object: Column;
};
}
export interface ColumnsDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
added: Column[];
removed: Column[];
updated: ColumnDiff['diff'][];
unchanged: Column[];
};
}
export interface ForeignKeyDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
name: string;
object: ForeignKey;
};
}
export interface ForeignKeysDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
added: ForeignKey[];
updated: ForeignKeyDiff['diff'][];
unchanged: ForeignKey[];
removed: ForeignKey[];
};
}
export interface IndexesDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
added: Index[];
updated: IndexDiff['diff'][];
unchanged: Index[];
removed: Index[];
};
}
export interface TableDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
name: string;
indexes: IndexesDiff['diff'];
columns: ColumnsDiff['diff'];
foreignKeys: ForeignKeysDiff['diff'];
};
}
export interface TablesDiff {
added: Table[];
removed: Table[];
updated: Array<TableDiff['diff']>;
unchanged: Table[];
}
export interface SchemaDiff {
status: 'CHANGED' | 'UNCHANGED';
diff: {
tables: TablesDiff;
};
}
//# sourceMappingURL=types.d.ts.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/schema/types.ts"],"names":[],"mappings":"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"}