dbmon
Version:
Database and Filesystem Monitor Utilities for Real Time Apps
194 lines (163 loc) • 7.03 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 Polling 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 startPolling=function(){
var tab=name(opts, 'history')+'_table', q;
q='select * from '+tab+' order by id desc limit 1';
cli.query(q, function(err,res){
var lastId=res && res.rows && res.rows.length ? res.rows[0].id : -1;
var poll=function(){
q='select * from '+tab+' where id>'+lastId+' order by id';
// clog('pppppppppp poll, lastId='+lastId+', q='+q);
cli.query(q, function(err,res){
if (err) console.dir(err);
if (res && res.rows && res.rows.length){
_.each(res.rows, function(r){
var op={i:'insert',u:'update',d:'delete'}[r.op];
// clog('emitting "'+op+'" for '+JSON.stringify(r));
me.emit(op, [r]);
});
lastId=res.rows[res.rows.length-1].id;
}
setTimeout(poll, 1000);
});
};
poll();
});
};
Step(
function createHistoryTableIfNecessary(){
if (opts.keyfld.name && opts.keyfld.type){
cli.query(historyTableStr(opts), this);
}else{
console.log('postgresql-polling-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); }
var afterAll=_.after(types.length, startPolling);
_.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); }
afterAll();
});
});
});
}
);
me.stop=function(callback){
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};