nest.js 集成数据迁移方案 sequelize、umzug -v2

本次升级,主要应用场景,用于采用.env配置方案,自动执行数据脚本迁移时候的应用。
ORM采用sequelize
迁移采用umzug
数据库基于mysql
不说废话,上干货。

# 安装依赖 生产环境需要自动运行所以 不再是 --save-dev安装
$ yarn add @nestjs/sequelize sequelize sequelize-typescript mysql2
$ yarn add umzug
$ yarn add @types/sequelize
$ yarn add cross-env
# 创建迁移文件夹
$ mkdir database

数据库迁移配置 跟随.env或者 nacos等动态配置即可不需要设置config.json、config.js 可以根据实际情况改造

  • database/config.json
    以下代码保留仅供参考,已经没有实际运行意义,项目已经删除此文件了。
{
  "local": {
    "port": 3306,
    "host": "127.0.0.1",
    "database": "xxxxx",
    "username": "root",
    "password": "xxxx",
    "dialect": "mysql",
    "define": {
      "charset": "utf8"
    },
    "logging": false
  },
  "development": {
    ...
  },
  "stage": {
    ...
  },
  "production": {
    ...
  },
  "test": {
    ...
  }
}

执行文件

升级umzug.ts文件进行直接运行的执行方式

  • database/umzug.ts
// 修正读取 .env文件

import { Umzug, SequelizeStorage, UmzugOptions } from 'umzug'
import { Dialect, Sequelize } from 'sequelize'
// config 加载
// import config from './config.json'
import { snakeCase, replace, get } from 'lodash'
import path from 'path'
import fs from 'fs'
import * as dotenv from 'dotenv'
dotenv.config()

// #region args
/**
 * up:MigrateUpOptions
 * down:MigrateUpOptions
 * create: {
        name: string;
        folder?: string;
        prefix?: 'TIMESTAMP' | 'DATE' | 'NONE';
        allowExtension?: string;
        allowConfusingOrdering?: boolean;
        skipVerify?: boolean;
    }
 */
const cliValue = ['up', 'down', 'create']

const args = process.argv.slice(2) // 从第三个元素开始是传递的参数
const namedArgs: {
  type: 'ts' | 'js'
  cli: (typeof cliValue)[number]
  temp: 'table' | 'seed'
} = { type: 'js' } as any

// 参数处理
for (let i = 0; i < args.length; i++) {
  const arg = args[i]

  const [name, value] = arg.split('=')
  // // 判断参数是否重复
  // if ((namedArgs as any)[name]) {
  //   console.error(`Error: Duplicate parameter found: ${name}`)
  //   process.exit(1)
  // }

  ;(namedArgs as any)[name] = value || true
}

// 判断是否包含 type 参数
if (!namedArgs.type || !['ts', 'js'].includes(namedArgs.type)) {
  console.error('Error: Missing required parameter: type, and type must `ts` or `js` ')
  process.exit(1)
}
// 判断是否包含 type 参数
if (!namedArgs.cli || !cliValue.includes(namedArgs.cli)) {
  console.log(namedArgs)
  console.error('Error: Missing required parameter: cli ')
  process.exit(1)
}
// #endregion

/**
 * 配置文件读取
 */
const config = {
  port: process.env.MYSQL_PORT_ADMIN as unknown as number,
  host: process.env.MYSQL_HOST_ADMIN,
  database: process.env.MYSQL_DATABASE_ADMIN,
  username: process.env.MYSQL_USERNAME_ADMIN,
  password: process.env.MYSQL_PASSWORD_ADMIN,
  dialect: 'mysql' as Dialect,
  define: {
    charset: 'utf8',
  },
  logging: false,
}

const rtlEnv = process.env.NODE_ENV
const sequelizeConfig = config
console.log(`[umzug]:db:${sequelizeConfig.database}`)
const sequelize = new Sequelize(sequelizeConfig)

/**
 * 默认模版替换
 * @param filepath 路径
 * @returns
 */
const findTemplate = (filepath: string, fileName = namedArgs.temp || 'table') => {
  const temp = fs.readFileSync(path.join(`database/template/${fileName}.ts`)).toString()
  const names = filepath.split('.')
  const name = snakeCase(names[names.length - 2])
  return replace(temp, /\[tableName\]/g, name)
}

const migrationsConfig: UmzugOptions<Sequelize> = {
  migrations: {
    glob: [`migrations/*.${namedArgs.type || 'ts'}`, { cwd: __dirname }],
  },
  create: {
    template: (filepath: string) => [[filepath, findTemplate(filepath)]],
    folder: path.join('database/migrations/'),
  },
  context: sequelize,
  storage: new SequelizeStorage({
    sequelize,
    tableName: process.env.UMZUG_TABLE_NAME || 'sequelize_umzug',
  }),
  logger: console,
}

const umzug: any = new Umzug(migrationsConfig)

umzug[namedArgs.cli]?.(namedArgs)?.then(() => {
  console.log(`Umzug success!`)
  process.exit(0)
})

  • database/utils/default-column.ts
    默认列 本地开发主外键设计
import Sequelize from 'sequelize';

const { DATE, STRING, INTEGER } = Sequelize;

export default {
  id: { type: STRING(50), primaryKey: true },
  // Creating two objects with the same value will throw an error. The unique property can be either a
  // boolean, or a string. If you provide the same string for multiple columns, they will form a
  created_at: {
    type: DATE,
    defaultValue: Sequelize.fn('now'),
    comment: '创建时间',
  },
  created_id: {
    type: STRING(50),
    defaultValue: '',
    comment: '创建人id',
  },
  updated_at: {
    type: DATE,
    defaultValue: Sequelize.fn('now'),
    comment: '修改时间',
  },
  updated_id: {
    type: STRING(50),
    comment: '修改人id',
  },
  deleted_at: { type: DATE, comment: '删除时间' },
  deleted_id: {
    type: STRING(50),
    comment: '删除人id',
  },
  business_code: {
    type: STRING(500),
    comment: '业务编码权限用',
  },
  remark: {
    type: STRING(500),
    comment: '备注',
  },
  version: {
    type: INTEGER,
    comment: 'BaseTable.version',
  },
  enable_flag: {
    type: INTEGER,
    comment: '状态 1启用 0停用默认1',
    defaultValue: 1,
  },
};

export const references = (tableName: string, keyName = 'id') => {
  if (process.env.NODE_ENV === 'production') {
    return undefined;
  }
  return {
    model: {
      tableName,
    },
    keyName,
  };
};

自定义模版

  • database/template/table.ts
import { MigrationFn } from 'umzug';
import { Sequelize } from 'sequelize';
// import { DataTypes } from 'sequelize';
import defaultColumns from '../utils/default-column';

export const up: MigrationFn<Sequelize> = async ({ context: sequelize }) => {
  return await sequelize.getQueryInterface().createTable('[tableName]', {
    ...defaultColumns,
  });
};

export const down: MigrationFn<Sequelize> = async ({ context: sequelize }) => {
  return await sequelize.getQueryInterface().dropTable('[tableName]');
};

  • database/template/seed.ts
/* eslint-disable @typescript-eslint/no-unused-vars */
import { MigrationFn } from 'umzug';
import { Sequelize } from 'sequelize';
// import { DataTypes } from 'sequelize';

export const up: MigrationFn<Sequelize> = async ({ context: sequelize }) => {
  return await sequelize.getQueryInterface().bulkInsert('[tableName]', [{}]);
};

export const down: MigrationFn<Sequelize> = async ({ context: sequelize }) => {
  return await sequelize.getQueryInterface().bulkDelete('[tableName]', {
    where: {
      id: [],
    },
  });
};

命令

umzug 结构迁移
seed 数据迁移
-h = 帮助
-up = 升级
-down = 降级
-create = 创建
type: ts、js
cli: up、down、create
temp: table、seed 对应/database/template/.ts
其他参数 直接传递
cli=create 之后的参数直接传递
/
*

  • up:MigrateUpOptions
  • down:MigrateUpOptions
  • create: name: string;
    folder?: string;
    prefix?: 'TIMESTAMP' | 'DATE' | 'NONE';
    allowExtension?: string;
    allowConfusingOrdering?: boolean;
    skipVerify?: boolean;
    */
$ npm run umzug -- type=ts cli=create name=user.ts
$ npm run umzug -- type=ts cli=up   
$ npm run umzug -- type=ts cli=down
{
  "script": {
        "umzug-prd": " node lib/database/umzug.js -- type=js cli=up",
        "umzug": " ts-node database/umzug.ts"
  }
}

error

yarn run v1.22.19
$ cross-env NODE_ENV=local node database/migrator.js create --name user.ts
/Users/wuzhanchao/Documents/万达项目组/好房推荐官/source/wd-nest-manager/node_modules/ts-node/src/index.ts:859
    return new TSError(diagnosticText, diagnosticCodes, diagnostics);
           ^
TSError: ⨯ Unable to compile TypeScript
TSError: ⨯ Unable to compile TypeScript:
database/umzug.ts:4:20 - error TS2732: Cannot find module './config.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.

4 import config from './config.json';

tsconfig.json
增加配置项

{
  "compilerOptions": {
    "resolveJsonModule": true
    "esModuleInterop": true
    ......
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容