db-migrate / node-db-migrate

Database migration framework for node
Other
2.32k stars 360 forks source link

Write migrations in TypeScript #717

Open levino opened 3 years ago

levino commented 3 years ago

I am struggling to write my migrations in TypeScript files. I guess that would be possible using babel etc. Does anyone have a running setup and would be willing to share it?

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

wzrdtales commented 3 years ago

i am not a user of typescript and will never be, so I will be not much of help here. However there is a plugin for typescript, that might be all you need: https://github.com/db-migrate/plugin-typescript/

flisboac commented 2 years ago

I've been looking into this recently, and indeed, the plugin-typescript works as expected, but it is a bit outdated. A warning is issued by db-migrate upon execution of any migration command (but it doesn't prevent the operation itself from being performed):

[WARN] The plugin 'db-migrate-plugin-typescript' is outdated! The hookmigrator:migration:hook:require was deprecated!
[WARN] Report the maintainer of 'db-migrate-plugin-typescript' to patch the hook instead with file:hook:require.

With this plugin, you can have both js and ts migrations in your migrations directory. You must have both db-migrate-plugin-typescript and ts-node installed.

The only piece of the puzzle that's missing is the typing. There's a @types/db-migrate-base in NPM, but it's a bit outdated. Instead of using it directly, I rewrote it as a new .d.ts in my projects (perhaps in the future I can create a PR; but the fact is that I don't know if the typings are accurate, or correct). Follows:

// I don't think we need Bluebird in this case. Any respectable JavaScript environment nowadays will have
// a useable Promise class.
export type PromiseConstructor = typeof Promise;

export interface DataTypeNameMap {
  CHAR: string;
  STRING: string;
  TEXT: string;
  SMALLINT: string;
  BIGINT: string;
  INTEGER: string;
  SMALL_INTEGER: string;
  BIG_INTEGER: string;
  REAL: string;
  DATE: string;
  DATE_TIME: string;
  TIME: string;
  BLOB: string;
  TIMESTAMP: string;
  BINARY: string;
  BOOLEAN: string;
  DECIMAL: string;
}

export interface Logger {
  isSilent: boolean;
  setLogLevel(level: string): void;
  // setEscape(...args: unknown[]): unknown;
  // silence(...args: unknown[]): unknown;
  info(message: string): void;
  warn(message: string): void;
  error(message: string): void;
  sql(message: string): void;
  verbose(message: string): void;
}

export declare class SeedLink {
  constructor(driver: unknown, internals: unknown);
  seeder: unknown;
  internals: unknown;
  links: Array<unknown>;
  seed(partialName: string): Promise<unknown>;
  link(partialName: string): void;
  process(): void;
  clear(): void;
}

export interface MigrationV1Config {
  version: string;
  dataType: DataTypeNameMap;
}

export interface MigrationV1Options {
  dbmigrate: MigrationV1Config;
  dryRun?: boolean;
  cwd: string;
  noTransactions: boolean;
  verbose: boolean;
  type: DataTypeNameMap;
  log: Logger;
  ignoreOnInit?: boolean;
  Promise: PromiseConstructor;
}

export interface MigrationV1SetupFunction {
  (options: MigrationV1Options, seedLink: SeedLink): void;
}

export interface MigrationV1Function<DbDriver = BaseDbDriver> {
  (db: DbDriver): Promise<any>;
}

export interface MigrationV1Meta {
  version: number;
}

export declare class BaseDbDriver {
  close(): Promise<any>;
  truncate(tableName: string): Promise<any>;
  checkDBMS(dbms: any): Promise<any>;
  createDatabase(...options: any[]): Promise<any>;
  switchDatabase(...options: any[]): Promise<any>;
  dropDatabase(...options: any[]): Promise<any>;
  recurseCallbackArray(foreignKeys: Array<string>): Promise<any>;
  createMigrationsTable(): Promise<any>;
  createSeedsTable(): Promise<any>;
  createTable(tableName: string, options: any | CreateTableOptions): Promise<any>;
  dropTable(tableName: string, options?: DropTableOptions): Promise<any>;
  renameTable(tableName: string, newTableName: string): Promise<any>;
  addColumn(tableName: string, columnName: string, columnSpec: ColumnSpec): Promise<any>;
  removeColumn(tableName: string, columnName: string): Promise<any>;
  renameColumn(tableName: string, oldColumnName: string, newColumnName: string): Promise<any>;
  changeColumn(tableName: string, columnName: string, columnSpec: ColumnSpec): Promise<any>;
  addIndex(tableName: string, indexName: string, columns: string | Array<string>, unique?: boolean): Promise<any>;
  insert(
    tableName: string,
    columnNameOrValueArray: any,
    valueArrayOrCb?: any | CallbackFunction,
    callback?: CallbackFunction,
  ): Promise<any>;
  update(
    tableName: string,
    columnNameOrValueArray: any,
    valueArrayOrIds?: any,
    idsOrCb?: any | CallbackFunction,
    callback?: CallbackFunction,
  ): Promise<any>;
  lookup(tableName: string, column: string, id?: any, callback?: CallbackFunction): Promise<any>;
  removeIndex(tableNameOrIndexName: string, indexName?: string): Promise<any>;
  addForeignKey(
    tableName: string,
    referencedTableName: string,
    keyName: string,
    fieldMapping: any,
    rules: ForeignKeyRules,
  ): Promise<any>;
  removeForeignKey(tableName: string, keyName: string, options?: RemoveForeignKeyOptions): Promise<any>;
  addMigrationRecord(name: string): Promise<any>;
  addSeedRecord(name: string): Promise<any>;
  startMigration(): Promise<any>;
  endMigration(callback: CallbackFunction): Promise<any>;
  runSql(sql?: string, params?: Array<any>): Promise<any>;
  allLoadedMigrations(): Promise<any>;
  allLoadedSeeds(): Promise<any>;
  deleteMigration(migrationName: string): Promise<any>;
  remove(table: string, ids: any): Promise<any>;
  deleteSeed(seedName: string): Promise<any>;
  all(sql: string, params?: Array<any>): Promise<any>;
}

export interface CallbackFunction {
  (err: any, response: any): void;
}

export interface InternalModule {
  log: Logger;
  type: DataTypeNameMap;
}

export interface InternalOptions {
  mod: InternalModule;
}

export interface ColumnSpec {
  length?: number | undefined;
  type: string;
  unsigned?: boolean | undefined;
  primaryKey?: boolean | undefined;
  autoIncrement?: boolean | undefined;
  notNull?: boolean | undefined;
  unique?: boolean | undefined;
  defaultValue?: any;
  foreignKey?: ForeignKeySpec | undefined;
}

export interface ForeignKeySpec {
  name: string;
  table: string;
  rules?: ForeignKeyRules | undefined;
  mapping: string | any;
}

export interface ForeignKeyRules {
  onDelete: string;
  onUpdate: string;
}

export interface RemoveForeignKeyOptions {
  dropIndex?: boolean | undefined;
}

export interface CreateTableOptions {
  columns?: Array<ColumnSpec> | undefined;
  ifNotExists?: boolean | undefined;
}

export interface DropTableOptions {
  ifExists?: boolean | undefined;
}

Then, your scripts could be written as such:

import {
  DataTypeNameMap,
  MigrationV1Config,
  MigrationV1Function,
  MigrationV1Meta,
  MigrationV1SetupFunction,
  SeedLink,
} from 'src/util/db/migrations'; // Could be anywhere; move it to wherever you like

let dbm: MigrationV1Config;
let type: DataTypeNameMap;
let seed: SeedLink;

export const setup: MigrationV1SetupFunction = (options, seedLink) => {
  dbm = options.dbmigrate;
  type = dbm.dataType;
  seed = seedLink;
};

export const up: MigrationV1Function = async db => {
  // TODO Change according to your desired implementation
  await db.createTable('test', {
    id: {
      type: type.BIGINT,
      primaryKey: true,
    },
    name: {
      type: type.STRING,
      length: 60,
    },
  });
};

export const down: MigrationV1Function = async db => {
  // TODO Change according to your desired implementation
  await db.dropTable('test');
};

export const _meta: MigrationV1Meta = {
  version: 1,
};

I don't have any definitions for V2 scripts yet, because I'm not really interested in anything V2 is introducing.

raviSussol commented 2 years ago

@flisboac can you please share some demo repo for the above case. Would really appreciate for your help on this. Thanks.

flisboac commented 2 years ago

Hello, @raviSussol I'm not actively using db-migrate anymore, as I've moved on from TypeScript and NodeJS, for the time being. But following my suggestion is quite easy:

  1. Install db-migrate, db-migrate-plugin-typescript and ts-node
  2. Save the typings I suggested to some file, e.g. db-migrate.types.ts
  3. Create a file with the template I provided in your migrations folder, and implement the migration.

It's really all there is to it. The plugin will take care of loading your file via ts-node.

Also, you can transpile your entire project and the run db-migrate, bit that's a lot more involved, and complicates things a lot (like, having to statically resolve/replace paths from tsconfig, and whatnot).

In any case, I'm not even sure if this template and typings are right anymore. I really haven't been keeping track.

I don't know if I'll have the time to make a sample project, because there's a lot of things I need to focus on rn, but I can try.

raviSussol commented 2 years ago

Hi @flisboac Thanks for the info. I was able to resolve the typing issue by redefining types based on the db-migrate functions' signature. I was only using some of their functions (not all) so I resolve their typings with redefining types to my local project. So thanks again.