theBenForce/data-migration

View on GitHub
packages/data-migration/src/Utils.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import * as babel from "@babel/core";
import * as fs from "fs";
import * as path from "path";

import { MigrationScript, ScriptContext } from "./";

import Configuration from "./Config";
import { MissingParameters } from "./Errors";
import { getMigrationsPath } from "./methods";
import { Logger } from "./Logger";

import * as glob from "fast-glob";

export function checkParameters(
  label: string,
  requiredParams: Array<string>,
  params: { [key: string]: any }
) {
  const missingParams = requiredParams.filter((key) => !params[key]);
  if (missingParams.length > 0) {
    throw new MissingParameters(label, missingParams);
  }
}

export function createLogger(observer?: ZenObservable.Observer<any>) {
  return function (message: string) {
    if (observer && observer.next) {
      observer.next(message);
    } else {
      // tslint:disable-next-line:no-console
      console.log(message);
    }
  };
}

export function createErrorLogger(observer?: ZenObservable.Observer<any>) {
  return function log(message: string) {
    if (observer && observer.error) {
      observer.error(message);
    } else {
      // tslint:disable-next-line:no-console
      console.error(message);
    }
  };
}

export class CompileScriptError extends Error {
  constructor(public filename: string) {
    super(`Error while compiling ${filename}`);
  }
}

export async function loadScript<T>(
  filename: string,
  log: Logger,
  scriptGlobals?: any
): Promise<T> {
  const babelConfig = {
    presets: ["@babel/preset-typescript", ["@babel/preset-env", { targets: { node: true } }]],
    plugins: [
      [
        "module-resolver",
        {
          root: [path.dirname(filename)],
          extensions: [".js", ".json", ".ts"],
          resolvePath(source: string, currentFile: string, opts: object) {
            log(`Resolving "${source}" from "${currentFile}"`);

            if (source.startsWith("./")) return path.resolve(path.dirname(currentFile), source);
            return source;
          },
        },
      ],
    ],
    cwd: path.dirname(filename),
    filename,
  };

  log(`Loading file "${filename}"`);
  log(`Babel Params:\n${JSON.stringify(babelConfig)}`);

  const fileContent = fs.readFileSync(filename, "utf-8");

  const prefix = `const context = ${JSON.stringify(scriptGlobals ?? {})};\n`;

  const transformResult = await babel.transformAsync(prefix + fileContent, babelConfig);

  let script: T;

  function evaluateScript(code: string): T {
    const __dirname = path.dirname(filename);
    const __filename = filename;
    return eval(code);
  }

  if (transformResult?.code) {
    script = evaluateScript(transformResult.code);
  } else {
    log(`Failed to compile "${filename}"`);
    log(JSON.stringify(transformResult));
    throw new CompileScriptError(filename);
  }

  // @ts-ignore
  return script;
}

export interface GetAllScripts {
  config: Configuration;
  scope?: string;
  log: Logger;
}

async function getScriptFiles(
  config: Configuration,
  log: Logger,
  scope?: string
): Promise<Array<string>> {
  const migrationsPath = getMigrationsPath(config);

  log(`Looking for scripts in path "${migrationsPath}", with scope "${scope}"`);

  const entries = await glob(scope ? path.join(scope, "*.ts") : "*.ts", {
    cwd: migrationsPath,
    onlyFiles: true,
    deep: 2,
    absolute: true,
  });

  entries.sort((a, b) => {
    const first = path.basename(a);
    const second = path.basename(b);

    if (first < second) return -1;
    if (first == second) return 0;

    return 1;
  });

  log(`Found ${entries.length} scripts: ${JSON.stringify(entries)}`);

  return entries;
}

export async function getAllScripts({
  config,
  scope,
  log,
}: GetAllScripts): Promise<Map<string, MigrationScript>> {
  const scripts = new Map<string, MigrationScript>();
  const scriptFiles = await getScriptFiles(config, log, scope);

  for (const filename of scriptFiles) {
    let script: MigrationScript;
    try {
      script = await loadScript<MigrationScript>(filename, log);
    } catch (ex) {
      log(ex.message);
      const errorMessage = ex.message;
      script = {
        async up(context: ScriptContext, log: Logger) {
          log(errorMessage);
          throw new Error(errorMessage);
        },
        async down(context: ScriptContext, log: Logger) {
          log(errorMessage);
          throw new Error(errorMessage);
        },
      };
    }

    scripts.set(path.basename(filename), script);
  }

  return scripts;
}