import { getDatabase } from "@vostro/c2-utils/lib/database"; import {DownAction, SequelizeStorage, Umzug} from "umzug"; import fs from "fs"; import { C2Engine } from "@vostro/c2-engine/lib/types"; import { promisify } from "util"; import { C2Utils } from "@vostro/c2-utils/lib/types"; // import { QueryTypes } from "sequelize"; import glob from "glob"; import path from "path"; import logger from '@vostro/c2-utils/lib/logger'; const log = logger("c2-module-core::migrate"); const globAsync = promisify(glob); const readFileAsync = promisify(fs.readFile); const existsAsync = promisify(fs.exists); export async function createMigrator(settings: C2Engine.ApplicationSettings, appContext: C2Engine.CoreContext, cfg: C2Engine.Config, options: C2Engine.MigratorConfig) { const sequelize = await getDatabase(); function getModule(name: string) { return (appContext?.engine?.modules || {})[name] } function moduleExists(name: string) { return !!getModule(name); } function runQuery(moduleName: string, sql: string, options?: any) { if(moduleExists(moduleName)) { return sequelize.query(sql, options); } } async function runQueryFile(moduleName: string, file: string, options?: any) { if(moduleExists(moduleName)) { const sql = await readFileAsync(file, {encoding: "utf-8"}); return sequelize.query(sql, options); } } const storage = new SequelizeStorage({ sequelize }); const alreadyComplete = await storage.executed(); const migrationObj: any = {}; alreadyComplete.forEach((name: any) => { migrationObj[name] = { name, up() {}, down(){}, } }); const availableMigrations = await globAsync("**/*.{js,ts,up.sql}", { cwd: options.path, }); await Promise.all(availableMigrations.map(async(p: string) => { const target = path.resolve(options.path, p); const relative = path.relative(__dirname, target); const dirname = path.dirname(p); const basename = path.basename(p, path.extname(p)); const name = path.join(dirname, basename); if (!p?.endsWith(".sql")) { const m = require(relative); migrationObj[p] = { dependencies: m.dependencies || [], up(...args: any[]) { if (options.fake) { return; } if(m.up) { return m.up(...args); } }, down(...args: any[]) { if (options.fake) { return; } if(m.down) { return m.down(...args); } }, name, }; } else { migrationObj[p] = { dependencies: [], async up() { if (options.fake) { return; } if (await existsAsync(target)) { const sql = await readFileAsync(target, {encoding: "utf8"}); return sequelize.query(sql) } }, async down() { if (options.fake) { return; } const newPath = target.replace(".up.sql", ".down.sql") if (await existsAsync(newPath)) { const sql = await readFileAsync(newPath, {encoding: "utf8"}); return sequelize.query(sql) } }, name, }; } })); const migrationList = depSort(migrationObj, log); const migrator = new Umzug({ storage, context: { options, sequelize, app: { config: cfg, context: appContext, settings }, getModule, moduleExists, runQuery, runQueryFile, } as C2Engine.MigratorContext, logger: console, migrations: migrationList.map((name) => migrationObj[name]), }); return migrator } function depSort(modules: {[key: string]: {dependencies: string[]}}, log: C2Utils.Logger) { let moduleList: string[] = []; let complete; let loop = 0; do { complete = true; loop++; if (loop > 50) { throw new Error("Unable to load modules - missing dependency"); } Object.keys(modules).forEach((moduleName) => { //eslint-disable-line const mod = modules[moduleName]; if (moduleList.indexOf(moduleName) === -1) { const depCheck = (mod.dependencies || []).filter((d: string) => { return moduleList.indexOf(d) === -1; }); if (depCheck.length > 0) { log.info(`unable to add module - ${moduleName} - waiting on`, {depCheck, module: mod.dependencies, moduleList}); complete = false; return; } log.info(`adding - ${moduleName}`); moduleList.push(moduleName); } }); } while (!complete); return moduleList; }