Merge pull request #1667 from xtuple/4_5_x
[xtuple] / node-datasource / main.js
1 #!/usr/bin/env node
2
3 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
4 regexp:true, undef:true, strict:true, trailing:true, white:true */
5 /*global X:true, Backbone:true, _:true, XM:true, XT:true, SYS:true, jsonpatch:true*/
6 process.chdir(__dirname);
7
8 Backbone = require("backbone");
9 _ = require("underscore");
10 jsonpatch = require("json-patch");
11 SYS = {};
12 XT = { };
13 var express = require('express');
14 var app;
15
16 (function () {
17   "use strict";
18
19   var options = require("./lib/options"),
20     authorizeNet,
21     schemaSessionOptions = {},
22     privSessionOptions = {};
23
24   /**
25    * Include the X framework.
26    */
27   require("./xt");
28
29   // Loop through files and load the dependencies.
30   // Apes the enyo package process
31   // TODO: it would be nice to use a more standardized way
32   // of loading our libraries (tools and backbone-x) here
33   // in node.
34   X.relativeDependsPath = "";
35   X.depends = function () {
36     var dir = X.relativeDependsPath,
37       files = X.$A(arguments),
38       pathBeforeRecursion;
39
40     _.each(files, function (file) {
41       if (X.fs.statSync(X.path.join(dir, file)).isDirectory()) {
42         pathBeforeRecursion = X.relativeDependsPath;
43         X.relativeDependsPath = X.path.join(dir, file);
44         X.depends("package.js");
45         X.relativeDependsPath = pathBeforeRecursion;
46       } else {
47         require(X.path.join(dir, file));
48       }
49     });
50   };
51
52
53   // Load other xTuple libraries using X.depends above.
54   require("backbone-relational");
55   X.relativeDependsPath = X.path.join(X.basePath, "../lib/tools/source");
56   require("../lib/tools");
57   X.relativeDependsPath = X.path.join(X.basePath, "../lib/backbone-x/source");
58   require("../lib/backbone-x");
59   Backbone.XM = XM;
60
61   // Argh!!! Hack because `XT` has it's own string format function that
62   // is incompatible with `X`....
63   String.prototype.f = function () {
64     return X.String.format.apply(this, arguments);
65   };
66
67   // Another hack: quiet the logs here.
68   XT.log = function () {};
69
70   // Set the options.
71   X.setup(options);
72
73   // load some more required files
74   var datasource = require("./lib/ext/datasource");
75   require("./lib/ext/models");
76   require("./lib/ext/smtp_transport");
77
78   datasource.setupPgListeners(X.options.datasource.databases, {
79     email: X.smtpTransport.sendMail
80   });
81
82   // load the encryption key, or create it if it doesn't exist
83   // it should created just once, the very first time the datasoruce starts
84   var encryptionKeyFilename = X.options.datasource.encryptionKeyFile || './lib/private/encryption_key.txt';
85   X.fs.exists(encryptionKeyFilename, function (exists) {
86     if (exists) {
87       X.options.encryptionKey = X.fs.readFileSync(encryptionKeyFilename, "utf8");
88     } else {
89       X.options.encryptionKey = Math.random().toString(36).slice(2);
90       X.fs.writeFile(encryptionKeyFilename, X.options.encryptionKey);
91     }
92   });
93
94
95   XT.session = Object.create(XT.Session);
96   XT.session.schemas.SYS = false;
97
98   var getExtensionDir = function (extension) {
99     var dirMap = {
100       "/private-extensions": X.path.join(__dirname, "../..", extension.location, "source", extension.name),
101       "/xtuple-extensions": X.path.join(__dirname, "../..", extension.location, "source", extension.name),
102       "/core-extensions": X.path.join(__dirname, "../enyo-client/extensions/source", extension.name),
103       "npm": X.path.join(__dirname, "../node_modules", extension.name)
104     };
105     return dirMap[extension.location];
106   };
107   var useClientDir = function (path, dir) {
108     path = path.indexOf("npm") === 0 ? "/" + path : path;
109     _.each(X.options.datasource.databases, function (orgValue, orgKey, orgList) {
110       app.use("/" + orgValue + path, express.static(dir, { maxAge: 86400000 }));
111     });
112   };
113   var loadExtensionClientside = function (extension) {
114     var extensionLocation = extension.location === "npm" ? extension.location : extension.location + "/source";
115     useClientDir(extensionLocation + "/" + extension.name + "/client", X.path.join(getExtensionDir(extension), "client"));
116   };
117   var loadExtensionRoutes = function (extension) {
118     var manifest = JSON.parse(X.fs.readFileSync(X.path.join(getExtensionDir(extension),
119         "database/source/manifest.js")));
120     _.each(manifest.routes || [], function (routeDetails) {
121       var verb = (routeDetails.verb || "all").toLowerCase(),
122         func = require(X.path.join(getExtensionDir(extension),
123           "node-datasource", routeDetails.filename))[routeDetails.functionName];
124
125       if (_.contains(["all", "get", "post", "patch", "delete"], verb)) {
126         app[verb]('/:org/' + routeDetails.path, func);
127       } else {
128         console.log("Invalid verb for extension-defined route " + routeDetails.path);
129       }
130     });
131   };
132
133   schemaSessionOptions.username = X.options.databaseServer.user;
134   schemaSessionOptions.database = X.options.datasource.databases[0];
135   // XXX note that I'm not addressing an underlying bug that we don't wait to
136   // listen on the port until all the setup is done
137   schemaSessionOptions.success = function () {
138     if (!SYS) {
139       return;
140     }
141     var extensions = new SYS.ExtensionCollection();
142     extensions.fetch({
143       database: X.options.datasource.databases[0],
144       success: function (coll, results, options) {
145         if (!app) {
146           // XXX time bomb: assuming app has been initialized, below, by now
147           XT.log("Could not load extension routes or client-side code because the app has not started");
148           process.exit(1);
149           return;
150         }
151         useClientDir("/client", "../enyo-client/application");
152         _.each(results, loadExtensionRoutes);
153         _.each(results, loadExtensionClientside);
154       }
155     });
156   };
157   XT.session.loadSessionObjects(XT.session.SCHEMA, schemaSessionOptions);
158
159   privSessionOptions.username = X.options.databaseServer.user;
160   privSessionOptions.database = X.options.datasource.databases[0];
161   XT.session.loadSessionObjects(XT.session.PRIVILEGES, privSessionOptions);
162
163 }());
164
165
166 /**
167   Grab the version number from the package.json file.
168  */
169
170 var packageJson = X.fs.readFileSync("../package.json");
171 try {
172   X.version = JSON.parse(packageJson).version;
173 } catch (error) {
174
175 }
176
177 /**
178  * Module dependencies.
179  */
180 var passport = require('passport'),
181   oauth2 = require('./oauth2/oauth2'),
182   routes = require('./routes/routes'),
183   socketio = require('socket.io'),
184   url = require('url'),
185   utils = require('./oauth2/utils'),
186   user = require('./oauth2/user'),
187   destroySession;
188
189 // TODO - for testing. remove...
190 //http://stackoverflow.com/questions/13091037/node-js-heap-snapshots-and-google-chrome-snapshot-viewer
191 //var heapdump = require("heapdump");
192 // Use it!: https://github.com/c4milo/node-webkit-agent
193 //var agent = require('webkit-devtools-agent');
194
195 /**
196  * ###################################################
197  * Overrides section.
198  *
199  * Sometimes we need to change how an npm packages works.
200  * Don't edit the packages directly, override them here.
201  * ###################################################
202  */
203
204 /**
205   Define our own authentication criteria for passport. Passport itself defines
206   its authentication function here:
207   https://github.com/jaredhanson/passport/blob/master/lib/passport/http/request.js#L74
208   We are stomping on that method with our own special business logic.
209   The ensureLoggedIn function will not need to be changed, because that calls this.
210  */
211 require('http').IncomingMessage.prototype.isAuthenticated = function () {
212   "use strict";
213
214   var creds = this.session.passport.user;
215
216   if (creds && creds.id && creds.username && creds.organization) {
217     return true;
218   } else {
219     destroySession(this.sessionID, this.session);
220     return false;
221   }
222 };
223
224 // Stomping on express/connect's Cookie.prototype to only update the expires property
225 // once a minute. Otherwise it's hit on every session check. This cuts down on chatter.
226 // See more details here: https://github.com/senchalabs/connect/issues/670
227 require('express/node_modules/connect/lib/middleware/session/cookie').prototype.__defineSetter__("expires", require('./stomps/expires').expires);
228
229 // Stomp on Express's cookie serialize() to not send an "expires" value to the browser.
230 // This makes the browser cooke a "session" cookie that will never expire and only
231 // gets removed when the user closes the browser. We still set express.session.cookie.maxAge
232 // below so our persisted session gets an expires value, but not the browser cookie.
233 // See this issue for more details: https://github.com/senchalabs/connect/issues/328
234 require('express/node_modules/cookie').serialize = require('./stomps/cookie').serialize;
235
236 // Stomp on Connect's session.
237 // https://github.com/senchalabs/connect/issues/641
238 function stompSessionLoad() {
239   "use strict";
240   return require('./stomps/session');
241 }
242 require('express/node_modules/connect').middleware.__defineGetter__('session', stompSessionLoad);
243 require('express/node_modules/connect').__defineGetter__('session', stompSessionLoad);
244 require('express').__defineGetter__('session', stompSessionLoad);
245
246 /**
247  * ###################################################
248  * END Overrides section.
249  * ###################################################
250  */
251
252 //
253 // Load the ssl data
254 //
255 var sslOptions = {};
256
257 sslOptions.key = X.fs.readFileSync(X.options.datasource.keyFile);
258 if (X.options.datasource.caFile) {
259   sslOptions.ca = _.map(X.options.datasource.caFile, function (obj) {
260     "use strict";
261
262     return X.fs.readFileSync(obj);
263   });
264 }
265 sslOptions.cert = X.fs.readFileSync(X.options.datasource.certFile);
266
267 /**
268  * Express configuration.
269  */
270 app = express();
271
272 var server = X.https.createServer(sslOptions, app),
273   parseSignedCookie = require('express/node_modules/connect').utils.parseSignedCookie,
274   //MemoryStore = express.session.MemoryStore,
275   XTPGStore = require('./oauth2/db/connect-xt-pg')(express),
276   io,
277   //sessionStore = new MemoryStore(),
278   sessionStore = new XTPGStore({ hybridCache: X.options.datasource.requireCache || false }),
279   Session = require('express/node_modules/connect/lib/middleware/session').Session,
280   Cookie = require('express/node_modules/connect/lib/middleware/session/cookie'),
281   cookie = require('express/node_modules/cookie'),
282   privateSalt = X.fs.readFileSync(X.options.datasource.saltFile).toString() || 'somesecret';
283
284 // Conditionally load express.session(). REST API endpoints using OAuth tokens do not get sessions.
285 var conditionalExpressSession = function (req, res, next) {
286   "use strict";
287
288   var key;
289
290   // REST API endpoints start with "/api" in their path.
291   // The 'assets' folder and login page are sessionless.
292   if ((/^api/i).test(req.path.split("/")[2]) ||
293       (/^\/assets/i).test(req.path) ||
294       req.path === "/" ||
295       req.path === "/favicon.ico" ||
296       req.path === "/forgot-password" ||
297       req.path === "/recover") {
298
299     next();
300   } else {
301     if (req.path === "/login") {
302       // TODO - Add check against X.options database array
303       key = req.body.database + ".sid";
304     } else if (req.path.split("/")[1]) {
305       key = req.path.split("/")[1] + ".sid";
306     } else {
307       // TODO - Dynamically name the cookie after the database.
308       console.log("### FIX ME ### setting cookie name to 'connect.sid' for path = ", JSON.stringify(req.path));
309       console.log("### FIX ME ### cookie name should match database name!!!");
310       console.trace("### At this location ###");
311       key = 'connect.sid';
312     }
313
314     // Instead of doing app.use(express.session()) we call the package directly
315     // which returns a function (req, res, next) we can call to do the same thing.
316     var init_session = express.session({
317         key: key,
318         store: sessionStore,
319         secret: privateSalt,
320         // See cookie stomp above for more details on how this session cookie works.
321         cookie: {
322           path: '/',
323           httpOnly: true,
324           secure: true,
325           maxAge: (X.options.datasource.sessionTimeout * 60 * 1000) || 3600000
326         },
327         sessionIDgen: function () {
328           // TODO: Stomp on connect's sessionID generate.
329           // https://github.com/senchalabs/connect/issues/641
330           return key.split(".")[0] + "." + utils.generateUUID();
331         }
332       });
333
334     init_session(req, res, next);
335   }
336 };
337
338 // Conditionally load passport.session(). REST API endpoints using OAuth tokens do not get sessions.
339 var conditionalPassportSession = function (req, res, next) {
340   "use strict";
341
342   // REST API endpoints start with "/api" in their path.
343   // The 'assets' folder and login page are sessionless.
344   if ((/^api/i).test(req.path.split("/")[2]) ||
345     (/^\/assets/i).test(req.path) ||
346     req.path === "/" ||
347     req.path === "/favicon.ico"
348     ) {
349
350     next();
351   } else {
352     // Instead of doing app.use(passport.session())
353     var init_passportSessions = passport.session();
354
355     init_passportSessions(req, res, next);
356   }
357 };
358
359 app.configure(function () {
360   "use strict";
361
362   // gzip all static files served.
363   app.use(express.compress());
364   // Add a basic view engine that will render files from "views" directory.
365   app.set('view engine', 'ejs');
366
367   // TODO - This outputs access logs like apache2 and some other user things.
368   //app.use(express.logger());
369
370   app.use(express.cookieParser());
371   app.use(express.bodyParser());
372
373   // Conditionally load session packages. Based off these examples:
374   // http://stackoverflow.com/questions/9348505/avoiding-image-logging-in-express-js/9351428#9351428
375   // http://stackoverflow.com/questions/13516898/disable-csrf-validation-for-some-requests-on-express
376   app.use(conditionalExpressSession);
377   app.use(passport.initialize());
378   app.use(conditionalPassportSession);
379
380   app.use(app.router);
381   app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
382 });
383
384 /**
385  * Passport configuration.
386  */
387 require('./oauth2/passport');
388
389 /**
390  * Setup HTTP routes and handlers.
391  */
392 var that = this;
393
394 app.use(express.favicon(__dirname + '/views/login/assets/favicon.ico'));
395 app.use('/assets', express.static('views/login/assets', { maxAge: 86400000 }));
396
397 app.get('/:org/dialog/authorize', oauth2.authorization);
398 app.post('/:org/dialog/authorize/decision', oauth2.decision);
399 app.post('/:org/oauth/token', oauth2.token);
400
401 app.get('/:org/discovery/v1alpha1/apis/v1alpha1/rest', routes.restDiscoveryGetRest);
402 app.get('/:org/discovery/v1alpha1/apis/:model/v1alpha1/rest', routes.restDiscoveryGetRest);
403 app.get('/:org/discovery/v1alpha1/apis', routes.restDiscoveryList);
404
405 app.get('/:org/api/userinfo', user.info);
406
407 app.post('/:org/api/v1alpha1/services/:service/:id', routes.restRouter);
408 app.all('/:org/api/v1alpha1/resources/:model/:id', routes.restRouter);
409 app.all('/:org/api/v1alpha1/resources/:model', routes.restRouter);
410 app.all('/:org/api/v1alpha1/resources/*', routes.restRouter);
411
412 app.get('/', routes.loginForm);
413 app.post('/login', routes.login);
414 app.get('/forgot-password', routes.forgotPassword);
415 app.post('/recover', routes.recoverPassword);
416 app.get('/:org/recover/reset/:id/:token', routes.verifyRecoverPassword);
417 app.post('/:org/recover/resetUpdate', routes.resetRecoveredPassword);
418 app.get('/login/scope', routes.scopeForm);
419 app.post('/login/scopeSubmit', routes.scope);
420 app.get('/logout', routes.logout);
421 app.get('/:org/logout', routes.logout);
422 app.get('/:org/app', routes.app);
423 app.get('/:org/debug', routes.debug);
424
425 app.all('/:org/credit-card', routes.creditCard);
426 app.all('/:org/change-password', routes.changePassword);
427 app.all('/:org/client/build/client-code', routes.clientCode);
428 app.all('/:org/email', routes.email);
429 app.all('/:org/export', routes.exxport);
430 app.get('/:org/file', routes.file);
431 app.get('/:org/generate-report', routes.generateReport);
432 app.all('/:org/install-extension', routes.installExtension);
433 app.get('/:org/locale', routes.locale);
434 app.all('/:org/oauth/generate-key', routes.generateOauthKey);
435 app.get('/:org/reset-password', routes.resetPassword);
436 app.post('/:org/oauth/revoke-token', routes.revokeOauthToken);
437 app.all('/:org/vcfExport', routes.vcfExport);
438
439
440 // Set up the other servers we run on different ports.
441
442 var redirectServer = express();
443 redirectServer.get(/.*/, routes.redirect); // RegEx for "everything"
444 redirectServer.listen(X.options.datasource.redirectPort, X.options.datasource.bindAddress);
445
446 /**
447  * Start the express server. This is the NEW way.
448  */
449 // TODO - Active browser sessions can make calls to this server when it hasn't fully started.
450 // That can cause it to crash at startup.
451 // Need a way to get everything loaded BEFORE we start listening.  Might just move this to the end...
452 io = socketio.listen(server.listen(X.options.datasource.port, X.options.datasource.bindAddress));
453
454 X.log("Server listening at: ", X.options.datasource.bindAddress);
455 X.log("node-datasource started on port: ", X.options.datasource.port);
456 X.log("redirectServer started on port: ", X.options.datasource.redirectPort);
457 X.log("Databases accessible from this server: \n", JSON.stringify(X.options.datasource.databases, null, 2));
458
459
460 /**
461  * Destroy a single session.
462  * @param {Object} val - Session object.
463  * @param {String} key - Session id.
464  */
465 destroySession = function (key, val) {
466   "use strict";
467
468   var sessionID;
469
470   // Timeout socket.
471   if (val && val.socket && val.socket.id) {
472     _.each(io.sockets.sockets, function (sockVal, sockKey, sockList) {
473       if (val.socket.id === sockKey) {
474         _.each(sockVal.manager.namespaces, function (spaceVal, spaceKey, spaceList) {
475           sockVal.flags.endpoint = spaceVal.name;
476           // Tell the client it timed out. This will redirect the client to /logout
477           // which will destroy the session, but we can't rely on the client for that.
478           sockVal.emit("timeout");
479         });
480
481         // Disconnect socket.
482         sockVal.disconnect();
483       }
484     });
485   }
486
487   sessionID = key.replace(sessionStore.prefix, '');
488
489   // Destroy session here incase the client never hits /logout.
490   sessionStore.destroy(sessionID, function (err) {
491     //X.debug("Session destroied: ", key, " error: ", err);
492   });
493 };
494
495 // TODO - Use NODE_ENV flag to switch between development and production.
496 // See "Understanding the configure method" at:
497 // https://github.com/LearnBoost/Socket.IO/wiki/Configuring-Socket.IO
498 io.configure(function () {
499   "use strict";
500
501   io.set('log', false);
502   // TODO - We need to implement a store for this if we run multiple processes:
503   // https://github.com/LearnBoost/socket.io/tree/0.9/lib/stores
504   //http://stackoverflow.com/questions/9267292/examples-in-using-redisstore-in-socket-io/9275798#9275798
505   //io.set('store', someNewStore);                // Use our someNewStore.
506   io.set('browser client minification', true);  // Send minified file to the client.
507   io.set('browser client etag', true);          // Apply etag caching logic based on version number
508   // TODO - grubmle - See prototype stomp above:
509   // https://github.com/LearnBoost/socket.io/issues/932
510   // https://github.com/LearnBoost/socket.io/issues/984
511   io.set('browser client gzip', true);          // gzip the file.
512   //io.set('log level', 1);                       // Reduce logging.
513   io.set('transports', [                        // Enable all transports.
514       'websocket',
515       'htmlfile',
516       'xhr-polling',
517       'jsonp-polling'
518     ]
519   );
520 });
521
522 /**
523  * Setup socket.io routes and handlers.
524  *
525  * Socket.io authorization modeled off of:
526  * https://github.com/LearnBoost/socket.io/wiki/Authorizing
527  * http://stackoverflow.com/questions/13095418/how-to-use-passport-with-express-and-socket-io
528  * https://github.com/jfromaniello/passport.socketio
529  */
530 io.of('/clientsock').authorization(function (handshakeData, callback) {
531   "use strict";
532
533   var key;
534
535   if (handshakeData.headers.cookie) {
536     handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
537
538     if (handshakeData.headers.referer && url.parse(handshakeData.headers.referer).path.split("/")[1]) {
539       key = url.parse(handshakeData.headers.referer).path.split("/")[1];
540     } else if (X.options.datasource.testDatabase) {
541       // for some reason zombie doesn't send the referrer in the socketio call
542       // https://groups.google.com/forum/#!msg/socket_io/MPpXrP5N9k8/xAyk1l8Iw8YJ
543       key = X.options.datasource.testDatabase;
544     } else {
545       return callback(null, false);
546     }
547
548
549     if (!handshakeData.cookie[key + '.sid']) {
550       return callback(null, false);
551     }
552
553     // Add sessionID so we can use it to check for valid sessions on each request below.
554     handshakeData.sessionID = parseSignedCookie(handshakeData.cookie[key + '.sid'], privateSalt);
555
556     sessionStore.get(handshakeData.sessionID, function (err, session) {
557       if (err) {
558         return callback(err);
559       }
560
561       // All requests get a session. Make sure the session is authenticated.
562       if (!session || !session.passport || !session.passport.user ||
563         !session.passport.user.id ||
564         !session.passport.user.organization ||
565         !session.passport.user.username ||
566         !session.cookie || !session.cookie.expires) {
567
568         destroySession(handshakeData.sessionID, session);
569
570         // Not an error exactly, but the cookie is invalid. The user probably logged off.
571         return callback(null, false);
572       }
573
574       // Prep the cookie and create a session object so we can touch() it on each request below.
575       session.cookie.expires = new Date(session.cookie.expires);
576       session.cookie = new Cookie(session.cookie);
577       handshakeData.session = new Session(handshakeData, session);
578
579       // Add sessionStore here so it can be used to lookup valid session on each request below.
580       handshakeData.sessionStore = sessionStore;
581
582       // Move along.
583       callback(null, true);
584     });
585   } else {
586     callback(null, false);
587   }
588 }).on('connection', function (socket) {
589   "use strict";
590
591   var ensureLoggedIn = function (callback, payload) {
592         socket.handshake.sessionStore.get(socket.handshake.sessionID, function (err, session) {
593           var expires,
594               current;
595
596           // All requests get a session. Make sure the session is authenticated.
597           if (err || !session || !session.passport || !session.passport.user ||
598               !session.passport.user.id ||
599               !session.passport.user.organization ||
600               !session.passport.user.username ||
601               !session.cookie || !session.cookie.expires) {
602
603             return destroySession(socket.handshake.sessionID, session);
604           }
605
606           // Make sure the sesion hasn't expired yet.
607           expires = new Date(session.cookie.expires);
608           current = new Date();
609           if (expires <= current) {
610             return destroySession(socket.handshake.sessionID, session);
611           } else {
612             // User is still valid
613
614             // Update session expiration timeout, unless this is an automated call of
615             // some sort (e.g. lock refresh)
616             if (!payload || !payload.automatedRefresh) {
617               socket.handshake.session.touch().save();
618             }
619
620             // Move along.
621             callback(session);
622           }
623         });
624       };
625
626   // Save socket.id to the session store so we can disconnect that socket server side
627   // when a session is timed out. That should notify the client imediately they have timed out.
628   socket.handshake.session.socket = {id: socket.id};
629   socket.handshake.session.save();
630
631   // To run this from the client:
632   // ???
633   socket.on('session', function (data, callback) {
634     ensureLoggedIn(function (session) {
635       var callbackObj = X.options.client || {};
636       callbackObj = _.extend(callbackObj,
637         {
638           data: session.passport.user,
639           code: 1,
640           debugging: X.options.datasource.debugging,
641           biAvailable: _.isObject(X.options.biServer) && !_.isEmpty(X.options.biServer),
642           emailAvailable: _.isString(X.options.datasource.smtpHost) && X.options.datasource.smtpHost !== "",
643           printAvailable: _.isString(X.options.datasource.printer) && X.options.datasource.printer !== "",
644           version: X.version
645         });
646       callback(callbackObj);
647     }, data && data.payload);
648   });
649
650   // To run this from the client:
651   socket.on('delete', function (data, callback) {
652     ensureLoggedIn(function (session) {
653       routes.queryDatabase("delete", data.payload, session, callback);
654     }, data && data.payload);
655   });
656
657   // To run this from the client:
658   // XT.dataSource.request(m = new XM.Contact(), "get", {nameSpace: "XM", type: "Contact", id: "1"}, {propagate: true, parse: true, success: function () {console.log("success", arguments)}, error: function () {console.log("error", arguments);}});
659   socket.on('get', function (data, callback) {
660     ensureLoggedIn(function (session) {
661       routes.queryDatabase("get", data.payload, session, callback);
662     }, data && data.payload);
663   });
664
665   // To run this from the client:
666   socket.on('patch', function (data, callback) {
667     ensureLoggedIn(function (session) {
668       routes.queryDatabase("patch", data.payload, session, callback);
669     }, data && data.payload);
670   });
671
672   // To run this from the client:
673   socket.on('post', function (data, callback) {
674     ensureLoggedIn(function (session) {
675       routes.queryDatabase("post", data.payload, session, callback);
676     }, data && data.payload);
677   });
678
679   // Tell the client it's connected.
680   socket.emit("ok");
681 });
682
683 /**
684  * Job loading section.
685  *
686  * The following are jobs that must be started at start up or scheduled to run periodically.
687  */
688
689 // TODO - Check pid file to see if this is already running.
690 // Kill process or create new pid file.
691
692 // Run the expireSessions cleanup/garbage collection once a minute.
693 setInterval(function () {
694     "use strict";
695
696     //X.debug("session cleanup called at: ", new Date());
697     sessionStore.expireSessions(destroySession);
698   }, 60000);