ForestAdmin/toolbelt

View on GitHub
src/services/schema/schema-service.js

Summary

Maintainability
B
5 hrs
Test Coverage
B
85%
const ForestCLIError = require('../../errors/forest-cli-error');

module.exports = class SchemaService {
  constructor({
    assertPresent,
    constants,
    database,
    databaseAnalyzer,
    forestExpressDumper,
    env,
    errorHandler,
    fs,
    logger,
    path,
    spinner,
  }) {
    assertPresent({
      constants,
      database,
      databaseAnalyzer,
      forestExpressDumper,
      env,
      errorHandler,
      fs,
      logger,
      path,
      spinner,
    });
    this.constants = constants;
    this.database = database;
    this.databaseAnalyzer = databaseAnalyzer;
    this.dumper = forestExpressDumper;
    this.env = env;
    this.errorHandler = errorHandler;
    this.fs = fs;
    this.logger = logger;
    this.path = path;
    this.spinner = spinner;
  }

  _assertOutputDirectory(outputDirectory) {
    if (!outputDirectory) {
      this.dumper.checkForestCLIProjectStructure();
    } else if (this.fs.existsSync(outputDirectory)) {
      throw new ForestCLIError(`The output directory "${outputDirectory}" already exists.`);
    }
  }

  _getDatabasesConfig(path) {
    const configPath = this.path.resolve(this.constants.CURRENT_WORKING_DIRECTORY, path);
    if (!this.fs.existsSync(configPath)) {
      throw new ForestCLIError(`The configuration file "${configPath}" does not exist.`);
    }

    // eslint-disable-next-line global-require, import/no-dynamic-require
    const databasesConfig = require(configPath);
    if (!this.database.areAllDatabasesOfTheSameType(databasesConfig)) {
      throw new ForestCLIError(`The "${configPath}" file contains different databases types.`);
    }
    return databasesConfig;
  }

  async _connectToDatabases(databasesConfig) {
    this.spinner.start({ text: 'Connecting to your database(s)' });
    const databasesConnectionPromise = this.database.connectFromDatabasesConfig(databasesConfig);
    return this.spinner.attachToPromise(databasesConnectionPromise);
  }

  async _disconnectFromDatabases(databaseConnections) {
    this.spinner.start({ text: 'Disconnecting from your database(s)' });
    const databasesConnectionPromise = this.database.disconnectFromDatabases(databaseConnections);
    return this.spinner.attachToPromise(databasesConnectionPromise);
  }

  async _analyzeDatabases(databasesConnection, dbSchema) {
    this.spinner.start({ text: 'Analyzing the database(s)' });
    const databasesSchemaPromise = Promise.all(
      databasesConnection.map(async databaseConnection => {
        const analyzerOptions = {
          dbDialect: this.database.getDialect(databaseConnection.connection.url),
          dbSchema,
        };

        const schema = await this.databaseAnalyzer.analyze(
          databaseConnection.connectionInstance,
          analyzerOptions,
          true,
        );

        return {
          ...databaseConnection,
          schema,
          analyzerOptions,
        };
      }),
    );
    return this.spinner.attachToPromise(databasesSchemaPromise);
  }

  async _dumpSchemas(databasesSchema, appName, isUpdate, useMultiDatabase) {
    this.spinner.start({ text: 'Generating your files' });

    const dumpPromise = Promise.all(
      databasesSchema.map(databaseSchema =>
        this.dumper.dump(
          {
            appConfig: {
              appName,
              isUpdate,
              useMultiDatabase,
              modelsExportPath: this.path.relative('models', databaseSchema.modelsDir),
            },
            dbConfig: {
              dbDialect: databaseSchema.analyzerOptions.dbDialect,
              dbSchema: databaseSchema.analyzerOptions.dbSchema,
            },
          },
          databaseSchema.schema,
        ),
      ),
    );
    return this.spinner.attachToPromise(dumpPromise);
  }

  _warnIfSingleToMulti(outputDirectory, useMultiDatabase) {
    const fromSingleToMultipleDatabases =
      !outputDirectory && useMultiDatabase && !this.dumper.hasMultipleDatabaseStructure();
    if (fromSingleToMultipleDatabases) {
      this.logger.warn('It looks like you are switching from a single to a multiple databases.');
      this.logger.log(
        'You will need to move the models files from your existing database to' +
          ' the dedicated folder, or simply remove them.',
      );
    }
  }

  async _update({ isUpdate, outputDirectory, dbSchema, dbConfigPath }) {
    this.dumper.checkLianaCompatiblityForUpdate();
    this._assertOutputDirectory(outputDirectory);
    const databasesConfig = this._getDatabasesConfig(dbConfigPath);
    const databasesConnection = await this._connectToDatabases(databasesConfig);
    const databasesSchema = await this._analyzeDatabases(databasesConnection, dbSchema);
    await this._disconnectFromDatabases(databasesConnection);
    const useMultiDatabase = databasesSchema.length > 1;

    await this._dumpSchemas(databasesSchema, outputDirectory, isUpdate, useMultiDatabase);

    this._warnIfSingleToMulti(outputDirectory, useMultiDatabase);
  }

  async update(options) {
    try {
      await this._update(options);
    } catch (error) {
      await this.errorHandler.handle(error);
    }
  }
};