Source: services/zDatabaseService.ts

import { zConfigDB } from '../configs';
import { DataType, DataTypes, Dialect, Model, ModelCtor, Sequelize } from 'sequelize';
import { concat, from, Observable, of, throwError } from 'rxjs';
import { zIAttributeDB, zIAttributeObjectDB, zIConfigDB, zIFieldDB, zITableDB } from '../interfaces';
import { catchError, delay, map, retryWhen, switchMap, tap, toArray } from 'rxjs/operators';
import { zEFieldTypeDB } from '../enums';
import { zTranslateService } from './zTranslateService';

/**
 * Service that contains the functions related to the database.
 * @author Ivan Antunes <ivanantnes75@gmail.com>
 * @copyright Ivan Antunes 2021
 */
export class zDatabaseService {

  /**
   * Stores zDatabaseService Service Instance.
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private static instance: zDatabaseService | null;

  /**
   * Sequelize instance containing the database connection.
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private connection: Sequelize | null = null;

  /**
   * Stores if initialized service.
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private isInitialized = false;

  /**
   * Stores instance zTranslateService.
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private tService = zTranslateService.getInstance();

  /**
   * Executes the database connection function and stores it.
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private constructor() {
    this.connectionDatabase(zConfigDB).subscribe((con) => {
      this.connection = con;
      console.log(this.tService.t('lbl_db_connection'));
      this.isInitialized = true;
    }, (err) => {
      throw new Error(`$${this.tService.t('lbl_db_fail_connection')} ${err}`);
    });
  }

  /**
   * Function used to get instance of zDatabaseService
   * @returns zDatabaseService
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  public static getInstance(): zDatabaseService {

    if (!zDatabaseService.instance) {
      zDatabaseService.instance = new zDatabaseService();
    }

    return zDatabaseService.instance;

  }

  /**
   * Function used to destroy instance of zDatabaseService
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  public static destroyInstance(): void {

    zDatabaseService.instance = null;

  }

  /**
   * Function to take the selected bank type in addition to checking if it is valid.
   * @param {string} dialect - Database type.
   * @returns Observable<Dialect>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private getDialectDatabase(dialect: string): Observable<Dialect> {

    switch (dialect) {
      case 'mysql':
        return of('mysql');
      case 'postgres':
        return of('postgres');
      case 'mariadb':
        return of('mariadb');
      case 'mssql':
        return of('mssql');
      default:
        return throwError(this.tService.t('lbl_db_fail_dialect'));
    }

  }

  /**
   * Function responsible for connecting to the database and testing the connection.
   * @param {zIConfigDB} config - Database Configs
   * @returns Observable<Sequelize>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private connectionDatabase(config: zIConfigDB): Observable<Sequelize> {

    return this.getDialectDatabase(config.DB_DIALECT).pipe(
      switchMap((currentDialect) => {
        return of(new Sequelize({
          database: config.DB_NAME,
          username: config.DB_USER,
          host: config.DB_HOST,
          dialect: currentDialect,
          password: config.DB_PASSWORD,
          port: config.DB_PORT,
        })).pipe(
          switchMap((con) => from(con.authenticate()).pipe(
            switchMap(() => of(con))
          ))
        );
      }));

  }

  /**
   * Function to get field type.
   * @param {zIFieldDB} field - Database Field
   * @returns Observable<DataType>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private getFieldType(field: zIFieldDB): Observable<DataType> {
    switch (field.fieldType) {
      case zEFieldTypeDB.INT:
        return of(
          DataTypes.INTEGER({
            length: field.fieldSize,
            precision: field.fieldPrecision
          })
        );
      case zEFieldTypeDB.BIGINT:
        return of(
          DataTypes.BIGINT({
            length: field.fieldSize
          })
        );
      case zEFieldTypeDB.VARCHAR:
        return of(
          DataTypes.STRING({
            length: field.fieldSize
          })
        );
      case zEFieldTypeDB.TEXT:
        return of(
          DataTypes.TEXT({
            length: field.fieldTextLength,
          })
        );
      case zEFieldTypeDB.FLOAT:
        return of(
          DataTypes.FLOAT({
            length: field.fieldSize,
            decimals: field.fieldPrecision
          })
        );
      case zEFieldTypeDB.BOOLEAN:
        return of(DataTypes.BOOLEAN);
      case zEFieldTypeDB.TIME:
        return of(DataTypes.TIME);
      case zEFieldTypeDB.DATE:
        return of(DataTypes.DATE({
          length: field.fieldSize
        }));
      case zEFieldTypeDB.ENUM:
        if (!field.fieldEnumValue) {
          return throwError(this.tService.t('lbl_db_fail_enum_type'));
        }

        return of(DataTypes.ENUM(...field.fieldEnumValue as string[]));
      case zEFieldTypeDB.BLOB:
        return of(DataTypes.BLOB({
          length: field.fieldTextLength
        }));
      default:
        return throwError(this.tService.t('lbl_db_fail_type_support'));
    }
  }

  /**
   * Function to generate attributes table.
   * @param {zITableDB | zIFieldDB} data - Table Generate Attributes | Field Generate Attributes
   * @returns Observable<zIAttributeDB[]>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private generateAttribute(data?: zITableDB | zIFieldDB): Observable<zIAttributeObjectDB | zIAttributeDB[]> {

    if (data) {
      if (Object.prototype.constructor(data).fieldName) {
        const field = data as zIFieldDB;

        return this.getFieldType(field).pipe(

          switchMap((fieldType) => {

            const baseAttribute: zIAttributeObjectDB = {
              type: fieldType,
              primaryKey: field.fieldPrimaryKey,
              autoIncrement: field.fieldAutoIncrement,
              allowNull: field.fieldAllowNull ? field.fieldAllowNull : false,
              validate: field.fieldValidate,
              defaultValue: field.fieldDefaultValue,
              unique: field.fieldUnique
            };

            if (field.fieldRelation) {
              baseAttribute.references = {
                model: field.fieldRelation.tableName,
                key: field.fieldRelation.fieldName
              };
            }

            return of(baseAttribute);
          })

        );
      }

      if (Object.prototype.constructor(data).tableName) {
        const table = data as zITableDB;

        return concat(...table.tableFields.map((f) => this.getFieldType(f).pipe(

          switchMap((fieldType) => {

            const baseAttribute: zIAttributeDB = {

              [`${f.fieldName}`]: {
                type: fieldType,
                validate: f.fieldValidate,
                defaultValue: f.fieldDefaultValue,
                allowNull: f.fieldAllowNull ? f.fieldAllowNull : false,
                unique: f.fieldUnique,
                primaryKey: f.fieldPrimaryKey,
                autoIncrement: f.fieldAutoIncrement,
              }

            };

            if (f.fieldRelation) {
              baseAttribute[f.fieldName].references = {
                model: f.fieldRelation.tableName,
                key: f.fieldRelation.fieldName
              };
            }

            return of(baseAttribute);
          })

        ))).pipe(
          toArray(),
          map((attributes) => {
            return attributes;
          })
        );
      }

      return throwError(this.tService.t('lbl_db_fail_generate_attr'));

    }

    return throwError(this.tService.t('lbl_db_fail_generate_attr_not_defined'));

  }

  /**
   * Function to check field in table.
   * @param {string} tableName - Table Name.
   * @param {string} fieldName - Field Name.
   * @returns Observable<boolean>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private checkField(tableName: string, fieldName: string): Observable<boolean> {
    return this.getConnection().pipe(

      switchMap((con) => from(con.getQueryInterface().describeTable(tableName)).pipe(

        catchError((err) => {
          return throwError(`${this.tService.t('lbl_db_fail_check_field')} ${err}`);
        }),

        map((fields) => {
          const keys = Object.keys(fields);

          if (keys.find((key) => key === fieldName || key === fieldName.toLowerCase() || key === fieldName.toUpperCase())) {
            return true;
          } else {
            return false;
          }

        })
      ))

    );

  }

  /**
   * Function to check table in database.
   * @param {string} tableName - Table Name.
   * @returns Observable<boolean>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private checkTable(tableName: string): Observable<boolean> {

    return this.getConnection().pipe(

      switchMap((con) => from(con.getQueryInterface().showAllTables()).pipe(

        catchError((err) => {
          return throwError(`${this.tService.t('lbl_db_fail_check_table')} ${err}`);
        }),

        map((tables: any[]) => {

          if (tables.find((t: { tableName: string, schema: string }) =>
            t.tableName === tableName ||
            t.tableName === tableName.toLowerCase() ||
            t.tableName === tableName.toUpperCase()
          )) {
            return true;
          } else {
            return false;
          }

        })

      ))

    );

  }

  /**
   * Function used to set table model.
   * @param {zITableDB} table - Table.
   * @returns Observable<ModelCtor<Model<any, any>>>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  private setTableModel(table: zITableDB): Observable<ModelCtor<Model<any, any>>> {
    return this.getConnection().pipe(
      switchMap((con) => this.generateAttribute(table).pipe(

        switchMap((attribute) => {

          const model = con.define(
            table.tableName,
            Object.assign({}, ...(attribute as zIAttributeDB[]).map((attr) => attr)),
            {...table.tableOptions, timestamps: false, tableName: table.tableName}
          );

          table.tableFields.map((field) => {

            if (field.fieldRelation) {
              model.hasOne(
                con.models[field.fieldRelation.tableName],
                {
                  foreignKey: field.fieldRelation.fieldName,
                  sourceKey: field.fieldName
                }
              );
            }

          });

          return of(model);

        }))
    ));
  }

  /**
   * Function to pick up the connection.
   * @returns Observable<Sequelize>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  public getConnection(): Observable<Sequelize> {
    return (new Observable<Sequelize>((obs) => {
      if (this.isInitialized) {
        obs.next(this.connection as Sequelize);
        return obs.complete();
      } else {
        return obs.error(this.tService.t('sys_db_fail_service'));
      }
    })).pipe(
      catchError((err) => {
        console.log(err);
        return throwError(err);
      }),
      retryWhen((err) => err.pipe(
        tap(() => console.log(this.tService.t('sys_db_fail_service_refresh'))),
        delay(5000)
      ))
    );
  }

  /**
   * Function to create tables and add columns.
   * @param {zITableDB} table - Table.
   * @returns Observable<unknown>
   * @author Ivan Antunes <ivanantnes75@gmail.com>
   * @copyright Ivan Antunes 2021
   */
  public createTable(table: zITableDB): Observable<unknown> {
    return this.getConnection().pipe(

      switchMap((con) => this.checkTable(table.tableName).pipe(

        catchError((err) => {
          console.log(`${this.tService.t('gnc_internal_server_error')} ${err}`);
          return of(false);
        }),

        switchMap((isTable) => {

          if (table.tableLogicalDelete) {
            table.tableFields.push({
              fieldName: 'IS_DELETED',
              fieldPrimaryKey: false,
              fieldRequired: true,
              fieldType: zEFieldTypeDB.BOOLEAN,
              fieldDefaultValue: '0',
            });
          }


          console.log(`${this.tService.t('gnc_lbl_table')} ${table.tableName} ${this.tService.t('gnc_lbl_exists')}: ${isTable}`);

          if (isTable) {

            return concat(...table.tableFields.map((field) => this.checkField(table.tableName, field.fieldName).pipe(

              catchError((err) => {
                console.log(`${this.tService.t('gnc_internal_server_error')} ${err}`);
                return of(false);
              }),

              switchMap((isField) => {
                console.log(`${this.tService.t('gnc_lbl_field')} ${field.fieldName} ${this.tService.t('gnc_lbl_exists')}: ${isField}`);

                if (isField) {
                  return of(1);
                }

                return this.generateAttribute(field).pipe(
                  switchMap((attr) => from(con.getQueryInterface().addColumn(
                    table.tableName,
                    field.fieldName,
                    attr as zIAttributeObjectDB,
                    table.tableOptions
                  )).pipe(

                    catchError((err) => {
                      return throwError(`${this.tService.t('lbl_db_fail_create_field')} ${err}`);
                    }),

                    tap(() => console.log(`${this.tService.t('lbl_db_create_field')} ${field.fieldName}`))

                  ))
                );

              })

            ))).pipe(
              toArray()
            );

          }

          return from(this.generateAttribute(table).pipe(

            switchMap((attributes) => from(con.getQueryInterface().createTable(
              table.tableName,
              Object.assign({}, ...(attributes as zIAttributeDB[]).map((attr) => attr)),
              table.tableOptions
            )).pipe(

              catchError((err) => {
                return throwError(`${this.tService.t('lbl_db_fail_create_table')} ${err}`);
              }),

              tap(() => console.log(`${this.tService.t('lbl_db_create_table')} ${table.tableName}`))

            ))

          ));

        }),

        switchMap(() => this.setTableModel(table).pipe(

          tap(() => console.log(`${this.tService.t('lbl_db_model_defined')} ${table.tableName}`))

        ))

      )),
    );

  }
}