|
| 1 | +import glob from "fast-glob"; |
| 2 | +import { readFile } from "node:fs/promises"; |
| 3 | +import { parse, resolve, sep } from "node:path"; |
| 4 | +import type { MigrationParams, RunnableMigration } from "umzug"; |
| 5 | +import { MongoDBStorage, Umzug, UmzugCLI } from "umzug"; |
| 6 | +import { MigrationContext, MigratorType } from "@scripts/common/cli.types"; |
| 7 | +import { Logger } from "@core/logger/winston.logger"; |
| 8 | +import mongoService from "@backend/common/services/mongo.service"; |
| 9 | + |
| 10 | +const logger = Logger("scripts.commands.migrations"); |
| 11 | + |
| 12 | +async function migrations( |
| 13 | + context: MigrationContext, |
| 14 | +): Promise<Array<RunnableMigration<MigrationContext>>> { |
| 15 | + const { unsafe } = context; |
| 16 | + const folder = `${context.migratorType.toLowerCase()}s`; |
| 17 | + const migrationsRoot = resolve(__dirname, "..", folder); |
| 18 | + const unsafeText = unsafe ? "unsafe " : ""; |
| 19 | + const fileGlob = `${migrationsRoot}/**/*.{ts,js}`; |
| 20 | + const files = glob.sync(fileGlob, { absolute: true }); |
| 21 | + |
| 22 | + const migrations = await Promise.all( |
| 23 | + files.map(async (path) => { |
| 24 | + const { default: Migration } = (await import(path ?? "")) as { |
| 25 | + default: { new (): RunnableMigration<MigrationContext> }; |
| 26 | + }; |
| 27 | + |
| 28 | + const migration = new Migration(); |
| 29 | + const name = parse(path).name; |
| 30 | + |
| 31 | + return { |
| 32 | + name, |
| 33 | + path, |
| 34 | + up: async ( |
| 35 | + params: MigrationParams<MigrationContext>, |
| 36 | + ): Promise<void> => { |
| 37 | + const { logger } = params.context; |
| 38 | + |
| 39 | + logger.debug(`Running ${unsafeText}up migration(${name})`); |
| 40 | + logger.debug(path); |
| 41 | + |
| 42 | + await migration.up(params); |
| 43 | + |
| 44 | + logger.debug(`Up migration(${name}) run successful`); |
| 45 | + }, |
| 46 | + down: async ( |
| 47 | + params: MigrationParams<MigrationContext>, |
| 48 | + ): Promise<void> => { |
| 49 | + if (!migration.down) return; |
| 50 | + |
| 51 | + const { logger } = params.context; |
| 52 | + |
| 53 | + logger.debug(`Running ${unsafeText}down migration(${name})`); |
| 54 | + logger.debug(path); |
| 55 | + |
| 56 | + await migration.down(params); |
| 57 | + |
| 58 | + logger.debug(`Down migration(${name}) ran successful`); |
| 59 | + }, |
| 60 | + }; |
| 61 | + }), |
| 62 | + ); |
| 63 | + |
| 64 | + return migrations.sort((prev, next) => |
| 65 | + prev.path.split("/").pop()!.localeCompare(next.path.split("/").pop()!), |
| 66 | + ); |
| 67 | +} |
| 68 | + |
| 69 | +async function template({ |
| 70 | + migratorType, |
| 71 | + filePath, |
| 72 | + migrationsRoot, |
| 73 | +}: { |
| 74 | + migratorType: MigratorType; |
| 75 | + filePath: string; |
| 76 | + migrationsRoot: string; |
| 77 | +}): Promise<[string, string][]> { |
| 78 | + const { base, name } = parse(filePath); |
| 79 | + const path = resolve(migrationsRoot, base); |
| 80 | + |
| 81 | + return readFile( |
| 82 | + resolve(migrationsRoot, "..", "common", "migrator-template.ts"), |
| 83 | + ).then((contents): [string, string][] => [ |
| 84 | + [ |
| 85 | + path, |
| 86 | + contents |
| 87 | + .toString() |
| 88 | + .replace("class Template", `class ${migratorType}`) |
| 89 | + .replace("{{name}}", name.split(".").pop()!) |
| 90 | + .replace("{{path}}", path.replace(`${migrationsRoot}${sep}`, "")), |
| 91 | + ], |
| 92 | + ]); |
| 93 | +} |
| 94 | + |
| 95 | +async function createMigrationCli( |
| 96 | + migratorType: MigratorType, |
| 97 | +): Promise<UmzugCLI> { |
| 98 | + const folder = `${migratorType.toLowerCase()}s`; |
| 99 | + const migrationsRoot = resolve(__dirname, "..", folder); |
| 100 | + const collection = mongoService.db.collection(folder); |
| 101 | + const storage = new MongoDBStorage({ collection }); |
| 102 | + |
| 103 | + const umzug = new Umzug<MigrationContext>({ |
| 104 | + storage, |
| 105 | + logger: undefined, |
| 106 | + migrations, |
| 107 | + create: { |
| 108 | + folder: migrationsRoot, |
| 109 | + template: (filePath) => |
| 110 | + template({ migratorType, filePath, migrationsRoot }), |
| 111 | + }, |
| 112 | + context: async (): Promise<MigrationContext> => { |
| 113 | + const unsafe = cli.getFlagParameter("--unsafe").value; |
| 114 | + |
| 115 | + return { logger, migratorType, unsafe }; |
| 116 | + }, |
| 117 | + }); |
| 118 | + |
| 119 | + const cli = new UmzugCLI(umzug as Umzug<object>, { |
| 120 | + toolDescription: "Compass migrator", |
| 121 | + toolFileName: "cli.ts", |
| 122 | + }); |
| 123 | + |
| 124 | + cli.defineFlagParameter({ |
| 125 | + parameterLongName: "--unsafe", |
| 126 | + parameterShortName: "-u", |
| 127 | + description: "Run unsafe migration code within up and down methods", |
| 128 | + environmentVariable: "MIGRATION_UNSAFE", |
| 129 | + }); |
| 130 | + |
| 131 | + return cli; |
| 132 | +} |
| 133 | + |
| 134 | +export const runMigrator = async ( |
| 135 | + migratorType: MigratorType, |
| 136 | + useDynamicDb = false, |
| 137 | +): Promise<void> => { |
| 138 | + try { |
| 139 | + await mongoService.start(useDynamicDb); |
| 140 | + |
| 141 | + const cli = await createMigrationCli(migratorType); |
| 142 | + |
| 143 | + await cli.executeAsync(process.argv.slice(3)); |
| 144 | + |
| 145 | + await mongoService.stop(); |
| 146 | + |
| 147 | + process.exit(0); |
| 148 | + } catch (error) { |
| 149 | + logger.error(error); |
| 150 | + |
| 151 | + process.exit(1); |
| 152 | + } |
| 153 | +}; |
0 commit comments