Comparing version 0.10.1 to 0.11.1
@@ -225,2 +225,6 @@ "use strict"; | ||
if (!template) { | ||
throw new Error('Statement not loaded as a template: ' + tplQuery.path); | ||
} | ||
var sql = template(templateParams); | ||
@@ -227,0 +231,0 @@ query = { |
@@ -5,5 +5,13 @@ 'use strict'; | ||
layoutModule = require('../layout'), | ||
parseMigrations = require('./parse'), | ||
storeModule = require('../store'); | ||
storeModule = require('../store'), | ||
fs = Promise.promisifyAll(require('fs')), | ||
path = require('path'), | ||
assert = require('assert'), | ||
moment = require('moment-timezone'); | ||
var CANDIDATE_STATUS = { | ||
PENDING: 'PENDING', | ||
MISSING: 'MISSING' | ||
}; | ||
module.exports = function(client) { | ||
@@ -13,3 +21,3 @@ | ||
var layout = layoutModule(client); | ||
var loadedMigrations = {}; | ||
var candidateMigrations = []; | ||
var templateVars = {}; | ||
@@ -34,2 +42,30 @@ | ||
function makeMigration(filePath, date, description, sql) { | ||
assert(_.isString(filePath)); | ||
assert(_.isString(description)); | ||
assert(!_.isEmpty(description)); | ||
assert(_.isDate(date)); | ||
var migration = { | ||
path: filePath, | ||
date: date, | ||
description: description, | ||
template: sql | ||
}; | ||
return migration; | ||
} | ||
function readMigration(filePath) { | ||
var filename = path.basename(filePath, '.sql'); | ||
return fs.readFileAsync(filePath).then(function(data) { | ||
return makeMigration( | ||
filePath, | ||
new Date(_.first(filename.split('_'))), | ||
_.rest(filename.split('_')).join('_'), | ||
data.toString() | ||
); | ||
}); | ||
} | ||
function ensureMigrationTable(schema) { | ||
@@ -44,18 +80,15 @@ return layout.ensureTable(schema + '.migration', { | ||
function loadLastMigrationDate(schema) { | ||
function getCommittedMigrations(schema) { | ||
return ensureMigrationTable(schema) | ||
.then(function() { | ||
return client.execTemplate( | ||
statements.getLastMigration, | ||
{schema: schema}); | ||
}) | ||
.then(function(result) { | ||
return result.length && result[0].date; | ||
}); | ||
.then(function() { | ||
return client.execTemplate( | ||
statements.getMigrations, | ||
{schema: schema}); | ||
}); | ||
} | ||
function recordMigration(migration) { | ||
function recordMigration(migration, schema) { | ||
return client.execTemplate( | ||
statements.recordMigration, | ||
{schema: migration.schema}, | ||
{schema: schema}, | ||
migration); | ||
@@ -79,10 +112,7 @@ } | ||
migrator.loadMigrations = loadMigrations; | ||
function loadMigrations(filePath, opts) { | ||
return parseMigrations(filePath) | ||
.then(function(migrations) { | ||
if (!migrations.length) { | ||
throw new Error('No migrations found: ' + filePath); | ||
} | ||
return addMigrations(migrations, opts); | ||
migrator.loadMigration = loadMigration; | ||
function loadMigration(filePath, opts) { | ||
return readMigration(filePath) | ||
.then(function(migration) { | ||
return addMigration(migration, opts); | ||
}); | ||
@@ -93,80 +123,139 @@ } | ||
migrator.addMigrations = addMigrations; | ||
function addMigrations(migrations, opts) { | ||
opts = _.defaults(opts || {}, { | ||
schema: 'public' | ||
}); | ||
migrator.addMigration = addMigration; | ||
function addMigration(migration) { | ||
var prevMigrationDate = new Date('1970-01-01'); | ||
migrations = _.map(migrations, function(migration) { | ||
var date = migration.date; | ||
var date = migration.date; | ||
if (isNaN(date.getTime()) || !_.isDate(date)) { | ||
throw new TypeError( | ||
'Invalid date object: ' + date); | ||
} | ||
if (isNaN(date.getTime()) || !_.isDate(date)) { | ||
throw new TypeError( | ||
'Invalid date object: ' + date); | ||
} | ||
if (prevMigrationDate.getTime() === date.getTime()) { | ||
throw new Error( | ||
'Migration has identical time stamp; bad: ' + date); | ||
} | ||
if (prevMigrationDate > date) { | ||
throw new Error( | ||
'Migrations must remain ordered by date; bad: ' + date); | ||
} | ||
prevMigrationDate = date; | ||
if (prevMigrationDate.getTime() === date.getTime()) { | ||
throw new Error( | ||
'Migration has identical time stamp; bad: ' + date); | ||
} | ||
if (prevMigrationDate > date) { | ||
throw new Error( | ||
'Migrations must remain ordered by date; bad: ' + date); | ||
} | ||
prevMigrationDate = date; | ||
return _.extend({}, {schema: opts.schema}, migration); | ||
}); | ||
candidateMigrations.push(_.cloneDeep(migration)); | ||
} | ||
loadedMigrations[opts.schema] = ( | ||
(loadedMigrations[opts.schema] || []).concat(migrations)); | ||
migrator.createMigration = createMigration; | ||
function createMigration(name, dir) { | ||
assert(name, 'migration name required'); | ||
assert(dir, 'migration directory required'); | ||
var timestamp = | ||
moment().tz('America/Los_Angeles').format('YYYY-MM-DDTHH:mm:ss'); | ||
var filename = | ||
timestamp + '_' + _.snakeCase(name); | ||
var contents = [ | ||
'-- ', | ||
'-- ' + _.snakeCase(name), | ||
'-- ', | ||
'', | ||
'' | ||
].join('\n'); | ||
var path = dir + '/' + filename; | ||
return fs.writeFileAsync(path, contents) | ||
.then(function() { | ||
return path; | ||
}); | ||
} | ||
migrator.getPending = getPending; | ||
function getPending() { | ||
return Promise.all(_.transform( | ||
loadedMigrations, | ||
function(results, setMigrations, schema) { | ||
results.push( | ||
loadLastMigrationDate(schema) | ||
.then(function(lastDate) { | ||
return _.filter(setMigrations, function(migration) { | ||
return (!lastDate || (migration.date > lastDate)); | ||
}); | ||
})); | ||
return results; | ||
}, [])) | ||
.then(function(resultsBySet) { | ||
return _.sortBy(_.flatten(resultsBySet), 'date'); | ||
}); | ||
migrator.redate = redate; | ||
function redate(filePath) { | ||
assert(filePath, 'a path is required'); | ||
var namePart = _.rest(path.basename(filePath).split('_')).join('_'); | ||
var dir = path.dirname(filePath); | ||
var timestamp = | ||
moment().tz('America/Los_Angeles').format('YYYY-MM-DDTHH:mm:ss'); | ||
var newPath = dir + '/' + timestamp + '_' + namePart; | ||
return fs.renameAsync(filePath, newPath) | ||
.then(function() { | ||
return newPath; | ||
}); | ||
} | ||
migrator.up = migrator.runPending = runPending; | ||
function runPending() { | ||
migrator.getStatus = getStatus; | ||
function getStatus(schema) { | ||
return onReady.then(function() { | ||
return getPending(); | ||
return getCommittedMigrations(schema); | ||
}) | ||
.then(function(pendingMigrations) { | ||
if (!pendingMigrations.length) { | ||
client._logger.info('No pending migrations'); | ||
return; | ||
} | ||
return Promise.reduce(pendingMigrations, function(__, migration) { | ||
client._logger.info( | ||
'Running migration', | ||
_.pick(migration, 'date', 'description')); | ||
return (migration.template | ||
? client.execTemplate(migration.template, templateVars) | ||
: client.exec(migration.sql)) | ||
.then(function() { | ||
return recordMigration(migration); | ||
.then(function(committedMigrations) { | ||
var candidate = _.map(candidateMigrations, function(m) { | ||
return _.extend({}, m, {isCommitted: false}); | ||
}); | ||
}, null); | ||
}); | ||
var committed = _.map(committedMigrations, function(m) { | ||
return _.extend({}, m, {isCommitted: true}); | ||
}); | ||
var committedByDate = _.indexBy(committed, 'date'); | ||
var candidateByDate = _.indexBy(candidate, 'date'); | ||
var allByDate = _.extend({}, candidateByDate, committedByDate); | ||
var allMigrations = _.sortBy(_.values(allByDate), 'date'); | ||
var hasMissing = false; | ||
var hasPending = false; | ||
var hasComitted = false; | ||
allMigrations = _.map(allMigrations.reverse(), function(m) { | ||
if (!hasComitted && !m.isCommitted) { | ||
m.candidateStatus = CANDIDATE_STATUS.PENDING; | ||
hasPending = true; | ||
} | ||
if (hasComitted && !m.isCommitted) { | ||
m.candidateStatus = CANDIDATE_STATUS.MISSING; | ||
hasMissing = true; | ||
} | ||
if (!hasComitted && m.isCommitted) { | ||
hasComitted = true; | ||
} | ||
return m; | ||
}).reverse(); | ||
// console.log("ARGS", allMigrations, hasPending, hasMissing); | ||
return [allMigrations, hasPending, hasMissing]; | ||
}); | ||
} | ||
migrator.up = migrator.runPending = runPending; | ||
function runPending(schema) { | ||
return onReady.then(function() { | ||
return getStatus(schema); | ||
}) | ||
.then(_.spread(function(migrations, hasPending, hasMissing) { | ||
if (!hasPending && !hasMissing) { | ||
client._logger.info('No pending migrations'); | ||
return; | ||
} | ||
if (hasMissing) { | ||
throw new Error('One ore more migrations were missed'); | ||
} | ||
return Promise.reduce( | ||
_.filter(migrations, {candidateStatus: CANDIDATE_STATUS.PENDING}), | ||
function(__, migration) { | ||
client._logger.info( | ||
'Running migration', | ||
_.pick(migration, 'date', 'description')); | ||
return (migration.template | ||
? client.execTemplate(migration.template, templateVars) | ||
: client.exec(migration.sql)) | ||
.then(function() { | ||
return recordMigration(migration, schema); | ||
}); | ||
}, null); | ||
})); | ||
} | ||
return migrator; | ||
}; | ||
'use strict'; | ||
var _ = require('lodash'), | ||
_str = require('underscore.string'), | ||
assert = require('assert'), | ||
Promise = require('bluebird'), | ||
fs = Promise.promisifyAll(require('fs')); | ||
fs = Promise.promisifyAll(require('fs')), | ||
path = require('path'); | ||
function makeMigration(date, description) { | ||
function makeMigration(filePath, date, description, sql) { | ||
assert(_.isString(filePath)); | ||
assert(_.isString(description)); | ||
@@ -13,5 +14,6 @@ assert(!_.isEmpty(description)); | ||
var migration = { | ||
path: filePath, | ||
date: date, | ||
description: description, | ||
template: '' | ||
template: sql | ||
}; | ||
@@ -21,119 +23,14 @@ return migration; | ||
function setSql(migration, sqlLines) { | ||
assert(_.isArray(sqlLines)); | ||
module.exports = function(filePath) { | ||
var filename = path.basename(filePath, '.sql'); | ||
// Throw away trailing comments, blanks, etc. | ||
var lines = _.clone(sqlLines); | ||
while(lines.length) { | ||
if (!_.isEmpty(_.last(lines)) && !isComment(_.last(lines))) break; | ||
lines.pop(); | ||
} | ||
// If there was nothing but comments and blanks, we keep it. | ||
migration.template = lines.length ? lines.join('\n') : sqlLines.join('\n'); | ||
return migration; | ||
} | ||
function readlines(filePath) { | ||
var lines; | ||
var lineNum = 0; | ||
var onReady = fs.readFileAsync(filePath) | ||
.then(function(content) { | ||
lines = content.toString().split('\n'); | ||
return fs.readFileAsync(filePath).then(function(data) { | ||
return makeMigration( | ||
filePath, | ||
new Date(_.first(filename.split('_'))), | ||
_.rest(filename.split('_')).join('_'), | ||
data.toString() | ||
); | ||
}); | ||
onReady.done(); | ||
return { | ||
next: function() { | ||
return onReady.then(function() { | ||
lineNum += 1; | ||
return [lines[lineNum - 1], lineNum]; | ||
}); | ||
} | ||
}; | ||
} | ||
function isComment(line) { | ||
return /^--/.test(line); | ||
} | ||
function isMigrationComment(line) { | ||
return /^-- *## /.test(line); | ||
} | ||
function parseMigrationComment(line) { | ||
var match = /## +migration +([^ ]*) +(.*)/.exec(line); | ||
var date = new Date(match[1]); | ||
var description = match[2]; | ||
return makeMigration(date, description); | ||
} | ||
function error(lineNum, message) { | ||
var err = new Error('Migration parse error: line ' + lineNum + ': ' + message); | ||
Error.captureStackTrace(err, error); | ||
throw err; | ||
} | ||
module.exports = function(filePath) { | ||
var lineReader = readlines(filePath); | ||
function parseMigrations(lineReader) { | ||
var migrations = []; | ||
// Discard any junk before first migration header | ||
return (function next() { | ||
return lineReader.next() | ||
.spread(function(line, lineNum) { | ||
if (line === undefined) { | ||
return []; | ||
} | ||
line = _str.trim(line); | ||
if (isMigrationComment(line)) { | ||
return _parseMigrations(line, lineNum); | ||
} | ||
// Discard comments before migration header | ||
if (isComment(line) || _.isEmpty(line)) { | ||
return next(); | ||
} | ||
error(lineNum, 'Unexpected characters before migration header: ' + line); | ||
}); | ||
})(); | ||
// Parse migration starting with header line | ||
function _parseMigrations(startLine, lineNum) { | ||
var sqlLines = [startLine]; | ||
return Promise.try(parseMigrationComment, startLine) | ||
.catch(function(err) { | ||
error( | ||
lineNum, | ||
'Unable to parse migration header: ' + err.message); | ||
}) | ||
.then(function(migration) { | ||
// Read lines until EOF or next header | ||
return (function next() { | ||
return lineReader.next() | ||
.spread(function(line, lineNum) { | ||
if (isMigrationComment(line)) { | ||
setSql(migration, sqlLines); | ||
migrations.push(migration); | ||
return _parseMigrations(line, lineNum); | ||
} | ||
if (line === undefined) { | ||
migrations.push(setSql(migration, sqlLines)); | ||
return migrations; | ||
} | ||
sqlLines.push(line); | ||
return next(); | ||
}); | ||
}()); | ||
}); | ||
} | ||
} | ||
return parseMigrations(lineReader); | ||
}; |
{ | ||
"name": "dbeasy", | ||
"version": "0.10.1", | ||
"version": "0.11.1", | ||
"description": "Promise-based wrapper for postgresql driver that makes easy what should be easy while protecting your foot.", | ||
@@ -30,5 +30,7 @@ "main": "index.js", | ||
"handlebars": "1.3.0", | ||
"lodash": "^2.4.1", | ||
"lodash": "^3.10.1", | ||
"moment-timezone": "^0.5.4", | ||
"pg": "4.5.0", | ||
"pg-native": "1.10.0", | ||
"sprintf-js": "^1.0.3", | ||
"underscore.string": "2.3.3" | ||
@@ -35,0 +37,0 @@ }, |
@@ -62,3 +62,3 @@ "use strict"; | ||
var migrator; | ||
var migration; | ||
var SCHEMA = 'school'; | ||
@@ -69,3 +69,3 @@ setup(function() { | ||
migrator = makeMigrator(client); | ||
return migrator.clearMigrations('school') | ||
return migrator.clearMigrations(SCHEMA) | ||
.then(function() { | ||
@@ -81,5 +81,5 @@ return layout(client).dropNamespace('school'); | ||
sql: 'CREATE TABLE school.classroom();\nCREATE TABLE school.cafeteria();' | ||
}], {schema: 'school'}); | ||
}]); | ||
return migrator.runPending() | ||
return migrator.runPending(SCHEMA) | ||
.then(function() { | ||
@@ -106,5 +106,5 @@ return Promise.all([ | ||
sql: 'CREATE TABLE school.cafeteria();' | ||
}], {schema: 'school'}); | ||
}]); | ||
return migrator.runPending() | ||
return migrator.runPending(SCHEMA) | ||
.then(function() { | ||
@@ -122,2 +122,58 @@ return Promise.all([ | ||
test('Identify missed migrations', function() { | ||
migrator.addMigrations([{ | ||
date: new Date('2014-11-12T01:24'), | ||
description: 'noop', | ||
sql: 'SELECT;' | ||
},{ | ||
date: new Date('2014-12-12T01:24'), | ||
description: 'noop', | ||
sql: 'SELECT;' | ||
}]); | ||
return Promise.resolve() | ||
.then(function() { | ||
return migrator | ||
.getStatus(SCHEMA) | ||
.then(_.spread(function(items, hasPending, hasMissing) { | ||
expect(hasPending).to.equal(true); | ||
expect(hasMissing).to.equal(false); | ||
})); | ||
}) | ||
.then(function() { | ||
return migrator | ||
.runPending(SCHEMA) | ||
.then(function() { | ||
return migrator.getStatus(SCHEMA); | ||
}) | ||
.then(_.spread(function(items, hasPending, hasMissing) { | ||
expect(hasPending).to.equal(false); | ||
expect(hasMissing).to.equal(false); | ||
})); | ||
}) | ||
.then(function() { | ||
migrator = makeMigrator(client); | ||
migrator.addMigrations([{ | ||
date: new Date('2014-11-13T01:24'), | ||
description: 'noop', | ||
sql: 'SELECT;' | ||
}]); | ||
return migrator | ||
.getStatus(SCHEMA) | ||
.then(_.spread(function(items, hasPending, hasMissing) { | ||
expect(hasPending).to.equal(false); | ||
expect(hasMissing).to.equal(true); | ||
expect(items[1]).to.eql({ | ||
date: new Date('2014-11-13T01:24'), | ||
description: 'noop', | ||
sql: 'SELECT;', | ||
isCommitted: false, | ||
candidateStatus: 'MISSING' | ||
}); | ||
})); | ||
}); | ||
}); | ||
test('Do not allow misordered migrations', function() { | ||
@@ -130,3 +186,3 @@ return Promise.try(function() { | ||
},{ | ||
date: new Date('2014-11-12T01:24'), | ||
date: new Date('2014-11-12T01:23'), | ||
description: 'create rooms', | ||
@@ -163,6 +219,6 @@ sql: '' | ||
sql: 'CREATE TABLE school.classroom();' | ||
}], {schema: 'school'}); | ||
return migrator.runPending() | ||
}]); | ||
return migrator.runPending(SCHEMA) | ||
.then(function() { | ||
return migrator.runPending(); | ||
return migrator.runPending(SCHEMA); | ||
}) | ||
@@ -175,3 +231,3 @@ .then(function() { | ||
}], {schema: 'school'}); | ||
return migrator.runPending(); | ||
return migrator.runPending(SCHEMA); | ||
}); | ||
@@ -198,3 +254,3 @@ | ||
.then(function() { | ||
return migrator.runPending(); | ||
return migrator.runPending(SCHEMA); | ||
}) | ||
@@ -213,7 +269,5 @@ .then(function() { | ||
migrator.templateVars['table'] = 'school.classroom'; | ||
return migrator.loadMigrations( | ||
testSqlPath + '11_create_table.sql.hbs', | ||
{schema: 'school'}) | ||
return migrator.loadMigrations(testSqlPath + '11_create_table.sql.hbs') | ||
.then(function() { | ||
return migrator.runPending(); | ||
return migrator.runPending(SCHEMA); | ||
}) | ||
@@ -230,2 +284,2 @@ .then(function() { | ||
}); | ||
}); |
72864
2033
9
4
+ Addedmoment-timezone@^0.5.4
+ Addedsprintf-js@^1.0.3
+ Addedlodash@3.10.1(transitive)
+ Addedmoment@2.30.1(transitive)
+ Addedmoment-timezone@0.5.47(transitive)
+ Addedsprintf-js@1.1.3(transitive)
- Removedlodash@2.4.2(transitive)
Updatedlodash@^3.10.1