├── .gitignore ├── History.md ├── Makefile ├── Readme.md ├── bin └── dbmon ├── index.js ├── lib ├── channelDefaults.js ├── dbmon.js ├── drivers │ ├── filesystem-driver.js │ ├── generic-driver.js │ └── postgresql-driver.js ├── methods │ ├── filesystem-inotifywait-method.js │ ├── postgresql-polling-method.js │ └── postgresql-trigger-method.js └── transports │ ├── console-transport.js │ ├── eventEmitter-transport.js │ ├── faye-transport.js │ ├── nowjs-transport.js │ ├── socketio-transport.js │ └── tcpsocket-transport.js ├── package.json └── test ├── test-filesystem-faye.js ├── test-filesystem-inotifywait.js ├── test-postgresql.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | COMMIT 2 | 3 | node_modules 4 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 1.1.0 / 2013-06-21 2 | ================== 3 | * dbmon cli lets you monitor tables from command line, without writing a single line of JS code 4 | * added postgresql polling method; uses triggers but not notify/listen 5 | 6 | 1.0.7 / 2012-01-14 7 | ================== 8 | * addlflds option added, see channelDefaults.js for an example usage 9 | 10 | 1.0.6 / 2011-11-08 11 | ================== 12 | * Added `cond` parameter that lets generate events only when the SQL condition is true. It's usage is deferred to driver methods; for postgresql, you can pass a SQL condition referring to NEW or OLD records inside trigger function. `cond` is evaluated ad an `underscore` template at runtime passing an object with a rec property that can be NEW (for insert and update) or OLD (for delete). Example usage, valid for insert/update/delete: `cond:"<%= rec %>.name='YOUR NAME'"` 13 | * Added `channel.stop(callaback)` support, deferred to method.stop implementation. PostgreSQL detroy triggers, trigger functions and history tables if stop is called 14 | * `channel.stop()` test integration 15 | 16 | 1.0.5 / 2011-11-05 17 | ================== 18 | * Added Nowjs transport for notifying real-time changes directly to browser clients very very easily 19 | * For the nowjs transport, the `fn` option can be an underscore template string that will be compiled at runtime with the row returned to the client; example opts: `{transports:'nowjs',transportsOpts:{nowjs:{fn:'onChangeKey<%= k %>'}}}`; k will be the row key when the event occur 20 | 21 | 1.0.4 / 2011-10-27 22 | ================== 23 | * Added Faye transport for notifying real-time changes via websocket 24 | * Tests improvements via Makefile (make test) 25 | * Readme updated 26 | * General bugfix and improvements 27 | 28 | 1.0.3 / 2011-10-26 29 | ================== 30 | * Added the filesystem driver and the inotifywait method; filesystem database emulation to have real-time file change notification using inotifywait child_process 31 | * Test bugfix and refactoring 32 | 33 | 1.0.2 / 2011-10-25 34 | ================== 35 | 36 | * Added the possibility to notify not only if something changes, but also what have changed, see channelDefaults.keyfld 37 | * Added the truncate monitoring via TRIGGER for postgresql driver 38 | 39 | 1.0.1 / 2011-10-24 40 | ================== 41 | 42 | * Initial release 43 | * Added PostgreSQL driver with TRIGGER and LISTEN/NOTIFY support added 44 | * Added Console and EventEmitter transports 45 | 46 | 1.0.0 / 2011-10-22 47 | ================== 48 | 49 | * Idea 50 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | node-command := xargs -n 1 -I file node file 4 | 5 | .PHONY : test 6 | 7 | test: 8 | @find test -name "test-*.js" | $(node-command) 9 | 10 | test-all: test 11 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Database and Filesystem monitor utilities for nodejs 2 | 3 | If you are trying to update a GUI when a database table changes (_insert_, _update_, _delete_) or when a file is being created/modified/deleted/moved, this library is for you. 4 | 5 | This is a node.js module supporting a growing number of database drivers and notification transports 6 | you can extend and improve. 7 | 8 | It is designed to be easily extended with simple sintax by anyone and, where possibile, 9 | to notify of changes without classic polling, but with real-time notification mechanism 10 | 11 | 12 | ## Usage sample 13 | 14 | This is a short example of the PostgreSQL driver; you can find more on `test/test-postgresql.js` 15 | 16 | Install a local postgresql database server; grant temporary trust access to the postgresql 17 | user editing the pg_hba.conf file and create a test table like this `create table testtable(id integer primary key, val varchar(10));` 18 | then run the following 19 | 20 | var pg=require('pg'), cli=new pg.Client('tcp://postgres@localhost/template1'), dbmon=require('dbmon'); 21 | 22 | cli.connect(); 23 | //uncomment if you want node to create the temporary table for you 24 | //cli.query('drop table if exists testtable; create table testtable(id integer primary key, val varchar(10));'); 25 | 26 | var channel=dbmon.channel({ 27 | driver:'postgresql', 28 | driverOpts:{ 29 | postgresql:{ 30 | cli:cli 31 | } 32 | }, 33 | table:'testtable', 34 | monitor:'all', 35 | keyfld:{ 36 | name:'id',type:'integer' 37 | } 38 | }); 39 | 40 | Now monitor the console and execute some insert/update/delete and see what happens... 41 | 42 | You should see come console messages saying you are modifiyng `testtable` like this 43 | 44 | Console Transport Notification: insert, row={"op":"i","k":2,"oldk":2,"id":1} 45 | 46 | In this case I've executed a simple insert like `insert into testtable values(2,'TWO');`. 47 | Console says that the type of notification is an insert and the row modified 48 | from last notification is `{"op":"i","k":2,"oldk":2,"id":1}` where fields means: 49 | 50 | - *op* is the operation type; can be *i* for insert, *u* for update, *d* for delete and *t* for truncate 51 | - *k* is the key inserted/updated/deleted based on what specified in `keyfld.name` 52 | - *oldk* is the old key value, see what happens executing `update testtable set id=20, val='twenty' where id=2` 53 | - *id* is an internal change sequence id, an ordered number useful to keep track of modifications 54 | 55 | It is very interesting to know that if you update 2 or more rows in the same transaction, there will 56 | arrive 2 ore more notifications based on the number or rows being modified 57 | 58 | Another good thing is that for PostgreSQL, *dbmon* is powered by the NOTIFY/LISTEN constructs. It means 59 | that, when something changes, the server that contacts node and node notify listeners via the transports specified, making 60 | it really real-time, not like other polling-based alternatives. 61 | 62 | To see the complete list of options see [lib/channelDefaults.js](https://github.com/straps/node-dbmon/blob/master/lib/channelDefaults.js) 63 | 64 | ### Dbmon cli 65 | 66 | Dbmon has also an executable called `dbmon`. 67 | 68 | With it you can start a socket.io server or a console db monitoring program without writing a single line of code. 69 | 70 | Sample usage: 71 | 72 | dbmon --driver=postgresql --driverOpts-postgresql-connStr=tcp://user:pwd@127.0.0.1:5432/db --driverOpts-postgresql-baseObjectsName=test_dbmon_cli --table=mytable --keyfld-name=id --keyfld-type=integer --transports=console,socketio --transportsOpts-socketio-port=8888 73 | 74 | Parameters are tranformed to a JSON object as expected by dbmon and like discussed before. 75 | 76 | In this case, the resulting json will be: 77 | 78 | { 79 | "driver": "postgresql", 80 | "driverOpts": { 81 | "postgresql": { 82 | "connStr": "tcp://user:pwd@127.0.0.1:5432/db", 83 | "baseObjectsName": "test_dbmon_cli" 84 | } 85 | }, 86 | "table": "mytable", 87 | "keyfld": { 88 | "name": "id", 89 | "type": "integer" 90 | }, 91 | "transports": "console,socketio", 92 | "transportsOpts": { 93 | "socketio": { 94 | "port": 8888 95 | } 96 | } 97 | } 98 | 99 | dbmon will start a channel passing that object as input, will monitor for *mytable* changes and will run a socket.io websocket on port 8888 for real-time web updates. Simple as effective... 100 | 101 | 102 | ### Sample for the new filesystem driver 103 | 104 | On linux, you can experiment the new `filesystem` driver. It is based on `inotifywait`, a linux command line utility 105 | that helps you monitor for file changes; on ubuntu you can install it by typing 106 | 107 | sudo apt-get install inotify-tools 108 | 109 | Now Execute this code 110 | 111 | require('dbmon').channel({ 112 | driver:'filesystem', 113 | driverOpts:{filesystem:{root:'/home'}}, 114 | method:'inotifywait', 115 | transports:'console' 116 | }); 117 | 118 | and monitor the console when you create/modify/delete files on your home directory or subdirectories (Desktop too). FUN 119 | 120 | ## Structure and Naming Conventions 121 | 122 | Dbmon is designed to be dynamic and easily extensible; there are 3 main actors to extend it 123 | 124 | - **transports**, in [lib/transports](https://github.com/straps/node-dbmon/tree/master/lib/transports) are the way dbmon notify events. You can use how many tranports you want separating them by comma. The name specified in the options object have to match the name of the file followed by `-tranport.js` in the `transports` foler, like the `console` transport in the example above. 125 | - **providers**, in [lib/providers](https://github.com/straps/node-dbmon/tree/master/lib/providers), have to initialize their method to fetch data and notify transports whene something happen; in most cases (surely for postgresql case) they should only require the `generic-driver` that dynamiccaly instantiate the method and notify transports 126 | - **methods**, in [lib/methods](https://github.com/straps/node-dbmon/tree/master/lib/methods), are the core of the system; their implementation depends upon the driver and the method specified in the configuration object and their name should respect `DRIVER-METHOD-method.js` convention (ie: postgresql-trigger-method.js). Methods init function return an `EventEmitter` inherited object that notify listeners where data changes firing the event notification chain 127 | 128 | 129 | ### How To Create a new Transport 130 | 131 | Creating a new transport is very simple; the node module have to export a single function `init` that `dbmon` will call passing the global options object. 132 | 133 | The `init` function have to return an object with a `notify` method, magically called from drivers, when something server side changes. 134 | 135 | Say we want a generic TCP Socket transport to communicate with another application, transmitting db update notification. 136 | 137 | Create the file `lib/transports/tcpsocket.js` and insert the following lines: 138 | 139 | //TCP Socket Tranport 140 | var init=function init(opts){ 141 | console.log('TCP Socket Transport init'); 142 | var me={ 143 | notify:function(type, row){ 144 | opts.transportsOpts.tcpsocket.client.write(JSON.stringify(row)); 145 | return me; 146 | } 147 | }; 148 | return me; 149 | }; 150 | module.exports={init:init}; 151 | 152 | Now use it from your node.js server socket app: 153 | 154 | var net = require('net'); 155 | var server = net.createServer(function (c) { 156 | c.on('data', function(data){ 157 | console.log('DATA FROM SOCKET HURRAAA --> '+data); 158 | }); 159 | }); 160 | server.listen(8124, 'localhost', function(){ 161 | var client=new net.Socket(); 162 | client.connect(8124, 'localhost', function(){ 163 | console.log('connected'); 164 | 165 | var pg=require('pg'), cli=new pg.Client('tcp://postgres@localhost/template1'), dbmon=require('dbmon'); 166 | cli.connect(); 167 | 168 | dbmon.channel({ 169 | driver:'postgresql', 170 | driverOpts:{ 171 | postgresql:{ 172 | cli:cli 173 | } 174 | }, 175 | table:'testtable', 176 | method:'trigger', 177 | transports:'tcpsocket', 178 | transportsOpts:{ 179 | tcpsocket:{ 180 | client:client 181 | } 182 | }, 183 | keyfld:{ name:'id', type:'integer'} 184 | }); 185 | }); 186 | }); 187 | 188 | In 20 lines of (uncompressed) code you can create and use a new tranport, contribute to the library and make others happy (me too :) 189 | 190 | Creating a new driver and a new driver method, could be some more complicated, but I thing, in next releases will be a generic 191 | mixed trigger/polling based driver I'm thinking on. 192 | 193 | 194 | ## Testing 195 | 196 | Test cases are home-made and could not be complete or well done, so feel free to fork and improve tests too. 197 | 198 | In any case, you can test the library doing a `make test` from main directory 199 | 200 | 201 | ## Installation 202 | 203 | Using npm: `npm install dbmon` 204 | 205 | Or `npm install dbmon -g` and `npm link dbmon` if you prefer linking a global installation 206 | 207 | Or you can download/fork and copy on a local folder inside your project 208 | 209 | 210 | ### External Dependencies, automatically installed if you use npm 211 | 212 | - [Underscore.js](http://documentcloud.github.com/underscore/) (`npm install underscore`) 213 | - [Step](https://github.com/creationix/step) (`npm install step`) 214 | 215 | Database drivers, depends on the driver you use, including 216 | 217 | - [Pg](https://github.com/brianc/node-postgres) (`npm install pg`) 218 | - [inotifywait](https://github.com/rvoicilas/inotify-tools/wiki/) (`sudo apt-get install inotify-tools`); required for `{driver:'filesystem',method:'inotifywait',...}` 219 | 220 | Transports drivers, depends on the transports you use, including 221 | 222 | - [Faye](http://faye.jcoglan.com/) (`npm install faye`) 223 | - [Nowjs](http://nowjs.com/) (`npm install now`) 224 | 225 | Only for test 226 | 227 | - [Colors](https://github.com/Marak/colors.js) (`npm install colors`) 228 | 229 | 230 | 231 | ## ToDo 232 | - Develop other drivers (MySQL, Oracle, MsSQL, etc...) 233 | - Develop other transports (Hook.io, etc..) 234 | - Write better unit tests 235 | 236 | 237 | ## License 238 | 239 | (The MIT License) 240 | 241 | Copyright (c) 2011 Francesco Strappini 242 | 243 | Permission is hereby granted, free of charge, to any person obtaining 244 | a copy of this software and associated documentation files (the 245 | 'Software'), to deal in the Software without restriction, including 246 | without limitation the rights to use, copy, modify, merge, publish, 247 | distribute, sublicense, and/or sell copies of the Software, and to 248 | permit persons to whom the Software is furnished to do so, subject to 249 | the following conditions: 250 | 251 | The above copyright notice and this permission notice shall be 252 | included in all copies or substantial portions of the Software. 253 | 254 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 255 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 256 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 257 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 258 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 259 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 260 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 261 | -------------------------------------------------------------------------------- /bin/dbmon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | //Run a dbmon process on the fly converting parameters to a JSON Object similar to ../lib/channelDefaults.js 4 | var dbmon = require('../'), 5 | argv = require('optimist').argv, 6 | _=require('underscore')._, 7 | clog=function(x){ console.log(x); }, 8 | cdir=function(x){ console.dir(x); }; 9 | 10 | //convert arguments to dbmon config object, ex: 11 | // --driver=postgres ==> { driver: 'postgresql' } 12 | var argv2Obj=function(argv){ 13 | var rv={}; 14 | _.each(argv,function(v,k){ 15 | var subKeys=k.split('-'), subObject=rv, subKey; 16 | while (subKeys.length){ 17 | subKey=subKeys.splice(0,1); 18 | subObject[subKey] = subObject[subKey] || {}; 19 | if (subKeys.length){ 20 | subObject=subObject[subKey]; 21 | } 22 | } 23 | subObject[subKey]=v; 24 | }); 25 | return rv; 26 | }; 27 | 28 | if (process.argv.length>2){ 29 | //convert arguments to object 30 | var channelOpts=argv2Obj(_.omit(argv,'_','$0')); 31 | 32 | clog("Starting dbmon, channelOpts=\n"+JSON.stringify(channelOpts, null, 2)); 33 | 34 | var channel=dbmon.channel(channelOpts); 35 | 36 | var onExitSignal=function(){ 37 | channel.stop(function(){ 38 | process.exit(0); 39 | }); 40 | }; 41 | 42 | process.on('SIGINT', onExitSignal); 43 | process.on('SIGTERM', onExitSignal); 44 | 45 | }else{ 46 | clog('Usage: '+process.argv[0]+' '+process.argv[1]+' [arguments...]'); 47 | clog(' [arguments...] will be converted to json, ie: '); 48 | clog(' --driver=postgresql --driverOpts-postgresql-connStr=tcp://user:pwd@ip:port/db ==> {driver:\'postgresql\', driverOpts:{postgresql:{connStr:\'tcp://user:pwd@ip:port/db\'}}}'); 49 | clog('Dbmon Specs on ../lib/channelDefaults.js'); 50 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/dbmon'); -------------------------------------------------------------------------------- /lib/channelDefaults.js: -------------------------------------------------------------------------------- 1 | /** Dbmon channel defaults */ 2 | var d={ 3 | /** Database driver to use; postgresql, mysql, oracle, etc... 4 | REQUIRED */ 5 | driver: null, 6 | 7 | /** Driver dedicated opts */ 8 | driverOpts:{ 9 | postgresql:{ 10 | /** connected client, required if connStr is null */ 11 | cli:null, 12 | 13 | /** connection string, required if cli is null, in the classical form tcp://user:pwd@ip:port/db */ 14 | connStr:null, 15 | 16 | /** Table/Function/Trigger base name; if null, a base name will be generated combining other options */ 17 | baseObjectsName:null 18 | } 19 | }, 20 | 21 | /** Table to monitor for changes, 22 | REQUIRED */ 23 | table: null, 24 | /** Key field info, returned as k where notifying changes */ 25 | keyfld:{ 26 | /** Key Field Name, ie: 'id' */ 27 | name:null, 28 | /** Key Field Type, ie: 'integer', or 'varchar(100)', etc.. */ 29 | type:null 30 | }, 31 | /** Additional fields to send to listeners 32 | Can also be an object where the key is the field name and value is his type, ie: 33 | addflds:{description:'varchar(250)'} 34 | */ 35 | addflds:[ 36 | /* example: 37 | {name:'description', type:'varchar(250)'} 38 | */ 39 | ], 40 | /** Trigger condition; notification are sent only if it evaluates to true; 41 | Can be an underscore template; input will be {rec:'NEW'} for insert/update or {rec:'OLD'} for delete; 42 | it is not considered for filesystem driver and for truncate triggers. 43 | Examples: cond:"<%= rec %>.codline='line01'" */ 44 | cond: null, 45 | 46 | /** What to monitor, comma separated list of insert,update,delete,truncate or all */ 47 | monitor: 'all', 48 | 49 | /** Type of monitor, trigger or polling */ 50 | method: 'trigger', 51 | /** Options dedicated to method types */ 52 | methodOpts:{ 53 | trigger:{}, 54 | polling:{} 55 | }, 56 | 57 | /** Comma separated list of transports, console, eventEmitter, tcp, faye, nowjs, socketio, more to come, etc.. */ 58 | transports: 'console', 59 | transportsOpts:{ 60 | eventEmitter:{ 61 | /** if transports contains eventEmitter, this is REQUIRED */ 62 | eventEmitter:null 63 | }, 64 | socketio:{ 65 | /** Can be an http server object or an express object */ 66 | server:null, 67 | /** Socket.io core object to notify clients, created at runtime if null */ 68 | io:null, 69 | /** Port to listen to, required if server and io are null */ 70 | port:null, 71 | 72 | /** Event emitted when something changes 73 | Can be passed an Undercore.js template as specified here http://documentcloud.github.com/underscore/#template 74 | Parameters of the template will be the row notified containing fields k,oldk,op,id, ie: 75 | fn:'changeKey<%= k %>' */ 76 | event:'dbmonNotify', 77 | /** Socket.io namespace support */ 78 | namespace:'' 79 | }, 80 | faye:{ 81 | /** Can be an http server object or an express object, if null faye initialize a 82 | it's own http server communicating on specified port */ 83 | server:null, 84 | port:8000, 85 | /** Faye mount and timeout, see: http://faye.jcoglan.com/node.html */ 86 | mount:'/faye', 87 | timeout: 45, 88 | /** Channel to publish updates on; _TYPE_ is replaced at runtime with insert/update/delete based on type of update */ 89 | channel:'/dbmon' 90 | }, 91 | nowjs:{ 92 | /** Can be an http server object or an express object */ 93 | server:null, 94 | /** Function name to call for notifying clients 95 | Can be passed an Undercore.js template as specified here http://documentcloud.github.com/underscore/#template 96 | Parameters of the template will be the row notified containing fields k,oldk,op,id, ie: 97 | fn:'changeKey<%= k %>' */ 98 | fn:'dbmonNotify', 99 | /** Nowjs core object to notify clients, created at runtime if null */ 100 | everyone:null 101 | } 102 | }, 103 | 104 | /** Debounce notification support, avoid server and listeners overload on frequent updates; 0=debounce disabled */ 105 | debouncedNotifications:100 106 | }; 107 | 108 | module.exports={channelDefaults:d}; -------------------------------------------------------------------------------- /lib/dbmon.js: -------------------------------------------------------------------------------- 1 | // DbMon - Copyright Francesco Strappini (MIT Licensed) 2 | var _=require('underscore')._, 3 | channelDefaults=require('./channelDefaults').channelDefaults; 4 | 5 | var dbmon={ 6 | version : '1.0.7' 7 | }; 8 | 9 | dbmon.channel = function(opts){ 10 | opts=_.extend({}, channelDefaults, opts); 11 | if (opts.monitor==='all') { 12 | opts.monitor='insert,update,delete,truncate'; 13 | } 14 | 15 | //Dynamic Transports Init 16 | var transports=[]; 17 | _.each(opts.transports.split(','), function(t){ 18 | t=t.trim(); 19 | //underscore does not support deep extend 20 | if (channelDefaults.transportsOpts[t]){ 21 | opts.transportsOpts[t]=_.extend({}, channelDefaults.transportsOpts[t], opts.transportsOpts[t]); 22 | } 23 | if (t){ 24 | transports.push(require('./transports/'+t+'-transport').init(opts)); 25 | } 26 | }); 27 | 28 | //Main Object to Return 29 | var me={ 30 | //Dynamic driver initialization 31 | // driver:require('./drivers/'+opts.driver+'-driver').init(opts, transports), 32 | 33 | transports:transports, 34 | 35 | stop:function(callback){ 36 | callback=callback || function(){}; 37 | me.driver.stop(callback); 38 | } 39 | }; 40 | me.driver=require('./drivers/'+opts.driver+'-driver').init(opts, transports); 41 | 42 | return me; 43 | }; 44 | 45 | module.exports=dbmon; -------------------------------------------------------------------------------- /lib/drivers/filesystem-driver.js: -------------------------------------------------------------------------------- 1 | console.log('Loading Filesystem Driver'); 2 | module.exports=require('./generic-driver'); -------------------------------------------------------------------------------- /lib/drivers/generic-driver.js: -------------------------------------------------------------------------------- 1 | var _=require('underscore')._; 2 | 3 | var init=function init(opts, transports){ 4 | console.log('Generic Driver Init'); 5 | 6 | var shorts={'i':'insert','u':'update','d':'delete','t':'truncate'}; 7 | 8 | //Make opts.addflds an object array in case it is a pure object 9 | //{description:'varchar(100)'} ==> [{name:'description', type:'varchar(100)'}] 10 | if (opts.addflds){ 11 | if (_.isObject(opts.addflds) && !_.isArray(opts.addflds)){ 12 | var addflds=[]; 13 | _.each(opts.addflds, function(type,name){ 14 | addflds.push({name:name, type:type}); 15 | }); 16 | opts.addflds=addflds; 17 | } 18 | } 19 | 20 | //Dynamic Method Init 21 | var method=require('../methods/'+opts.driver+'-'+opts.method+'-method').init(opts); 22 | _.each(_.values(shorts), function(op){ 23 | method.on(op, function(rows){ 24 | // console.log('generic-driver method.on('+op+')'); 25 | if (rows && rows.length){ 26 | _.each(rows, function(row){ 27 | _.each(transports, function(t){ 28 | t.notify(shorts[row.op], row); 29 | }); 30 | }); 31 | }else{ 32 | t.notify(op); 33 | } 34 | }); 35 | }); 36 | 37 | var me={ 38 | stop:function(callback){ 39 | method.stop(callback); 40 | } 41 | }; 42 | return me; 43 | }; 44 | 45 | module.exports={init:init}; -------------------------------------------------------------------------------- /lib/drivers/postgresql-driver.js: -------------------------------------------------------------------------------- 1 | console.log('Loading PostgreSQL Driver'); 2 | module.exports=require('./generic-driver'); 3 | -------------------------------------------------------------------------------- /lib/methods/filesystem-inotifywait-method.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn, 2 | events=require('events'), 3 | _=require('underscore'); 4 | 5 | var init=function init(opts){ 6 | var me=new events.EventEmitter(); 7 | 8 | var nChanges=0; 9 | 10 | var inotifywait = spawn('inotifywait', ['-m', '-r', '-q', '--format', '"%e %w%f"', opts.driverOpts[opts.driver].root]); 11 | 12 | console.log('Monitoring '+opts.driverOpts[opts.driver].root+' and subfolders'); 13 | 14 | inotifywait.stderr.setEncoding('utf8'); 15 | inotifywait.stderr.on('data', function (data) { 16 | if (/^execvp\(\)/.test(data)) { 17 | console.log('ERR: Failed to start inotifywait process.'); 18 | } 19 | }); 20 | 21 | inotifywait.stdout.on('data', function(data){ 22 | data=''+data; 23 | _.each(data.split('\n'), function(row){ 24 | if (row){ 25 | //Remove " chars 26 | row=row.substr(1, row.length-2); 27 | 28 | //console.log(row); 29 | 30 | var isep=row.indexOf(' '), 31 | cmds=row.substr(0, isep), 32 | file=row.substr(isep+1); 33 | 34 | var type=cmds.indexOf('ISDIR')>-1?'d':'f'; 35 | 36 | _.each(cmds.split(','), function(cmd){ 37 | switch(cmd){ 38 | case 'CREATE': 39 | case 'MOVED_TO': 40 | me.emit('insert', [{op:'i', k:file, oldk:file, id:++nChanges, type:type}]); 41 | break; 42 | case 'CLOSE_WRITE': 43 | me.emit('update', [{op:'u', k:file, oldk:file, id:++nChanges, type:type}]); 44 | break; 45 | case 'DELETE': 46 | case 'MOVED_FROM': 47 | me.emit('delete', [{op:'d', k:file, oldk:file, id:++nChanges, type:type}]); 48 | break; 49 | } 50 | }); 51 | } 52 | }); 53 | }); 54 | 55 | me.stop=function(callback){ 56 | inotifywait.kill(); 57 | callback(); 58 | }; 59 | 60 | return me; 61 | }; 62 | 63 | module.exports={init:init}; -------------------------------------------------------------------------------- /lib/methods/postgresql-polling-method.js: -------------------------------------------------------------------------------- 1 | //Trigger method for postgresql driver 2 | var events=require('events'), _=require('underscore')._, Step=require('step'); 3 | 4 | var clog=function(text){ 5 | console.log(Date.now()+' '+text); 6 | }; 7 | 8 | /** Return a base name for functions and trigger based on table name and options */ 9 | var name=function name(opts, type){ 10 | var rv; 11 | 12 | if (opts.driverOpts.postgresql.baseObjectsName){ 13 | rv='dbmon_'+opts.driverOpts.postgresql.baseObjectsName+'_'+type; 14 | }else{ 15 | rv='dbmon_'+opts.table+'_'+type+'_trigger'; 16 | if (opts.addflds && opts.addflds.length){ 17 | rv+='_'+_.map(opts.addflds,function(f){return f.name;}).join('_'); 18 | } 19 | if (opts.cond){ 20 | //Dynamic names lets create more than one trigger and trigger fn 21 | rv+='_'+opts.cond.replace(/\W/g, '_').replace(/_+/g, '_'); 22 | } 23 | } 24 | 25 | //63=Max postgresql function length 26 | return rv.substr(0, 55); 27 | }; 28 | 29 | /** Compose and returns CREATE FUNCTION query */ 30 | var triggerFnStr=function triggerFnStr(type, opts){ 31 | if (opts.table){ 32 | var n=name(opts, type), 33 | historyTable=name(opts, 'history')+'_table', 34 | shortType=type.charAt(0), 35 | rec=type==='delete'?'OLD':'NEW', 36 | cond=opts.cond && type!=='truncate'?_.template(opts.cond, {rec:rec}):'', 37 | //Additional fields support 38 | addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name;}).join(','):'', 39 | addfldsNulls=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return 'NULL';}).join(','):'', 40 | addfldsValues=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return rec+'.'+f.name;}).join(','):''; 41 | var rv='CREATE OR REPLACE FUNCTION '+n+'_fn() RETURNS trigger AS $$\n'+ 42 | 'DECLARE\n'+ 43 | 'BEGIN\n'+ 44 | (cond?'IF '+cond+'\nTHEN\n':'')+ 45 | (opts.keyfld.name && opts.keyfld.type? 46 | 'INSERT INTO '+historyTable+' (op, k, oldk'+addflds+')'+ 47 | (type==='truncate'? 48 | 'VALUES (\''+shortType+'\', NULL, NULL'+addfldsNulls+');\n': 49 | //If UPDATE, save also the old key field value 50 | 'VALUES (\''+shortType+'\', '+rec+'.'+opts.keyfld.name+', '+(type==='update'?'OLD.'+opts.keyfld.name:rec+'.'+opts.keyfld.name)+addfldsValues+');\n' 51 | ) 52 | :'')+ 53 | // 'NOTIFY '+n+';\n'+ 54 | (cond?'END IF;\n':'')+ 55 | 'RETURN '+rec+';\n'+ 56 | 'END;\n'+ 57 | '$$ LANGUAGE plpgsql;'; 58 | 59 | return rv; 60 | }else{ 61 | console.log('postgresql-trigger-method.js, opts.table REQUIRED'); 62 | } 63 | }; 64 | /** Compose and returns CREATE TRIGGER query */ 65 | var triggerStr=function triggerStr(type, opts){ 66 | var n=name(opts, type); 67 | var rv='CREATE TRIGGER '+n+' AFTER '+type+' ON '+opts.table+' FOR EACH '+(type==='truncate'?'STATEMENT':'ROW')+' EXECUTE PROCEDURE '+n+'_fn();'; 68 | 69 | return rv; 70 | }; 71 | /** Compose and returns CREATE TABLE query for history table */ 72 | var historyTableStr=function historyTableStr(opts){ 73 | var n=name(opts, 'history')+'_table'; 74 | var addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name+' '+f.type;}).join(','):''; 75 | var rv= //'DROP TABLE IF EXISTS '+n+' CASCADE; '+ 76 | 'CREATE TABLE '+n+' (id serial primary key, op char(1), k '+opts.keyfld.type+', oldk '+opts.keyfld.type+addflds+');'; 77 | 78 | return rv; 79 | }; 80 | 81 | /** Main function, returns the main EventEmitter object used by the driver */ 82 | var init=function init(opts){ 83 | console.log('PostgreSQL Polling Method Init'); 84 | 85 | //The returned object is an eventemitter, so others can listen for events easily 86 | var me=new events.EventEmitter(), 87 | cli=opts.driverOpts[opts.driver].cli; 88 | 89 | if (!cli && opts.driverOpts[opts.driver].connStr){ 90 | var pg=require('pg'); 91 | cli=new pg.Client(opts.driverOpts[opts.driver].connStr); 92 | cli.connect(); 93 | } 94 | 95 | //Normalize type of events 96 | var types=opts.monitor.split(','); 97 | types=_.map(types, function(t){return t.trim().toLowerCase()}); 98 | 99 | //Required for PostgreSQL 8.x 100 | cli.query('create language plpgsql;', function(){ /*plpgsql is created only the first time, an error could occour*/ }); 101 | 102 | //Time to notify all listeners 103 | var historyId=-1, historyTable=name(opts, 'history')+'_table', 104 | addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name;}).join(','):'', 105 | historySql='select op,k,oldk,id'+addflds+' from '+historyTable+' where id>$1 order by id'; 106 | 107 | var historyIdCache={}; 108 | 109 | var startPolling=function(){ 110 | var tab=name(opts, 'history')+'_table', q; 111 | q='select * from '+tab+' order by id desc limit 1'; 112 | cli.query(q, function(err,res){ 113 | 114 | var lastId=res && res.rows && res.rows.length ? res.rows[0].id : -1; 115 | 116 | var poll=function(){ 117 | q='select * from '+tab+' where id>'+lastId+' order by id'; 118 | // clog('pppppppppp poll, lastId='+lastId+', q='+q); 119 | cli.query(q, function(err,res){ 120 | if (err) console.dir(err); 121 | 122 | if (res && res.rows && res.rows.length){ 123 | _.each(res.rows, function(r){ 124 | var op={i:'insert',u:'update',d:'delete'}[r.op]; 125 | // clog('emitting "'+op+'" for '+JSON.stringify(r)); 126 | me.emit(op, [r]); 127 | }); 128 | lastId=res.rows[res.rows.length-1].id; 129 | } 130 | setTimeout(poll, 1000); 131 | 132 | }); 133 | }; 134 | poll(); 135 | 136 | }); 137 | }; 138 | 139 | Step( 140 | function createHistoryTableIfNecessary(){ 141 | if (opts.keyfld.name && opts.keyfld.type){ 142 | cli.query(historyTableStr(opts), this); 143 | }else{ 144 | console.log('postgresql-polling-method.js, history table not created, opts.keyfld or opts.keytype not valid'); 145 | this(); 146 | } 147 | }, 148 | function emptyHistoryTable(err){ 149 | if (err){ console.log(err.message); } 150 | var n=name(opts, 'history')+'_table'; 151 | cli.query('truncate table '+n, this); 152 | }, 153 | function createTriggerStuff(err){ 154 | if (err){ console.log(err.message); } 155 | 156 | var afterAll=_.after(types.length, startPolling); 157 | 158 | _.each(types, function(type){ 159 | var chname=name(opts, type); 160 | cli.query(triggerFnStr(type, opts), function(err){ 161 | if (err){ console.log(err.message); } 162 | cli.query(triggerStr(type, opts), function(err){ 163 | if (err){ console.log(err.message); } 164 | 165 | afterAll(); 166 | 167 | }); 168 | }); 169 | }); 170 | } 171 | ); 172 | 173 | me.stop=function(callback){ 174 | var asyncCallback=_.after((types.length*2)+1, function(){ 175 | //check if db connection has been made by dbmon 176 | if (!opts.driverOpts[opts.driver].cli && cli){ 177 | cli.end(); 178 | } 179 | callback(); 180 | }); 181 | 182 | cli.query('DROP TABLE IF EXISTS '+name(opts, 'history')+'_table CASCADE', asyncCallback); 183 | _.each(types, function(t){ 184 | var iname=name(opts, t); 185 | cli.query('DROP TRIGGER IF EXISTS '+iname+' on '+opts.table+' CASCADE', asyncCallback); 186 | cli.query('DROP FUNCTION IF EXISTS '+iname+'_fn() CASCADE', asyncCallback); 187 | }); 188 | }; 189 | 190 | return me; 191 | }; 192 | 193 | module.exports={init:init}; 194 | -------------------------------------------------------------------------------- /lib/methods/postgresql-trigger-method.js: -------------------------------------------------------------------------------- 1 | //Trigger method for postgresql driver 2 | var events=require('events'), _=require('underscore')._, Step=require('step'); 3 | 4 | var clog=function(text){ 5 | console.log(Date.now()+' '+text); 6 | }; 7 | 8 | /** Return a base name for functions and trigger based on table name and options */ 9 | var name=function name(opts, type){ 10 | var rv; 11 | 12 | if (opts.driverOpts.postgresql.baseObjectsName){ 13 | rv='dbmon_'+opts.driverOpts.postgresql.baseObjectsName+'_'+type; 14 | }else{ 15 | rv='dbmon_'+opts.table+'_'+type+'_trigger'; 16 | if (opts.addflds && opts.addflds.length){ 17 | rv+='_'+_.map(opts.addflds,function(f){return f.name;}).join('_'); 18 | } 19 | if (opts.cond){ 20 | //Dynamic names lets create more than one trigger and trigger fn 21 | rv+='_'+opts.cond.replace(/\W/g, '_').replace(/_+/g, '_'); 22 | } 23 | } 24 | 25 | //63=Max postgresql function length 26 | return rv.substr(0, 55); 27 | }; 28 | 29 | /** Compose and returns CREATE FUNCTION query */ 30 | var triggerFnStr=function triggerFnStr(type, opts){ 31 | if (opts.table){ 32 | var n=name(opts, type), 33 | historyTable=name(opts, 'history')+'_table', 34 | shortType=type.charAt(0), 35 | rec=type==='delete'?'OLD':'NEW', 36 | cond=opts.cond && type!=='truncate'?_.template(opts.cond, {rec:rec}):'', 37 | //Additional fields support 38 | addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name;}).join(','):'', 39 | addfldsNulls=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return 'NULL';}).join(','):'', 40 | addfldsValues=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return rec+'.'+f.name;}).join(','):''; 41 | var rv='CREATE OR REPLACE FUNCTION '+n+'_fn() RETURNS trigger AS $$\n'+ 42 | 'DECLARE\n'+ 43 | 'BEGIN\n'+ 44 | (cond?'IF '+cond+'\nTHEN\n':'')+ 45 | (opts.keyfld.name && opts.keyfld.type? 46 | 'INSERT INTO '+historyTable+' (op, k, oldk'+addflds+')'+ 47 | (type==='truncate'? 48 | 'VALUES (\''+shortType+'\', NULL, NULL'+addfldsNulls+');\n': 49 | //If UPDATE, save also the old key field value 50 | 'VALUES (\''+shortType+'\', '+rec+'.'+opts.keyfld.name+', '+(type==='update'?'OLD.'+opts.keyfld.name:rec+'.'+opts.keyfld.name)+addfldsValues+');\n' 51 | ) 52 | :'')+ 53 | 'NOTIFY '+n+';\n'+ 54 | (cond?'END IF;\n':'')+ 55 | 'RETURN '+rec+';\n'+ 56 | 'END;\n'+ 57 | '$$ LANGUAGE plpgsql;'; 58 | 59 | return rv; 60 | }else{ 61 | console.log('postgresql-trigger-method.js, opts.table REQUIRED'); 62 | } 63 | }; 64 | /** Compose and returns CREATE TRIGGER query */ 65 | var triggerStr=function triggerStr(type, opts){ 66 | var n=name(opts, type); 67 | var rv='CREATE TRIGGER '+n+' AFTER '+type+' ON '+opts.table+' FOR EACH '+(type==='truncate'?'STATEMENT':'ROW')+' EXECUTE PROCEDURE '+n+'_fn();'; 68 | 69 | return rv; 70 | }; 71 | /** Compose and returns CREATE TABLE query for history table */ 72 | var historyTableStr=function historyTableStr(opts){ 73 | var n=name(opts, 'history')+'_table'; 74 | var addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name+' '+f.type;}).join(','):''; 75 | var rv= //'DROP TABLE IF EXISTS '+n+' CASCADE; '+ 76 | 'CREATE TABLE '+n+' (id serial primary key, op char(1), k '+opts.keyfld.type+', oldk '+opts.keyfld.type+addflds+');'; 77 | 78 | return rv; 79 | }; 80 | 81 | /** Main function, returns the main EventEmitter object used by the driver */ 82 | var init=function init(opts){ 83 | console.log('PostgreSQL Trigger Method Init'); 84 | 85 | //The returned object is an eventemitter, so others can listen for events easily 86 | var me=new events.EventEmitter(), 87 | cli=opts.driverOpts[opts.driver].cli; 88 | 89 | if (!cli && opts.driverOpts[opts.driver].connStr){ 90 | var pg=require('pg'); 91 | cli=new pg.Client(opts.driverOpts[opts.driver].connStr); 92 | cli.connect(); 93 | } 94 | 95 | //Normalize type of events 96 | var types=opts.monitor.split(','); 97 | types=_.map(types, function(t){return t.trim().toLowerCase()}); 98 | 99 | //Required for PostgreSQL 8.x 100 | cli.query('create language plpgsql;', function(){ /*plpgsql is created only the first time, an error could occour*/ }); 101 | 102 | //Time to notify all listeners 103 | var historyId=-1, historyTable=name(opts, 'history')+'_table', 104 | addflds=opts.addflds&&opts.addflds.length?','+_.map(opts.addflds,function(f){return f.name;}).join(','):'', 105 | historySql='select op,k,oldk,id'+addflds+' from '+historyTable+' where id>$1 order by id'; 106 | 107 | var historyIdCache={}; 108 | 109 | var onNotification=function(type){ 110 | 111 | // clog('dbmon, onNotification, historyTable='+historyTable); 112 | 113 | if (opts.keyfld.name && opts.keyfld.type){ 114 | var shortType=type.charAt(0); 115 | 116 | (function(historyIdMemo){ 117 | if (!historyIdCache[historyIdMemo]){ 118 | historyIdCache[historyIdMemo]=true; 119 | // clog('postgresql-trigger-method.js, onNotification, before query, historyId='+historyId+', historyIdMemo='+historyIdMemo); 120 | cli.query(historySql, [historyIdMemo], function(err, res){ 121 | delete historyIdCache[historyIdMemo]; 122 | if (historyId===historyIdMemo){ 123 | // clog('postgresql-trigger-method.js, onNotification, historyId===historyIdMemo==='+historyId+', rows='+res.rows.length); 124 | if (res.rows.length){ 125 | historyId=res.rows[res.rows.length-1].id; 126 | // clog('postgresql-trigger-method.js, onNotification, new historyId='+historyId); 127 | me.emit(type, res.rows); 128 | }else{ 129 | clog('postgresql-trigger-method.js, onNotification, res.ros.length=0'); 130 | } 131 | }else{ 132 | clog('postgresql-trigger-method.js, onNotification, historyId='+historyId+', historyIdMemo='+historyIdMemo); 133 | } 134 | }); 135 | } 136 | })(historyId); 137 | }else{ 138 | me.emit(type); 139 | } 140 | 141 | }; 142 | 143 | //If keyfld is not specified, I'll try to notify always clients for changes */ 144 | if (opts.keyfld.name && opts.keyfld.type && opts.debouncedNotifications){ 145 | onNotification=_.debounce(onNotification, opts.debouncedNotifications); 146 | } 147 | 148 | Step( 149 | function createHistoryTableIfNecessary(){ 150 | if (opts.keyfld.name && opts.keyfld.type){ 151 | cli.query(historyTableStr(opts), this); 152 | }else{ 153 | console.log('postgresql-trigger-method.js, history table not created, opts.keyfld or opts.keytype not valid'); 154 | this(); 155 | } 156 | }, 157 | function emptyHistoryTable(err){ 158 | if (err){ console.log(err.message); } 159 | var n=name(opts, 'history')+'_table'; 160 | cli.query('truncate table '+n, this); 161 | }, 162 | function createTriggerStuff(err){ 163 | if (err){ console.log(err.message); } 164 | _.each(types, function(type){ 165 | var chname=name(opts, type); 166 | cli.query(triggerFnStr(type, opts), function(err){ 167 | if (err){ console.log(err.message); } 168 | cli.query(triggerStr(type, opts), function(err){ 169 | if (err){ console.log(err.message); } 170 | 171 | //Listening for NOTIFYs 172 | cli.query('LISTEN '+chname, function(err){ 173 | if (err){ console.log(err.message); } 174 | }); 175 | 176 | cli.on('notification', function(data){ 177 | if (data.channel===chname){ 178 | onNotification(type); 179 | } 180 | }); 181 | 182 | }); 183 | }); 184 | }); 185 | } 186 | ); 187 | 188 | me.stop=function(callback){ 189 | 190 | console.log('postgresql-trigger-method stop called'); 191 | 192 | var asyncCallback=_.after((types.length*2)+1, function(){ 193 | //check if db connection has been made by dbmon 194 | if (!opts.driverOpts[opts.driver].cli && cli){ 195 | cli.end(); 196 | } 197 | callback(); 198 | }); 199 | 200 | cli.query('DROP TABLE IF EXISTS '+name(opts, 'history')+'_table CASCADE', asyncCallback); 201 | _.each(types, function(t){ 202 | var iname=name(opts, t); 203 | cli.query('DROP TRIGGER IF EXISTS '+iname+' on '+opts.table+' CASCADE', asyncCallback); 204 | cli.query('DROP FUNCTION IF EXISTS '+iname+'_fn() CASCADE', asyncCallback); 205 | }); 206 | }; 207 | 208 | return me; 209 | }; 210 | 211 | module.exports={init:init}; 212 | -------------------------------------------------------------------------------- /lib/transports/console-transport.js: -------------------------------------------------------------------------------- 1 | //Console Tranport 2 | var init=function init(opts){ 3 | console.log('Console Transport init'); 4 | 5 | var me={ 6 | notify:function(type, row){ 7 | console.log('Console Transport Notification: '+type+', row='+JSON.stringify(row)); 8 | return me; 9 | } 10 | }; 11 | return me; 12 | 13 | }; 14 | 15 | module.exports={init:init}; -------------------------------------------------------------------------------- /lib/transports/eventEmitter-transport.js: -------------------------------------------------------------------------------- 1 | //EventEmitter Tranport 2 | var init=function init(opts){ 3 | console.log('EventEmitter Transport init'); 4 | 5 | var me={ 6 | notify:function(type, row){ 7 | opts.transportsOpts.eventEmitter.eventEmitter.emit(type, row); 8 | return me; 9 | } 10 | }; 11 | return me; 12 | 13 | }; 14 | 15 | module.exports={init:init}; -------------------------------------------------------------------------------- /lib/transports/faye-transport.js: -------------------------------------------------------------------------------- 1 | //Faye Tranport 2 | var faye = require('faye'), bayeux; 3 | var init=function init(opts){ 4 | console.log('Faye Transport init, mount='+opts.transportsOpts.faye.mount+', channel='+opts.transportsOpts.faye.channel); 5 | 6 | bayeux = new faye.NodeAdapter({mount: opts.transportsOpts.faye.mount, timeout: opts.transportsOpts.faye.timeout}); 7 | if (opts.transportsOpts.faye.server){ 8 | bayeux.attach(opts.transportsOpts.faye.server); 9 | }else{ 10 | bayeux.listen(opts.transportsOpts.faye.port); 11 | } 12 | 13 | var rxType=/_TYPE_/g; //compiled only the first time 14 | var me={ 15 | notify:function(type, row){ 16 | var channel=opts.transportsOpts.faye.channel.replace(rxType, type); 17 | 18 | console.log('Faye Notifying on channel '+channel+', row='+JSON.stringify(row)); 19 | 20 | bayeux.getClient().publish(channel, row); 21 | return me; 22 | }, 23 | stop:function(){ 24 | bayeux.stop(); 25 | }, 26 | bayeux:bayeux 27 | }; 28 | return me; 29 | 30 | }; 31 | 32 | module.exports={init:init}; 33 | -------------------------------------------------------------------------------- /lib/transports/nowjs-transport.js: -------------------------------------------------------------------------------- 1 | //Nowjs Tranport 2 | var _=require('underscore')._; 3 | var init=function init(opts){ 4 | console.log('Nowjs Transport init, fn='+opts.transportsOpts.nowjs.fn); 5 | 6 | if (!opts.transportsOpts.nowjs.everyone){ 7 | console.log('Nowjs Transport, creating everyone object'); 8 | opts.transportsOpts.nowjs.everyone=require('now').initialize(opts.transportsOpts.nowjs.server); 9 | }else{ 10 | console.log('Nowjs Transport, everyone object exists'); 11 | } 12 | 13 | //Use underscore templating for function name templating 14 | var tcompiled=_.template(opts.transportsOpts.nowjs.fn); 15 | 16 | var me={ 17 | notify:function(type, row){ 18 | console.log('Nowjs transport calling '+opts.transportsOpts.nowjs.fn); 19 | var fnName=tcompiled(row); 20 | var fn=opts.transportsOpts.nowjs.everyone.now[fnName]; 21 | if (fn){ 22 | fn(row); 23 | }else{ 24 | console.log('Nowjs Transport, everyone.'+fnName+' dont exists yet'); 25 | } 26 | return me; 27 | }, 28 | stop:function(){ 29 | } 30 | }; 31 | return me; 32 | }; 33 | 34 | module.exports={init:init}; 35 | -------------------------------------------------------------------------------- /lib/transports/socketio-transport.js: -------------------------------------------------------------------------------- 1 | //Socket.io Tranport 2 | var _=require('underscore')._; 3 | var init=function init(opts){ 4 | //Simple shortcut 5 | var sio=opts.transportsOpts.socketio; 6 | 7 | console.log('Socket.io Transport init, event='+sio.event); 8 | 9 | //Socket.io io object creation 10 | if (!sio.io){ 11 | console.log('Socket.io Transport, creating io object'); 12 | sio.io=require('socket.io').listen(sio.server || sio.port); 13 | }else{ 14 | console.log('Socket.io Transport, io object exists'); 15 | } 16 | 17 | //Socket.io namespace support 18 | if (sio.io && sio.namespace){ 19 | var namespace=sio.namespace.indexOf('/')===0?sio.namespace:'/'+sio.namespace; 20 | sio.io.of(namespace); 21 | } 22 | 23 | //Use underscore templating for function name templating 24 | var tcompiled=_.template(sio.event); 25 | 26 | if (sio.io){ 27 | sio.io.on('connection', function(socket){ 28 | console.log('socket connected'); 29 | }); 30 | } 31 | 32 | var me={ 33 | notify:function(type, row){ 34 | console.log('Socket.io transport calling '+sio.event); 35 | var event=tcompiled(row); 36 | 37 | if (sio.io){ 38 | sio.io.sockets.emit(event, row); 39 | }else{ 40 | console.log('Socket.io Transport, io dont exists yet'); 41 | } 42 | 43 | return me; 44 | }, 45 | stop:function(){ 46 | } 47 | }; 48 | return me; 49 | }; 50 | 51 | module.exports={init:init}; 52 | -------------------------------------------------------------------------------- /lib/transports/tcpsocket-transport.js: -------------------------------------------------------------------------------- 1 | //TCP Socket Tranport 2 | var init=function init(opts){ 3 | console.log('TCP Socket Transport init'); 4 | var me={ 5 | notify:function(type, row){ 6 | opts.transportsOpts.tcpsocket.client.write(JSON.stringify(row)); 7 | return me; 8 | } 9 | }; 10 | return me; 11 | }; 12 | module.exports={init:init}; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { "name": "dbmon", 2 | "version": "1.1.0", 3 | "description": "Database and Filesystem Monitor Utilities for Real Time Apps", 4 | "keywords" : ["dbmon", "monitoring", "postgresql", "mysql", "oracle", "polling", "rdbms", "faye", "nowjs", "realtime"], 5 | "author": "Strx ", 6 | "main": "./index.js", 7 | "dependencies": { 8 | "pg": ">= 0.6.0", 9 | "faye": ">= 0.6.5", 10 | "now": ">= 0.6.5", 11 | "step": ">= 0.0.4", 12 | "underscore": ">= 1.2.0", 13 | "optimist": ">= 0.0.0" 14 | }, 15 | "devDependencies": { 16 | "colors": ">= 0.5.1" 17 | }, 18 | "homepage": "https://github.com/straps/node-dbmon", 19 | "repository" : { 20 | "type" : "git", 21 | "url" : "git://github.com/straps/node-dbmon.git" 22 | }, 23 | "scripts" : { 24 | "test" : "make test" 25 | }, 26 | "bin": { 27 | "dbmon": "./bin/dbmon" 28 | }, 29 | "engines": { 30 | "node": "*" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/test-filesystem-faye.js: -------------------------------------------------------------------------------- 1 | var assert=require('assert'), utils=require('./utils').utils, dbmon=require('../lib/dbmon'), 2 | events=require('events'), _=require('underscore')._, fs=require('fs'); 3 | 4 | var notifications=0, 5 | eventEmitter=new events.EventEmitter(); 6 | 7 | var dir='/tmp/dbmon', path=dir+'/dbmon-test-filesystem.tmp'; 8 | 9 | try { 10 | fs.unlinkSync(path); 11 | fs.rmdirSync(dir); 12 | }catch(e){} 13 | try { 14 | fs.mkdirSync(dir, '777'); 15 | }catch(e){} 16 | 17 | 18 | var channel=dbmon.channel({ 19 | driver:'filesystem', 20 | driverOpts:{ 21 | filesystem:{ 22 | root:dir 23 | } 24 | }, 25 | method:'inotifywait', 26 | transports:'faye', 27 | transportsOpts:{ 28 | faye:{ 29 | channel:'/dbmon' 30 | } 31 | } 32 | }); 33 | 34 | //Subscribing from server 35 | channel.transports[0].bayeux.getClient().subscribe('/dbmon', function(row){ 36 | notifications++; 37 | console.log('on channel /dbmon, row='+JSON.stringify(row)); 38 | }); 39 | 40 | 41 | setTimeout(function(){ 42 | console.log('Creating '+path); 43 | var f=fs.openSync(path, 'w+'); //fire insert 44 | 45 | fs.writeSync(f, 'testing update'); //fire update 46 | fs.closeSync(f); 47 | fs.unlinkSync(path); //fire delete 48 | 49 | setTimeout(function(){ 50 | utils.assertclog(notifications===3, 'Everything is OK', 'Theres Something Wrong, emitted notifications='+notifications); 51 | 52 | channel.stop(); 53 | process.exit(0); 54 | 55 | }, 100); 56 | 57 | }, 500); -------------------------------------------------------------------------------- /test/test-filesystem-inotifywait.js: -------------------------------------------------------------------------------- 1 | var assert=require('assert'), utils=require('./utils').utils, dbmon=require('../lib/dbmon'), 2 | events=require('events'), _=require('underscore')._, fs=require('fs'); 3 | 4 | var notifications=0, 5 | eventEmitter=new events.EventEmitter(); 6 | 7 | var dir='/tmp/dbmon', path=dir+'/dbmon-test-filesystem.tmp'; 8 | 9 | try { 10 | fs.unlinkSync(path); 11 | fs.rmdirSync(dir); 12 | }catch(e){} 13 | try { 14 | fs.mkdirSync(dir, '777'); 15 | }catch(e){} 16 | 17 | var channel=dbmon.channel({ 18 | driver:'filesystem', 19 | driverOpts:{ 20 | filesystem:{ 21 | root:dir 22 | } 23 | }, 24 | method:'inotifywait', 25 | transports:'eventEmitter', 26 | transportsOpts:{ 27 | eventEmitter:{ 28 | eventEmitter:eventEmitter 29 | } 30 | } 31 | }); 32 | 33 | //EventEmitter events for filesystem are very similar to database ones, exept for the truncate (obv..) 34 | _.each(['insert', 'update', 'delete'], function(op){ 35 | eventEmitter.on(op, function(row){ 36 | utils.clogok('EventEmitter on '+op+' called OK, row='+JSON.stringify(row)); 37 | notifications++; 38 | }); 39 | }); 40 | 41 | setTimeout(function(){ 42 | console.log('Creating '+path); 43 | var f=fs.openSync(path, 'w+'); //fire insert 44 | 45 | fs.writeSync(f, 'testing update'); //fire update 46 | fs.closeSync(f); 47 | fs.unlinkSync(path); //fire delete 48 | 49 | setTimeout(function(){ 50 | utils.assertclog(notifications===3, 'Everything is OK', 'Theres Something Wrong, emitted notifications='+notifications); 51 | 52 | channel.stop(); 53 | }, 100); 54 | 55 | }, 500); -------------------------------------------------------------------------------- /test/test-postgresql.js: -------------------------------------------------------------------------------- 1 | var assert=require('assert'), Step=require('step'), colors = require('colors'), events=require('events'), _=require('underscore')._, 2 | utils=require('./utils').utils, 3 | dbmon=require('../lib/dbmon'); 4 | 5 | utils.clogok('**********************').clogok('Starting Postgresql driver test, conString='+utils.pg.conString); 6 | 7 | var pgcli=utils.pg.getCli(); 8 | 9 | var notifications=0, dbmonChannel; 10 | Step( 11 | function createTempTable(){ 12 | utils.clogok('Creating Temp Table'); 13 | pgcli.query('drop table if exists dbmontmp; create table dbmontmp (i integer primary key, v varchar(10));', this); 14 | }, 15 | function fillTempTable(err){ 16 | utils.chkerr(err).clogok('Fill Temp Table'); 17 | pgcli.query('insert into dbmontmp values(0, \'zero\')', this); 18 | }, 19 | function theFunPart(err){ 20 | utils.chkerr(err).clogok('The Fun Part'); 21 | var i, toTearDown=this; 22 | 23 | var eventEmitter=new events.EventEmitter(); 24 | 25 | dbmonChannel=dbmon.channel({ 26 | driver:'postgresql', monitor: 'insert,update,delete,truncate', method: 'trigger', 27 | table:'dbmontmp', 28 | keyfld: { name:'i', type:'integer' }, 29 | driverOpts:{ 30 | postgresql:{ 31 | cli:pgcli 32 | } 33 | }, 34 | transports: 'eventEmitter', 35 | transportsOpts:{ 36 | eventEmitter:{ 37 | eventEmitter:eventEmitter 38 | } 39 | }, 40 | debouncedNotifications:0 41 | }); 42 | 43 | _.each(['insert', 'update', 'delete', 'truncate'], function(op){ 44 | eventEmitter.on(op, function(row){ 45 | utils.clogok('EventEmitter on '+op+' called OK, row='+JSON.stringify(row)); 46 | notifications++; 47 | }); 48 | }); 49 | 50 | //Triggering notifications 51 | setTimeout(function(){ 52 | pgcli.query('insert into dbmontmp values (1, \'one\')', function(){ 53 | //TEST ERROR 54 | pgcli.query('insert into dbmontmp values (1, \'one\')', function(err){ 55 | assert.ok(err!==null, 'Duplicate values should not be permitted'.red); 56 | }); 57 | }); 58 | pgcli.query('update dbmontmp set v=\'ZERO\' where i=0'); 59 | pgcli.query('delete from dbmontmp where i=0'); 60 | }, 500); 61 | 62 | 63 | for (i=100; i<200; i++){ 64 | setTimeout(function(x){ 65 | // console.log('insert '+x); 66 | pgcli.query('insert into dbmontmp values ('+x+', \''+x+'\')', function(){}); 67 | }, 500+(i*2/10), i); 68 | } 69 | 70 | //Huge query test 71 | setTimeout(function(){ 72 | var q=[]; 73 | for (i=200; i<300; i++){ 74 | q.push('insert into dbmontmp values ('+i+', \''+i+'\')'); 75 | } 76 | pgcli.query(q.join(';'), function(){}); 77 | }, 800); 78 | 79 | for (i=0; i<50; i++){ 80 | setTimeout(function(x){ 81 | pgcli.query('insert into dbmontmp values ('+(x+1000)+', \''+(x+1000)+'\')', function(){}); 82 | }, 500+i*100, i); 83 | } 84 | 85 | for (i=0; i<50; i++){ 86 | setTimeout(function(x){ 87 | pgcli.query('insert into dbmontmp values ('+(x+2000)+', \''+(x+2000)+'\')', function(){}); 88 | }, 550+i*100, i); 89 | } 90 | 91 | for (i=0; i<50; i++){ 92 | setTimeout(function(x){ 93 | pgcli.query('insert into dbmontmp values ('+(x+3000)+', \''+(x+3000)+'\')', function(){}); 94 | }, 300+i*110, i); 95 | } 96 | 97 | for (i=0; i<50; i++){ 98 | setTimeout(function(x){ 99 | pgcli.query('insert into dbmontmp values ('+(x+4000)+', \''+(x+4000)+'\')', function(){}); 100 | }, 300+i*120, i); 101 | } 102 | 103 | //Stop 104 | setTimeout(toTearDown, 7000); 105 | }, 106 | function tearDown(){ 107 | utils.clogok('Everything is ok'); 108 | dbmonChannel.stop(function(){ 109 | assert.ok(notifications===403, ('notifications='+notifications+', should be 403').red); 110 | pgcli.query('drop table dbmontmp cascade', function(){ 111 | utils.pg.end(); 112 | }); 113 | }); 114 | } 115 | ); 116 | 117 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | // Test Utils 2 | var colors=require('colors'), assert=require('assert'), pg=require('pg'); 3 | 4 | var u={ 5 | /** PostgreSQL facilities */ 6 | pg:{ 7 | cli:null, 8 | requests:0, 9 | conString:'tcp://postgres@localhost:5432/template1', 10 | getCli:function(){ 11 | u.pg.requests++; 12 | if (!u.pg.cli){ 13 | console.log(u.pg.conString); 14 | var pg=require('pg'); 15 | u.pg.cli=new pg.Client(u.pg.conString); 16 | u.pg.cli.connect(); 17 | } 18 | return u.pg.cli; 19 | }, 20 | end:function(){ 21 | if (!--u.pg.requests){ 22 | u.pg.cli.end(); 23 | } 24 | } 25 | }, 26 | 27 | /** Color and log facilities */ 28 | arrcolor:function(arr, color){ 29 | for (var i=0; i