inspired-server
Version:
A RESTful entity server
157 lines (130 loc) • 4.7 kB
text/coffeescript
Q = require 'q'
class Migration
constructor: (@classes) ->
@db = new App.DB
@queries = {
tables: []
dropConstraints: []
dropFields: []
fields: []
uniques: []
constraints: []
}
promises = [];
for table, entityClass of @classes
promises.push @prepareTable(table, entityClass)
Q.all(promises)
.then =>
@runQueries()
.catch (e) ->
console.log '[ERROR]', e
runQuery: (sql) ->
console.log '[MIGRATION]', sql
@db.adapter().query sql, []
handleError: (sql, e) ->
switch e.code
when '42P07' then #nothing
when '42710' then #nothing
else console.log '[ERROR]', sql, e
runQueries: ->
deferred = Q.defer()
deferred.resolve()
for table, queries of @queries
for sql in queries when sql? and sql isnt ''
deferred.promise
.then @runQuery.bind(@, sql)
.catch @handleError.bind(@, sql)
prepareTable: (table, entityClass) ->
sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='#{table}'"
@db.adapter().query(sql, []).then @parseTable.bind(@, table, entityClass)
parseTable: (table, entityClass, result) ->
instance = new entityClass
fields = instance.fields()
data = {
fields: {
inCode: {}, inDB: {}, onlyInCode: {}, onlyInDB: {}, inBoth: {}
}
constraints: {
inCode: {}, inDB: {}, onlyInCode: {}, onlyInDB: {}, inBoth: {}
}
}
for name, field of fields
data.fields.inCode[name] = field
data.constraints.inCode[name] = field if field.isForeignKey()
###
Index the result set by column name.
The input is numerically keyed but the
output uses the column name as the key.
###
data.fields.inDB = result.rows.reduce (rows, row) ->
rows[row.column_name] = row
rows
, {}
###
@todo Refactor
###
for name, row of data.fields.inDB when not data.fields.inCode[name]?
data.fields.onlyInDB[name] = row
for name, field of data.fields.inCode when not data.fields.inDB[name]?
data.fields.onlyInCode[name] = field
for name, field of data.fields.inCode when data.fields.inDB[name]?
data.fields.inBoth[name] = field
for name, row of data.constraints.inDB when not data.constraints.inCode[name]?
data.constraints.onlyInDB[name] = row
for name, field of data.constraints.inCode when not data.constraints.inDB[name]?
data.constraints.onlyInCode[name] = field
# Make sure the table exists or create it
@queries.tables.push "CREATE TABLE IF NOT EXISTS \"#{table}\" ()"
# DROP constraints
for column, row of data.constraints.onlyInDB
@queries.dropConstraints.push "ALTER TABLE \"#{table}\" DROP CONSTRAINT #{table}_#{column}_unique"
@queries.dropConstraints.push "ALTER TABLE \"#{table}\" DROP CONSTRAINT #{table}_#{column}_fk"
# DROP fields
for column, row of data.fields.onlyInDB
@queries.dropFields.push "ALTER TABLE \"#{table}\" DROP IF EXISTS \"#{column}\" CASCADE"
# ADD fields
for column, field of data.fields.onlyInCode
if field.isForeignKey() and field.multiple()
# if field.unique() # OneToMany
# else # ManyToMany
###
@todo Handle ManyToMany
###
else
@fieldHelper table, column, field
# ALTER fields
for column, field of data.fields.inBoth
@fieldHelper table, column, field, data.fields.inDB[column]
# ADD constraints
for column, field of data.constraints.onlyInCode
if field.multiple()
# if field.unique() # OneToMany
# else # ManyToMany
###
@todo Handle ManyToMany
###
else
@fieldUniqueHelper table, column if field.unique() # OneToOne
constraintName = "#{table}_#{column}_fk"
@queries.constraints.push "ALTER TABLE \"#{table}\" ADD CONSTRAINT #{constraintName} FOREIGN KEY (#{column}) REFERENCES \"#{field.references()}\" (\"uuid\")"
fieldHelper: (table, column, field, row) ->
sql = ''
sqlLength = if field.length() then "(#{field.length()})" else ''
if row?
typeChange = (field.type() isnt row.udt_name)
lengthChange = (field.length() isnt row.character_maximum_length)
defaultChange = (field.default() isnt row.column_default)
sql += " TYPE #{field.type()}#{sqlLength}" if typeChange or lengthChange
sql += " SET DEFAULT #{field.default()}" if defaultChange
else
sql += " #{field.type()}#{sqlLength}"
sql += " DEFAULT #{field.default()}"
operation = if row? then "ALTER" else "ADD"
sql = "ALTER TABLE \"#{table}\" #{operation} \"#{column}\"" + sql if sql
@queries.fields.push sql if sql
if field.unique()
@fieldUniqueHelper table, column
fieldUniqueHelper: (table, column) ->
constraintName = "#{table}_#{column}_unique"
@queries.uniques.push "ALTER TABLE \"#{table}\" ADD CONSTRAINT #{constraintName} UNIQUE (#{column})"
module.exports = Migration