From 2973b9f146ccf4a9a61e4921700708d55731f280 Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Thu, 13 Jan 2022 12:57:31 -0500 Subject: [PATCH] feat: add local test mirror db and safe db mirror utility --- package.json | 2 +- src/config/config.cjs | 4 + src/models/co-benefits/co-benefits.model.js | 16 ++-- .../co-benefits/co-benefits.model.mirror.js | 6 +- src/models/database.js | 16 +++- src/models/locations/locations.model.js | 16 ++-- .../locations/locations.model.mirror.js | 6 +- src/models/projects/projects.model.js | 16 ++-- src/models/projects/projects.model.mirror.js | 6 +- .../qualifications/qualifications.model.js | 23 ++++-- .../qualifications.model.mirror.js | 6 +- src/models/ratings/ratings.model.js | 16 ++-- src/models/ratings/ratings.model.mirror.js | 6 +- src/models/units/units.model.js | 74 +++++++++--------- src/models/units/units.model.mirror.js | 6 +- src/models/vintages/vintages.model.js | 16 ++-- src/models/vintages/vintages.model.mirror.js | 6 +- testMirror.sqlite3 | Bin 0 -> 159744 bytes 18 files changed, 143 insertions(+), 98 deletions(-) create mode 100644 testMirror.sqlite3 diff --git a/package.json b/package.json index 281566c8..fb41b37b 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "release": "./node_modules/.bin/standard-version && git push --tags", "postinstall": "npm run requirements-check", "resetDb": "rm -f ./data.sqlite3 && npx sequelize-cli db:migrate --env local && npx sequelize-cli db:seed:all --debug --env local", - "resetTestDb": "rm -f ./test.sqlite3 && npx sequelize-cli db:migrate --env test && npx sequelize-cli db:seed:all --debug --env test", + "resetTestDb": "rm -f ./test.sqlite3 && npx sequelize-cli db:migrate --env test && npx sequelize-cli db:seed:all --debug --env test && rm -f ./testMirror.sqlite3 && npx sequelize-cli db:migrate --env mirrorTest", "resetMirrorDb": "npx sequelize-cli db:drop --env mirror && npx sequelize-cli db:create --env mirror && npx sequelize-cli db:migrate --env mirror --debug" }, "dependencies": { diff --git a/src/config/config.cjs b/src/config/config.cjs index 93777c3d..f1b5456c 100644 --- a/src/config/config.cjs +++ b/src/config/config.cjs @@ -9,6 +9,10 @@ module.exports = { dialect: 'sqlite', storage: './test.sqlite3', }, + mirrorTest: { + dialect: 'sqlite', + storage: './testMirror.sqlite3', + }, mirror: { username: process.env.DB_USERNAME, password: process.env.DB_PASSWORD, diff --git a/src/models/co-benefits/co-benefits.model.js b/src/models/co-benefits/co-benefits.model.js index 46d564b1..77752cc5 100644 --- a/src/models/co-benefits/co-benefits.model.js +++ b/src/models/co-benefits/co-benefits.model.js @@ -3,7 +3,7 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; import { CoBenefitMirror } from './co-benefits.model.mirror'; -import { sequelize } from '../database'; +import { sequelize, safeMirrorDbHandler } from '../database'; import { Project } from '../projects'; import ModelTypes from './co-benifets.modeltypes.cjs'; @@ -15,26 +15,28 @@ class CoBenefit extends Model { foreignKey: 'projectId', }); - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { CoBenefitMirror.belongsTo(Project, { onDelete: 'CASCADE', targetKey: 'warehouseProjectId', foreignKey: 'projectId', }); - } + }); } static async create(values, options) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { CoBenefitMirror.create(values, options); - } + }); + return super.create(values, options); } static async destroy(values) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { CoBenefitMirror.destroy(values); - } + }); + return super.destroy(values); } } diff --git a/src/models/co-benefits/co-benefits.model.mirror.js b/src/models/co-benefits/co-benefits.model.mirror.js index 9acc63db..7e31e799 100644 --- a/src/models/co-benefits/co-benefits.model.mirror.js +++ b/src/models/co-benefits/co-benefits.model.mirror.js @@ -3,16 +3,16 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelizeMirror } from '../database'; +import { sequelizeMirror, safeMirrorDbHandler } from '../database'; import ModelTypes from './co-benifets.modeltypes.cjs'; class CoBenefitMirror extends Model {} -if (process.env.DB_USE_MIRROR === 'true') { +safeMirrorDbHandler(() => { CoBenefitMirror.init(ModelTypes, { sequelize: sequelizeMirror, modelName: 'coBenefit', }); -} +}); export { CoBenefitMirror }; diff --git a/src/models/database.js b/src/models/database.js index 72a0bb4f..6d4d9f12 100644 --- a/src/models/database.js +++ b/src/models/database.js @@ -5,4 +5,18 @@ dotenv.config(); // possible values: local, test export const sequelize = new Sequelize(config[process.env.NODE_ENV]); -export const sequelizeMirror = new Sequelize(config['mirror']); + +const mirrorConfig = process.env.NODE_ENV === 'local' ? 'mirror' : 'mirrorTest'; +export const sequelizeMirror = new Sequelize(config[mirrorConfig]); + +export const safeMirrorDbHandler = (callback) => { + sequelizeMirror + .authenticate() + .then(() => { + console.log('Mirror DB connected'); + callback(); + }) + .catch((err) => { + console.log('Mirror DB not connected'); + }); +}; diff --git a/src/models/locations/locations.model.js b/src/models/locations/locations.model.js index 4181b00f..4cb26ce0 100644 --- a/src/models/locations/locations.model.js +++ b/src/models/locations/locations.model.js @@ -3,7 +3,7 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelize } from '../database'; +import { sequelize, safeMirrorDbHandler } from '../database'; import { Project } from '../projects'; import ModelTypes from './locations.modeltypes.cjs'; @@ -17,26 +17,28 @@ class ProjectLocation extends Model { foreignKey: 'projectId', }); - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { ProjectLocationMirror.belongsTo(Project, { onDelete: 'CASCADE', targetKey: 'warehouseProjectId', foreignKey: 'projectId', }); - } + }); } static async create(values, options) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { ProjectLocationMirror.create(values, options); - } + }); + return super.create(values, options); } static async destroy(values) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { ProjectLocationMirror.destroy(values); - } + }); + return super.destroy(values); } } diff --git a/src/models/locations/locations.model.mirror.js b/src/models/locations/locations.model.mirror.js index 4d9ff494..38229625 100644 --- a/src/models/locations/locations.model.mirror.js +++ b/src/models/locations/locations.model.mirror.js @@ -3,16 +3,16 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelizeMirror } from '../database'; +import { sequelizeMirror, safeMirrorDbHandler } from '../database'; import ModelTypes from './locations.modeltypes.cjs'; class ProjectLocationMirror extends Model {} -if (process.env.DB_USE_MIRROR === 'true') { +safeMirrorDbHandler(() => { ProjectLocationMirror.init(ModelTypes, { sequelize: sequelizeMirror, modelName: 'projectLocation', }); -} +}); export { ProjectLocationMirror }; diff --git a/src/models/projects/projects.model.js b/src/models/projects/projects.model.js index fd2542ef..768bc983 100644 --- a/src/models/projects/projects.model.js +++ b/src/models/projects/projects.model.js @@ -4,7 +4,7 @@ import Sequelize from 'sequelize'; import rxjs from 'rxjs'; const { Model } = Sequelize; -import { sequelize } from '../database'; +import { sequelize, safeMirrorDbHandler } from '../database'; import { RelatedProject, @@ -28,19 +28,20 @@ class Project extends Model { Project.hasMany(CoBenefit); Project.hasMany(RelatedProject); - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { ProjectMirror.hasMany(ProjectLocation); ProjectMirror.hasMany(Qualification); ProjectMirror.hasMany(Vintage); ProjectMirror.hasMany(CoBenefit); ProjectMirror.hasMany(RelatedProject); - } + }); } static async create(values, options) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { ProjectMirror.create(values, options); - } + }); + const createResult = await super.create(values, options); const { orgUid } = values; @@ -51,9 +52,10 @@ class Project extends Model { } static async destroy(values) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { ProjectMirror.destroy(values); - } + }); + const record = await super.findOne(values.where); const { orgUid } = record.dataValues; diff --git a/src/models/projects/projects.model.mirror.js b/src/models/projects/projects.model.mirror.js index e7fc15fd..324835ec 100644 --- a/src/models/projects/projects.model.mirror.js +++ b/src/models/projects/projects.model.mirror.js @@ -3,17 +3,17 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelizeMirror } from '../database'; +import { sequelizeMirror, safeMirrorDbHandler } from '../database'; import ModelTypes from './projects.modeltypes.cjs'; class ProjectMirror extends Model {} -if (process.env.DB_USE_MIRROR === 'true') { +safeMirrorDbHandler(() => { ProjectMirror.init(ModelTypes, { sequelize: sequelizeMirror, modelName: 'project', foreignKey: 'projectId', }); -} +}); export { ProjectMirror }; diff --git a/src/models/qualifications/qualifications.model.js b/src/models/qualifications/qualifications.model.js index 97a32853..a62f550d 100644 --- a/src/models/qualifications/qualifications.model.js +++ b/src/models/qualifications/qualifications.model.js @@ -1,7 +1,7 @@ 'use strict'; import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelize } from '../database'; +import { sequelize, safeMirrorDbHandler } from '../database'; import { Project } from '../projects'; import { Unit } from '../units'; @@ -23,19 +23,32 @@ class Qualification extends Model { through: 'qualification_unit', as: 'unit', }); + + safeMirrorDbHandler(() => { + QualificationMirror.belongsTo(Project, { + targetKey: 'warehouseProjectId', + foreignKey: 'projectId', + }); + + QualificationMirror.belongsToMany(Unit, { + through: 'qualification_unit', + as: 'unit', + }); + }); } static async create(values, options) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { QualificationMirror.create(values, options); - } + }); + return super.create(values, options); } static async destroy(values) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { QualificationMirror.destroy(values); - } + }); return super.destroy(values); } } diff --git a/src/models/qualifications/qualifications.model.mirror.js b/src/models/qualifications/qualifications.model.mirror.js index 395aad05..1937fd3c 100644 --- a/src/models/qualifications/qualifications.model.mirror.js +++ b/src/models/qualifications/qualifications.model.mirror.js @@ -3,17 +3,17 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelizeMirror } from '../database'; +import { sequelizeMirror, safeMirrorDbHandler } from '../database'; import ModelTypes from './qualifications.modeltypes.cjs'; class QualificationMirror extends Model {} -if (process.env.DB_USE_MIRROR === 'true') { +safeMirrorDbHandler(() => { QualificationMirror.init(ModelTypes, { sequelize: sequelizeMirror, modelName: 'qualification', foreignKey: 'qualificationId', }); -} +}); export { QualificationMirror }; diff --git a/src/models/ratings/ratings.model.js b/src/models/ratings/ratings.model.js index 82343124..accecef4 100644 --- a/src/models/ratings/ratings.model.js +++ b/src/models/ratings/ratings.model.js @@ -1,7 +1,7 @@ 'use strict'; import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelize } from '../database'; +import { sequelize, safeMirrorDbHandler } from '../database'; import { Project } from '../projects/index'; import ModelTypes from './ratings.modeltypes.cjs'; @@ -15,26 +15,28 @@ class Rating extends Model { foreignKey: 'projectId', }); - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { RatingMirror.belongsTo(Project, { onDelete: 'CASCADE', targetKey: 'warehouseProjectId', foreignKey: 'projectId', }); - } + }); } static async create(values, options) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { RatingMirror.create(values, options); - } + }); + return super.create(values, options); } static async destroy(values) { - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { RatingMirror.destroy(values); - } + }); + return super.destroy(values); } } diff --git a/src/models/ratings/ratings.model.mirror.js b/src/models/ratings/ratings.model.mirror.js index ec8562c5..f8273640 100644 --- a/src/models/ratings/ratings.model.mirror.js +++ b/src/models/ratings/ratings.model.mirror.js @@ -3,16 +3,16 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelizeMirror } from '../database'; +import { sequelizeMirror, safeMirrorDbHandler } from '../database'; import ModelTypes from './ratings.modeltypes.cjs'; class RatingMirror extends Model {} -if (process.env.DB_USE_MIRROR === 'true') { +safeMirrorDbHandler(() => { RatingMirror.init(ModelTypes, { sequelize: sequelizeMirror, modelName: 'projectRating', }); -} +}); export { RatingMirror }; diff --git a/src/models/units/units.model.js b/src/models/units/units.model.js index acfc1a56..50fc24b5 100644 --- a/src/models/units/units.model.js +++ b/src/models/units/units.model.js @@ -1,7 +1,7 @@ 'use strict'; import Sequelize from 'sequelize'; -import { sequelize } from '../database'; +import { sequelize, safeMirrorDbHandler } from '../database'; import { Qualification, Vintage } from '../../models'; import { UnitMirror } from './units.model.mirror'; import ModelTypes from './units.modeltypes.cjs'; @@ -60,7 +60,7 @@ class Unit extends Model { as: 'qualifications', }); - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { UnitMirror.hasOne(Vintage); // https://gist.github.com/elliette/20ddc4e827efd9d62bc98752e7a62610#some-important-addendums @@ -68,13 +68,11 @@ class Unit extends Model { through: 'qualification_unit', as: 'qualifications', }); - } + }); } static async create(values, options) { - if (process.env.DB_USE_MIRROR === 'true') { - UnitMirror.create(values, options); - } + safeMirrorDbHandler(() => UnitMirror.create(values, options)); const createResult = await super.create(values, options); const { orgUid } = createResult; @@ -85,9 +83,8 @@ class Unit extends Model { } static async destroy(values) { - if (process.env.DB_USE_MIRROR === 'true') { - UnitMirror.destroy(values); - } + safeMirrorDbHandler(() => UnitMirror.destroy(values)); + const record = await super.findOne(values.where); const { orgUid } = record.dataValues; @@ -95,15 +92,15 @@ class Unit extends Model { return super.destroy(values); } - + static async fts(searchStr, orgUid, pagination, columns = []) { const dialect = sequelize.getDialect(); - + const handlerMap = { sqlite: Unit.findAllSqliteFts, mysql: Unit.findAllMySQLFts, }; - + // Check if we need to include the virtual field dep for (const col of Object.keys(virtualColumns)) { if (columns.includes(col)) { @@ -113,29 +110,32 @@ class Unit extends Model { break; } } - + return handlerMap[dialect]( searchStr, orgUid, pagination, - columns - .filter((col) => ![ - 'createdAt', - 'updatedAt', - 'unitBlockStart', - 'unitBlockEnd', - 'unitCount'].includes(col)) + columns.filter( + (col) => + ![ + 'createdAt', + 'updatedAt', + 'unitBlockStart', + 'unitBlockEnd', + 'unitCount', + ].includes(col), + ), ); } - + static async findAllMySQLFts(searchStr, orgUid, pagination, columns = []) { const { offset, limit } = pagination; - + let fields = '*'; if (columns.length) { fields = columns.join(', '); } - + let sql = ` SELECT ${fields} FROM units WHERE MATCH ( unitOwnerOrgUid, @@ -159,13 +159,13 @@ class Unit extends Model { correspondingAdjustmentStatus ) AGAINST ":search" `; - + if (orgUid) { sql = `${sql} AND orgUid = :orgUid`; } - + const replacements = { search: searchStr, orgUid }; - + const count = ( await sequelize.query(sql, { model: Unit, @@ -173,11 +173,11 @@ class Unit extends Model { replacements, }) ).length; - + if (limit && offset) { sql = `${sql} ORDER BY relevance DESC LIMIT :limit OFFSET :offset`; } - + return { count, rows: await sequelize.query(sql, { @@ -189,25 +189,25 @@ class Unit extends Model { }), }; } - + static async findAllSqliteFts(searchStr, orgUid, pagination, columns = []) { const { offset, limit } = pagination; - + let fields = '*'; if (columns.length) { fields = columns.join(', '); } - + searchStr = searchStr = searchStr.replaceAll('-', '+'); - + let sql = `SELECT ${fields} FROM units_fts WHERE units_fts MATCH :search`; - + if (orgUid) { sql = `${sql} AND orgUid = :orgUid`; } - + const replacements = { search: `${searchStr}*`, orgUid }; - + const count = ( await sequelize.query(sql, { model: Unit, @@ -215,11 +215,11 @@ class Unit extends Model { replacements, }) ).length; - + if (limit && offset) { sql = `${sql} ORDER BY rank DESC LIMIT :limit OFFSET :offset`; } - + return { count, rows: await sequelize.query(sql, { diff --git a/src/models/units/units.model.mirror.js b/src/models/units/units.model.mirror.js index 93be5920..94ea738e 100644 --- a/src/models/units/units.model.mirror.js +++ b/src/models/units/units.model.mirror.js @@ -2,17 +2,17 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelizeMirror } from '../database'; +import { sequelizeMirror, safeMirrorDbHandler } from '../database'; import ModelTypes from './units.modeltypes.cjs'; class UnitMirror extends Model {} -if (process.env.DB_USE_MIRROR === 'true') { +safeMirrorDbHandler(() => { UnitMirror.init(ModelTypes, { sequelize: sequelizeMirror, modelName: 'unit', foreignKey: 'unitId', }); -} +}); export { UnitMirror }; diff --git a/src/models/vintages/vintages.model.js b/src/models/vintages/vintages.model.js index aac5dfd2..02f9d7d2 100644 --- a/src/models/vintages/vintages.model.js +++ b/src/models/vintages/vintages.model.js @@ -1,7 +1,7 @@ 'use strict'; import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelize } from '../database'; +import { sequelize, safeMirrorDbHandler } from '../database'; import { Project, Unit } from '../../models'; import ModelTypes from './vintages.modeltypes.cjs'; @@ -18,7 +18,7 @@ class Vintage extends Model { foreignKey: 'unitId', }); - if (process.env.DB_USE_MIRROR === 'true') { + safeMirrorDbHandler(() => { VintageMirror.belongsTo(Project, { targetKey: 'warehouseProjectId', foreignKey: 'projectId', @@ -27,16 +27,22 @@ class Vintage extends Model { targetKey: 'warehouseUnitId', foreignKey: 'unitId', }); - } + }); } static async create(values, options) { - VintageMirror.create(values, options); + safeMirrorDbHandler(() => { + VintageMirror.create(values, options); + }); + return super.create(values, options); } static async destroy(values) { - VintageMirror.destroy(values); + safeMirrorDbHandler(() => { + VintageMirror.destroy(values); + }); + return super.destroy(values); } } diff --git a/src/models/vintages/vintages.model.mirror.js b/src/models/vintages/vintages.model.mirror.js index 33e64b30..47a6974a 100644 --- a/src/models/vintages/vintages.model.mirror.js +++ b/src/models/vintages/vintages.model.mirror.js @@ -3,16 +3,16 @@ import Sequelize from 'sequelize'; const { Model } = Sequelize; -import { sequelizeMirror } from '../database'; +import { sequelizeMirror, safeMirrorDbHandler } from '../database'; import ModelTypes from './vintages.modeltypes.cjs'; class VintageMirror extends Model {} -if (process.env.DB_USE_MIRROR === 'true') { +safeMirrorDbHandler(() => { VintageMirror.init(ModelTypes, { sequelize: sequelizeMirror, modelName: 'vintage', }); -} +}); export { VintageMirror }; diff --git a/testMirror.sqlite3 b/testMirror.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..af9b9fdf8b32e28271ed534e36c4412ce4023297 GIT binary patch literal 159744 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 literal 0 HcmV?d00001