dbmon
Version:
Database and Filesystem Monitor Utilities for Real Time Apps
212 lines (180 loc) • 8.02 kB
JavaScript
//Trigger method for postgresql driver
var events=require('events'), _=require('underscore')._, Step=require('step');
var clog=function(text){
console.log(Date.now()+' '+text);
};
/** Return a base name for functions and trigger based on table name and options */
var name=function name(opts, type){
var rv;
if (opts.driverOpts.postgresql.baseObjectsName){
rv='dbmon_'+opts.driverOpts.postgresql.baseObjectsName+'_'+type;
}else{
rv='dbmon_'+opts.table+'_'+type+'_trigger';
if (opts.addflds && opts.addflds.length){
rv+='_'+_.map(opts.addflds,function(f){return f.name;}).join('_');
}
if (opts.cond){
//Dynamic names lets create more than one trigger and trigger fn
rv+='_'+opts.cond.replace(/\W/g, '_').replace(/_+/g, '_');
}
}
//63=Max postgresql function length
return rv.substr(0, 55);
};
/** Compose and returns CREATE FUNCTION query */
var triggerFnStr=function triggerFnStr(type, opts){
if (opts.table){
var n=name(opts, type),
historyTable=name(opts, 'history')+'_table',
shortType=type.charAt(0),
rec=type==='delete'?'OLD':'NEW',
cond=opts.cond && type!=='truncate'?_.template(opts.cond, {rec:rec}):'',
//Additional fields support
addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name;}).join(','):'',
addfldsNulls=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return 'NULL';}).join(','):'',
addfldsValues=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return rec+'.'+f.name;}).join(','):'';
var rv='CREATE OR REPLACE FUNCTION '+n+'_fn() RETURNS trigger AS $$\n'+
'DECLARE\n'+
'BEGIN\n'+
(cond?'IF '+cond+'\nTHEN\n':'')+
(opts.keyfld.name && opts.keyfld.type?
'INSERT INTO '+historyTable+' (op, k, oldk'+addflds+')'+
(type==='truncate'?
'VALUES (\''+shortType+'\', NULL, NULL'+addfldsNulls+');\n':
//If UPDATE, save also the old key field value
'VALUES (\''+shortType+'\', '+rec+'.'+opts.keyfld.name+', '+(type==='update'?'OLD.'+opts.keyfld.name:rec+'.'+opts.keyfld.name)+addfldsValues+');\n'
)
:'')+
'NOTIFY '+n+';\n'+
(cond?'END IF;\n':'')+
'RETURN '+rec+';\n'+
'END;\n'+
'$$ LANGUAGE plpgsql;';
return rv;
}else{
console.log('postgresql-trigger-method.js, opts.table REQUIRED');
}
};
/** Compose and returns CREATE TRIGGER query */
var triggerStr=function triggerStr(type, opts){
var n=name(opts, type);
var rv='CREATE TRIGGER '+n+' AFTER '+type+' ON '+opts.table+' FOR EACH '+(type==='truncate'?'STATEMENT':'ROW')+' EXECUTE PROCEDURE '+n+'_fn();';
return rv;
};
/** Compose and returns CREATE TABLE query for history table */
var historyTableStr=function historyTableStr(opts){
var n=name(opts, 'history')+'_table';
var addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name+' '+f.type;}).join(','):'';
var rv= //'DROP TABLE IF EXISTS '+n+' CASCADE; '+
'CREATE TABLE '+n+' (id serial primary key, op char(1), k '+opts.keyfld.type+', oldk '+opts.keyfld.type+addflds+');';
return rv;
};
/** Main function, returns the main EventEmitter object used by the driver */
var init=function init(opts){
console.log('PostgreSQL Trigger Method Init');
//The returned object is an eventemitter, so others can listen for events easily
var me=new events.EventEmitter(),
cli=opts.driverOpts[opts.driver].cli;
if (!cli && opts.driverOpts[opts.driver].connStr){
var pg=require('pg');
cli=new pg.Client(opts.driverOpts[opts.driver].connStr);
cli.connect();
}
//Normalize type of events
var types=opts.monitor.split(',');
types=_.map(types, function(t){return t.trim().toLowerCase()});
//Required for PostgreSQL 8.x
cli.query('create language plpgsql;', function(){ /*plpgsql is created only the first time, an error could occour*/ });
//Time to notify all listeners
var historyId=-1, historyTable=name(opts, 'history')+'_table',
addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name;}).join(','):'',
historySql='select op,k,oldk,id'+addflds+' from '+historyTable+' where id>$1 order by id';
var historyIdCache={};
var onNotification=function(type){
// clog('dbmon, onNotification, historyTable='+historyTable);
if (opts.keyfld.name && opts.keyfld.type){
var shortType=type.charAt(0);
(function(historyIdMemo){
if (!historyIdCache[historyIdMemo]){
historyIdCache[historyIdMemo]=true;
// clog('postgresql-trigger-method.js, onNotification, before query, historyId='+historyId+', historyIdMemo='+historyIdMemo);
cli.query(historySql, [historyIdMemo], function(err, res){
delete historyIdCache[historyIdMemo];
if (historyId===historyIdMemo){
// clog('postgresql-trigger-method.js, onNotification, historyId===historyIdMemo==='+historyId+', rows='+res.rows.length);
if (res.rows.length){
historyId=res.rows[res.rows.length-1].id;
// clog('postgresql-trigger-method.js, onNotification, new historyId='+historyId);
me.emit(type, res.rows);
}else{
clog('postgresql-trigger-method.js, onNotification, res.ros.length=0');
}
}else{
clog('postgresql-trigger-method.js, onNotification, historyId='+historyId+', historyIdMemo='+historyIdMemo);
}
});
}
})(historyId);
}else{
me.emit(type);
}
};
//If keyfld is not specified, I'll try to notify always clients for changes */
if (opts.keyfld.name && opts.keyfld.type && opts.debouncedNotifications){
onNotification=_.debounce(onNotification, opts.debouncedNotifications);
}
Step(
function createHistoryTableIfNecessary(){
if (opts.keyfld.name && opts.keyfld.type){
cli.query(historyTableStr(opts), this);
}else{
console.log('postgresql-trigger-method.js, history table not created, opts.keyfld or opts.keytype not valid');
this();
}
},
function emptyHistoryTable(err){
if (err){ console.log(err.message); }
var n=name(opts, 'history')+'_table';
cli.query('truncate table '+n, this);
},
function createTriggerStuff(err){
if (err){ console.log(err.message); }
_.each(types, function(type){
var chname=name(opts, type);
cli.query(triggerFnStr(type, opts), function(err){
if (err){ console.log(err.message); }
cli.query(triggerStr(type, opts), function(err){
if (err){ console.log(err.message); }
//Listening for NOTIFYs
cli.query('LISTEN '+chname, function(err){
if (err){ console.log(err.message); }
});
cli.on('notification', function(data){
if (data.channel===chname){
onNotification(type);
}
});
});
});
});
}
);
me.stop=function(callback){
console.log('postgresql-trigger-method stop called');
var asyncCallback=_.after((types.length*2)+1, function(){
//check if db connection has been made by dbmon
if (!opts.driverOpts[opts.driver].cli && cli){
cli.end();
}
callback();
});
cli.query('DROP TABLE IF EXISTS '+name(opts, 'history')+'_table CASCADE', asyncCallback);
_.each(types, function(t){
var iname=name(opts, t);
cli.query('DROP TRIGGER IF EXISTS '+iname+' on '+opts.table+' CASCADE', asyncCallback);
cli.query('DROP FUNCTION IF EXISTS '+iname+'_fn() CASCADE', asyncCallback);
});
};
return me;
};
module.exports={init:init};