Home Reference Source Repository

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();