Alan Colon 7 роки тому
батько
коміт
567f7c2d50

+ 17 - 2
bin/project.js

@@ -10,28 +10,43 @@ const server = require('../lib/server')
 
 const vorpal = new Vorpal()
 const main = async () => {
-  await database.init();
+  let _initDb = null
+  const initDb = () => _initDb || (_initDb = database.init())
 
   vorpal.command('create user <email> [password]', 'Creates a user')
   .action(async model => {
     if (!model.password) {
       model.password = await prompt('Password: ')
     }
+    await initDb()
     const user = await User.create(model)
     console.log('Created user')
   })
 
   vorpal.command('list users', 'Lists all users')
   .action(async () => {
+    await initDb()
     console.log(asTable((await User.all()).map(x => x.dataValues)))
   })
 
   vorpal.command('server', 'Runs the web server')
-  .action(() => server.start())
+  .action(async () => {
+    await initDb()
+    return server.start()
+  })
 
   vorpal.command('repl', 'Runs Node REPL')
   .action(() => require('repl').start())
 
+  vorpal.command('migration [namespace] [version]', 'Runs database migration scripts')
+  .action(async (model) => {
+    if (!model.namespace) {
+      await database.setup.migrateAll()
+    } else {
+      await database.setup.migrate(model.namespace, (+database.version) || null)
+    }
+  })
+
   vorpal.delimiter('project>')
   if (process.argv.length > 2) {
     await vorpal.parse(process.argv)

+ 2 - 2
lib/crud/controller.js

@@ -90,7 +90,7 @@ const crudController = (opts) => {
   }
   const read = async (req, res) => {
     const data = (await Type.findOne({where: {id: req.params[opts.routeParam]}}))
-    const json = data && data.sanitize ? data.sanitize() : data.toJSON()
+    const json = data && (data.sanitize ? data.sanitize() : data.toJSON())
     await getAssociations(data, json)
     res.status(200).send(json)
   }
@@ -100,7 +100,7 @@ const crudController = (opts) => {
       const record = _.omit(req.body, _.keys(await subset(req)))
       const updated = (await Type.update(record, { where: { id: req.params[opts.routeParam] }, transaction }))
       const data = (await Type.findOne({where: { id: req.params[opts.routeParam] }}))
-      const json = data && data.sanitize ? data.sanitize() : data.toJSON()
+      const json = data && (data.sanitize ? data.sanitize() : data.toJSON())
 
       await setAssociations(record, data, transaction)
       await transaction.commit()

+ 4 - 2
lib/database/index.js

@@ -5,6 +5,7 @@ const sequelize = require('./sequelize')
 const User = require('./user')
 const Session = require('./session')
 const Role = require('./role')
+const setup = require('./setup')
 
 const UserRole = User.belongsToMany(Role, { through: 'userRoles' })
 
@@ -29,12 +30,13 @@ const upsert = async (Type, object, fields) => {
 
 module.exports = {
   init: () => {
-    return sequelize.sync()
+    //return sequelize.sync()
   },
   sequelize,
   User,
   Session,
   Role,
   UserRole,
-  upsert
+  upsert,
+  setup
 }

+ 16 - 0
lib/database/migrations/0.1-migration.js

@@ -0,0 +1,16 @@
+module.exports = {
+  version: 0.1,
+  name: 'Migration',
+  description: 'Sets up the hidden `_version` table to track database schema version.',
+  up: async (queryInterface, Sequelize) => {
+    await queryInterface.createTable('_version', {
+      key: {
+        type: Sequelize.STRING,
+        primaryKey: true
+      },
+      value: Sequelize.DOUBLE
+    })
+    await queryInterface.insert(null, '_version', { key: 'material-framework', value: 0.1 }, {})
+    return 0.1
+  }
+}

+ 7 - 0
lib/database/migrations/index.js

@@ -0,0 +1,7 @@
+module.exports = {
+  'material-framework': [
+    require('./0.1-migration.js')
+  ]
+}
+
+module.exports['material-framework'].defaultVersion = -1

+ 84 - 0
lib/database/setup.js

@@ -0,0 +1,84 @@
+const _ = require('lodash')
+const sequelize = require('./sequelize')
+const Sequelize = require('sequelize')
+const migrations = require('./migrations')
+const chalk = require('chalk')
+
+const Version = sequelize.define('_version', {
+  key: {
+    type: Sequelize.STRING,
+    primaryKey: true
+  },
+  value: Sequelize.DOUBLE
+}, {
+  tableName: '_version',
+  timestamps: false
+})
+
+const getVersion = async (namespace, queryInterface) => {
+  const defaultVersion = migrations[namespace].defaultVersion || migrations[namespace].map(x => x.version).reduce((a, b) => Math.max(a, b), 0.0)
+  const tables = await queryInterface.showAllTables()
+  if (!tables.includes('_version')) {
+    return defaultVersion
+  } else {
+    const result = await Version.findOne({where: {key: namespace}})
+    if (result) {
+      return result.value
+    } else {
+      return defaultVersion
+    }
+  }
+}
+
+const setVersion = async (namespace, version, queryInterface) => {
+  await Version.upsert({key: namespace, value: version}, { where: { key: namespace }})
+}
+
+const migrate = async (namespace, targetVersion) => {
+  const queryInterface = sequelize.getQueryInterface()
+  const currentVersion = await getVersion(namespace, queryInterface)
+  let migs = _.sortBy(migrations[namespace], 'version')
+  if (!targetVersion) targetVersion = migs.map(x => x.version).reduce((a, b) => Math.max(a, b), 0)
+  console.log(chalk.green(`Migrating ${namespace}`))
+  console.log(chalk.yellow(`Current version: ${currentVersion}`))
+  console.log(chalk.yellow(`Target version:  ${targetVersion}`))
+  // Sanity check
+  for (let mig of migs) {
+    if (!mig.version) throw new Error(`All migrations require versions`)
+    if (!mig.up) throw new Error('All migrations require `up`')
+  }
+  // Filter to relevant migrations
+  migs = migs.filter(mig => mig.version > currentVersion && mig.version <= targetVersion)
+  console.log(`Migrations to run: ${migs.map(x => x.version).join(', ')}`)
+  let lastVersion = currentVersion
+  for (let mig of migs) {
+    if (mig.version > lastVersion) {
+      console.log(chalk.blue(`Running migration ${mig.version}: ${mig.name}`))
+      mig.description && console.log(chalk.blue(mig.description))
+      let newVersion = await mig.up(queryInterface, Sequelize)
+      if (!Number.isFinite(newVersion)) throw new Error(`All migrations must return the resulting version`)
+      if (newVersion < lastVersion) throw new Error(`Up migration downgraded database: ${mig.version} -> ${newVersion}`)
+      await setVersion(namespace, newVersion, queryInterface)
+      console.log(`Database now at version ${newVersion}`)
+      lastVersion = newVersion
+    } else {
+      console.log(chalk.blue(`Skipping migration ${mig.version}: ${mig.name}`))
+    }
+  }
+  console.log(chalk.cyan(`Migration complete. Current database version: ${await getVersion(namespace, queryInterface)}`))
+}
+
+const migrateAll = async () => {
+  const namespaces = _.keys(_.omit(migrations, 'material-framework'))
+  await migrate('material-framework')
+  console.log(_.keys(migrations))
+  for (let namespace of namespaces) {
+    await migrate(namespace)
+  }
+}
+
+module.exports = {
+  getVersion,
+  migrate,
+  migrateAll
+}