src/index.js
import validator from 'schema-mapper-validator';
class Optimizer {
/**
* Optimize changes
* @param {Changes} changes An array of changes
* @return {Changes} An array of optimized changes
*/
optimize(changes, agressive = false) {
var validationResult = validator.validateChanges(changes);
if (validationResult.success === false) {
throw new Error(validationResult.message);
}
var index = this.createIndex(changes);
var optimizedIndex = this.optimizeIndex(index, agressive);
return this.indexToChanges(optimizedIndex);
}
/**
* Index the changes so we can easily process them
* @param {Changes} changes An array of changes
* @return {Object} The changes indexed by their type and schema (and column id in case of column related changes)
*/
createIndex(changes) {
var index = {
createdProjects: {},
removedProjects: {},
renamedProjects: {},
taggedProjects: {},
createdSchemas: {},
removedSchemas: {},
renamedSchemas: {},
createdColumns: {},
removedColumns: {},
renamedColumns: {},
typeChangedColumns: {}
};
changes.forEach((change) => {
if (change.change === 'project.create') {
index.createdProjects[change.projectId] = change;
}
if (change.change === 'project.remove') {
index.removedProjects[change.projectId] = change;
}
if (change.change === 'project.rename') {
index.renamedProjects[change.projectId] = change;
}
if (change.change === 'project.tag') {
index.taggedProjects[change.projectId] = change;
}
if (change.change === 'schema.create') {
if ( ! index.createdSchemas[change.projectId]) index.createdSchemas[change.projectId] = {};
index.createdSchemas[change.projectId][change.schemaId] = change;
}
if (change.change === 'schema.remove') {
if ( ! index.removedSchemas[change.projectId]) index.removedSchemas[change.projectId] = {};
index.removedSchemas[change.projectId][change.schemaId] = change;
}
if (change.change === 'schema.rename') {
if ( ! index.renamedSchemas[change.projectId]) index.renamedSchemas[change.projectId] = {};
index.renamedSchemas[change.projectId][change.schemaId] = change;
}
if (change.change === 'column.create') {
if ( ! index.createdColumns[change.projectId]) index.createdColumns[change.projectId] = {};
if ( ! index.createdColumns[change.projectId][change.schemaId]) index.createdColumns[change.projectId][change.schemaId] = {};
index.createdColumns[change.projectId][change.schemaId][change.columnId] = change;
}
if (change.change === 'column.remove') {
if ( ! index.removedColumns[change.projectId]) index.removedColumns[change.projectId] = {};
if ( ! index.removedColumns[change.projectId][change.schemaId]) index.removedColumns[change.projectId][change.schemaId] = {};
index.removedColumns[change.projectId][change.schemaId][change.columnId] = change;
}
if (change.change === 'column.rename') {
if ( ! index.renamedColumns[change.projectId]) index.renamedColumns[change.projectId] = {};
if ( ! index.renamedColumns[change.projectId][change.schemaId]) index.renamedColumns[change.projectId][change.schemaId] = {};
index.renamedColumns[change.projectId][change.schemaId][change.columnId] = change;
}
if (change.change === 'column.typechange') {
if ( ! index.typeChangedColumns[change.projectId]) index.typeChangedColumns[change.projectId] = {};
if ( ! index.typeChangedColumns[change.projectId][change.schemaId]) index.typeChangedColumns[change.projectId][change.schemaId] = {};
index.typeChangedColumns[change.projectId][change.schemaId][change.columnId] = change;
}
});
return index;
}
/**
* Optimize and index of changes
* @param {Object} index Changes indexed by their type and schema (and column id in case of column related changes)
* @return {Object} Optimized changes indexed by their type and schema (and column id in case of column related changes)
*/
optimizeIndex(index, agressive) {
// by doing this first we might be able to remove related changes and do less work later
this.optimizeRemovedProjectsIndex(index, agressive);
this.optimizeRenamedProjectsIndex(index, agressive);
this.optimizeTaggedProjectsIndex(index, agressive);
// by doing this early on we might be able to remove related changes and do less work later
this.optimizeRemovedSchemasIndex(index, agressive);
this.optimizeRenamedSchemasIndex(index, agressive);
this.optimizeRemovedColumnsIndex(index, agressive);
this.optimizeRenamedColumnsIndex(index, agressive);
this.optimizeTypeChangedColumnsIndex(index, agressive);
// this one have to go after all other optimizations so changes can be merged with created column
this.optimizeCreatedColumnsIndex(index, agressive);
// in this stage, column changes that were merged with a created schema can now be further optimized to be merged with a project
this.optimizeCreatedSchemasIndex(index, agressive);
return index;
}
optimizeRemovedProjectsIndex(index, agressive) {
// loop over all removed projects
Object.keys(index.removedProjects).forEach((projectId) => {
// when a project.create change is found for this project
if (index.createdProjects[projectId]) {
// delete all changes that have this project id
if ( ! agressive) {
delete index.removedProjects[projectId];
}
delete index.createdProjects[projectId];
delete index.renamedProjects[projectId];
delete index.taggedProjects[projectId];
delete index.createdSchemas[projectId];
delete index.removedSchemas[projectId];
delete index.renamedSchemas[projectId];
delete index.createdColumns[projectId];
delete index.removedColumns[projectId];
delete index.renamedColumns[projectId];
delete index.typeChangedColumns[projectId];
}
});
return index;
}
optimizeRenamedProjectsIndex(index, agressive) {
if ( ! agressive) {
return index;
}
// loop over all renamed projects
Object.keys(index.renamedProjects).forEach((projectId) => {
// when a project.create change is found for this project
if (index.createdProjects[projectId]) {
// apply the rename to the project.create change
index.createdProjects[projectId].project.name = index.renamedProjects[projectId].name;
// delete the rename change for this project
delete index.renamedProjects[projectId];
}
});
return index;
}
optimizeTaggedProjectsIndex(index, agressive) {
if ( ! agressive) {
return index;
}
// loop over all tagged projects
Object.keys(index.taggedProjects).forEach((projectId) => {
// when a project.create change is found for this project
if (index.createdProjects[projectId]) {
// apply the tag change to the project.create change
index.createdProjects[projectId].project.version = index.taggedProjects[projectId].version;
// delete the tag change
delete index.taggedProjects[projectId];
}
});
return index;
}
optimizeRemovedSchemasIndex(index, agressive) {
// loop over all removed schemas
Object.keys(index.removedSchemas).forEach((projectId) => {
Object.keys(index.removedSchemas[projectId]).forEach((schemaId) => {
// when a schema.create change is found for this schema
if (index.createdSchemas[projectId] && index.createdSchemas[projectId][schemaId]) {
// delete all changes that have this schemaId
if (agressive) {
delete index.removedSchemas[projectId][schemaId];
}
if (index.createdSchemas[projectId]) delete index.createdSchemas[projectId][schemaId];
if (index.renamedSchemas[projectId]) delete index.renamedSchemas[projectId][schemaId];
if (index.createdColumns[projectId]) delete index.createdColumns[projectId][schemaId];
if (index.removedColumns[projectId]) delete index.removedColumns[projectId][schemaId];
if (index.renamedColumns[projectId]) delete index.renamedColumns[projectId][schemaId];
if (index.typeChangedColumns[projectId]) delete index.typeChangedColumns[projectId][schemaId];
}
// when a project.create change is found for this schema
if (index.createdProjects[projectId] && index.createdProjects[projectId].schemas[schemaId]) {
if (agressive) {
// remove the schema from the project.create change
delete index.createdProjects[projectId].schemas[schemaId];
// delete all changes that have this schemaId
delete index.removedSchemas[projectId][schemaId];
}
if (index.createdSchemas[projectId]) delete index.createdSchemas[projectId][schemaId];
if (index.renamedSchemas[projectId]) delete index.renamedSchemas[projectId][schemaId];
if (index.createdColumns[projectId]) delete index.createdColumns[projectId][schemaId];
if (index.removedColumns[projectId]) delete index.removedColumns[projectId][schemaId];
if (index.renamedColumns[projectId]) delete index.renamedColumns[projectId][schemaId];
if (index.typeChangedColumns[projectId]) delete index.typeChangedColumns[projectId][schemaId];
}
});
});
return index;
}
optimizeRenamedSchemasIndex(index, agressive) {
if ( ! agressive) {
return index;
}
// loop over all renamed schemas
Object.keys(index.renamedSchemas).forEach((projectId) => {
Object.keys(index.renamedSchemas[projectId]).forEach((schemaId) => {
// when a schema.create change is found for this schema
if (index.createdSchemas[projectId] && index.createdSchemas[projectId][schemaId]) {
// apply the rename to the schema.create change
index.createdSchemas[projectId][schemaId].schema.name = index.renamedSchemas[projectId][schemaId].name;
// delete the rename change for this schema
delete index.renamedSchemas[projectId][schemaId];
}
// when a project.create change is found for this schema
if (index.createdProjects[projectId] && index.createdProjects[projectId].project.schemas[schemaId]) {
// apply the rename to the project.create change
index.createdProjects[projectId].project.schemas[schemaId].schema.name = index.renamedSchemas[projectId][schemaId].name;
// delete the rename change for this schema
delete index.renamedSchemas[projectId][schemaId];
}
});
});
return index;
}
optimizeRemovedColumnsIndex(index, agressive) {
// loop over all removed columns
Object.keys(index.removedColumns).forEach((projectId) => {
Object.keys(index.removedColumns[projectId]).forEach((schemaId) => {
Object.keys(index.removedColumns[projectId][schemaId]).forEach((columnId) => {
// when a column.create change is found for this column
if (index.createdColumns[projectId] && index.createdColumns[projectId][schemaId] && index.createdColumns[projectId][schemaId][columnId]) {
// delete all changes that have this columnId
if (agressive) {
delete index.removedColumns[projectId][schemaId][columnId];
}
if (index.createdColumns[projectId] && index.createdColumns[projectId][schemaId]) delete index.createdColumns[projectId][schemaId][columnId];
if (index.renamedColumns[projectId] && index.renamedColumns[projectId][schemaId]) delete index.renamedColumns[projectId][schemaId][columnId];
if (index.typeChangedColumns[projectId] && index.typeChangedColumns[projectId][schemaId]) delete index.typeChangedColumns[projectId][schemaId][columnId];
}
// when a schema.create change is found for this column
if (index.createdSchemas[projectId] && index.createdSchemas[projectId][schemaId] && index.createdSchemas[projectId][schemaId].schema.columns[columnId]) {
if (agressive) {
// remove the column from the schema.create change
delete index.createdSchemas[projectId][schemaId].schema.columns[columnId];
// delete all changes that have this columnId
delete index.removedColumns[projectId][schemaId][columnId];
}
if (index.createdColumns[projectId] && index.createdColumns[projectId][schemaId]) delete index.createdColumns[projectId][schemaId][columnId];
if (index.renamedColumns[projectId] && index.renamedColumns[projectId][schemaId]) delete index.renamedColumns[projectId][schemaId][columnId];
if (index.typeChangedColumns[projectId] && index.typeChangedColumns[projectId][schemaId]) delete index.typeChangedColumns[projectId][schemaId][columnId];
}
// when a project.create change is found for this column
if (index.createdProjects[projectId] && index.createdProjects[projectId].project.schemas[schemaId] && index.createdProjects[projectId].project.schemas[schemaId].columns[columnId]) {
if (agressive) {
// remove the column from the project.create change
delete index.createdProjects[projectId].project.schemas[schemaId].columns[columnId];
// delete all changes that have this columnId
delete index.removedColumns[projectId][schemaId][columnId];
}
if (index.createdColumns[projectId] && index.createdColumns[projectId][schemaId]) delete index.createdColumns[projectId][schemaId][columnId];
if (index.renamedColumns[projectId] && index.renamedColumns[projectId][schemaId]) delete index.renamedColumns[projectId][schemaId][columnId];
if (index.typeChangedColumns[projectId] && index.typeChangedColumns[projectId][schemaId]) delete index.typeChangedColumns[projectId][schemaId][columnId];
}
});
});
});
}
optimizeRenamedColumnsIndex(index, agressive) {
if ( ! agressive) {
return index;
}
// loop over all renamed columns
Object.keys(index.renamedColumns).forEach((projectId) => {
Object.keys(index.renamedColumns[projectId]).forEach((schemaId) => {
Object.keys(index.renamedColumns[projectId][schemaId]).forEach((columnId) => {
// when a column.create change is found for this column
if (index.createdColumns[projectId] && index.createdColumns[projectId][schemaId] && index.createdColumns[projectId][schemaId][columnId]) {
// apply the rename to the column.create change
index.createdColumns[projectId][schemaId][columnId].column.name = index.renamedColumns[projectId][schemaId][columnId].name;
// delete the rename change for this column
delete index.renamedColumns[projectId][schemaId][columnId];
}
// when a schema.create change is found for this column
if (index.createdSchemas[projectId] && index.createdSchemas[projectId][schemaId] && index.createdSchemas[projectId][schemaId].schema.columns[columnId]) {
// apply the rename to the schema.create change
index.createdSchemas[projectId][schemaId].schema.columns[columnId].name = index.renamedColumns[projectId][schemaId][columnId].name;
// delete the rename change for this column
delete index.renamedColumns[projectId][schemaId][columnId];
}
// when a project.create change is found for this column
if (index.createdProjects[projectId] && index.createdProjects[projectId].project.schemas[schemaId] && index.createdProjects[projectId].project.schemas[schemaId].columns[columnId]) {
// apply the rename to the project.create change
index.createdProjects[projectId].project.schemas[schemaId].columns[columnId].name = index.renamedColumns[projectId][schemaId][columnId].name;
// delete the rename change for this column
delete index.renamedColumns[projectId][schemaId][columnId];
}
});
});
});
return index;
}
optimizeTypeChangedColumnsIndex(index, agressive) {
if ( ! agressive) {
return index;
}
// loop over all typechanged column
Object.keys(index.typeChangedColumns).forEach((projectId) => {
Object.keys(index.typeChangedColumns[projectId]).forEach((schemaId) => {
Object.keys(index.typeChangedColumns[projectId][schemaId]).forEach((columnId) => {
// when a column.create change is found for this column
if (index.createdColumns[projectId] && index.createdColumns[projectId][schemaId] && index.createdColumns[projectId][schemaId][columnId]) {
// apply the typechange to the column.create change
index.createdColumns[projectId][schemaId][columnId].column.type = index.typeChangedColumns[projectId][schemaId][columnId].type;
// delete the typechange change for this column
delete index.typeChangedColumns[projectId][schemaId][columnId];
}
// when a schema.create change is found for this column
if (index.createdSchemas[projectId] && index.createdSchemas[projectId][schemaId] && index.createdSchemas[projectId][schemaId].schema.columns[columnId]) {
// apply the typechange to the schema.create change
index.createdSchemas[projectId][schemaId].schema.columns[columnId].type = index.typeChangedColumns[projectId][schemaId][columnId].type;
// delete the typechange change for this column
delete index.typeChangedColumns[projectId][schemaId][columnId];
}
// when a project.create change is found for this column
if (index.createdProjects[projectId] && index.createdProjects[projectId].project.schemas[schemaId] && index.createdProjects[projectId].project.schemas[schemaId].columns[columnId]) {
// apply the typechange to the project.create change
index.createdProjects[projectId].project.schemas[schemaId].columns[columnId].type = index.typeChangedColumns[projectId][schemaId][columnId].type;
// delete the typechange change for this column
delete index.typeChangedColumns[projectId][schemaId][columnId];
}
});
});
});
return index;
}
optimizeCreatedColumnsIndex(index, agressive) {
if ( ! agressive) {
return index;
}
// loop over all created columns
Object.keys(index.createdColumns).forEach((projectId) => {
Object.keys(index.createdColumns[projectId]).forEach((schemaId) => {
Object.keys(index.createdColumns[projectId][schemaId]).forEach((columnId) => {
// when a schema.create change is found for this column
if (index.createdSchemas[projectId] && index.createdSchemas[projectId][schemaId]) {
// add the column to the schema.create change
index.createdSchemas[projectId][schemaId].schema.columns[columnId] = index.createdColumns[projectId][schemaId][columnId].column;
// delete the create change for this column
delete index.createdColumns[projectId][schemaId][columnId];
}
// when a project.create change is found for this column
if (index.createdProjects[projectId] && index.createdProjects[projectId].project.schemas[schemaId]) {
// add the column to the project.create change
index.createdProjects[projectId].project.schemas[schemaId].columns[columnId] = index.createdColumns[projectId][schemaId][columnId].column;
// delete the create change for this column
delete index.createdColumns[projectId][schemaId][columnId];
}
});
});
});
return index;
}
optimizeCreatedSchemasIndex(index, agressive) {
if ( ! agressive) {
return index;
}
// loop over all created schemas
Object.keys(index.createdSchemas).forEach((projectId) => {
Object.keys(index.createdSchemas[projectId]).forEach((schemaId) => {
// when a project.create change is found for this schema
if (index.createdProjects[projectId]) {
// add the schema to the project.create change
index.createdProjects[projectId].project.schemas[schemaId] = index.createdSchemas[projectId][schemaId].schema;
// delete this schema.create change
delete index.createdSchemas[projectId][schemaId];
}
});
});
return index;
}
/**
* Turn an index of changes back into an array of changes
* @param {Object} index Changes indexed by their type and schema (and column id in case of column related changes)
* @return {Changes} An array of changes
*/
indexToChanges(index) {
var changes = [];
changes = changes.concat(this.objectValues(index.createdProjects));
changes = changes.concat(this.objectValues(index.renamedProjects));
changes = changes.concat(this.objectValues(index.taggedProjects));
Object.keys(index.createdSchemas).forEach((projectId) => {
changes = changes.concat(this.objectValues(index.createdSchemas[projectId]));
});
Object.keys(index.renamedSchemas).forEach((projectId) => {
changes = changes.concat(this.objectValues(index.renamedSchemas[projectId]));
});
Object.keys(index.createdColumns).forEach((projectId) => {
Object.keys(index.createdColumns[projectId]).forEach((schemaId) => {
changes = changes.concat(this.objectValues(index.createdColumns[projectId][schemaId]));
});
});
Object.keys(index.renamedColumns).forEach((projectId) => {
Object.keys(index.renamedColumns[projectId]).forEach((schemaId) => {
changes = changes.concat(this.objectValues(index.renamedColumns[projectId][schemaId]));
});
});
Object.keys(index.typeChangedColumns).forEach((projectId) => {
Object.keys(index.typeChangedColumns[projectId]).forEach((schemaId) => {
changes = changes.concat(this.objectValues(index.typeChangedColumns[projectId][schemaId]));
});
});
Object.keys(index.removedColumns).forEach((projectId) => {
Object.keys(index.removedColumns[projectId]).forEach((schemaId) => {
changes = changes.concat(this.objectValues(index.removedColumns[projectId][schemaId]));
});
});
Object.keys(index.removedSchemas).forEach((projectId) => {
changes = changes.concat(this.objectValues(index.removedSchemas[projectId]));
});
changes = changes.concat(this.objectValues(index.removedProjects));
return changes;
}
/**
* Helper method to get the values of an object as an array
* @param {Object} object The object to get the values from
* @return {Array} An array containing the values of the input object
*/
objectValues(object) {
return Object.keys(object).map((key) => {
return object[key];
});
}
}
export default new Optimizer();