From f92dc28c64b656bf5c05e1f7ffcff8f4837695df Mon Sep 17 00:00:00 2001 From: Jonathan Gros-Dubois Date: Thu, 22 Oct 2015 02:30:32 +1100 Subject: [PATCH] Got everything setup --- README.md | 91 ++------------- package.json | 2 +- public/pages/page-category-list.html | 2 +- sc_modules/access-control.js | 8 +- sc_modules/authentication.js | 20 ++-- sc_modules/dummy-data.js | 159 +++++++++------------------ worker.js | 1 + 7 files changed, 83 insertions(+), 200 deletions(-) diff --git a/README.md b/README.md index 07ac0fc..38b111c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ # sc-sample-inventory -A sample inventory tracking realtime single page app built with SocketCluster (http://socketcluster.io/) and Google's Polymer (v1.0) framework. +A sample inventory tracking realtime single page app built with SocketCluster (http://socketcluster.io/), Google's Polymer (v1.0) Framework and RethinkDB. +It demonstrates a way of building realtime apps which I decided to call "Granular REAl-Time CRUD" (GREAT CRUD)... Unless you can think of a better name. All code for the server-side worker logic is linked from worker.js - It's mostly generic so feel free to reuse/modify for your own app -or you can use this app as a base to build yours if starting from scratch. I will try to make these modules available publicly as soon as -possible. +or you can use this app as a base to build yours if starting from scratch. -This sample app aims to demonstrate all the cutting edge features that one might want when +Aside from SocketCluster, Polymer and RethinkDB, this sample app uses the following modules: +- sc-collection (https://github.com/SocketCluster/sc-collection - ```bower install sc-collection --save```) +- sc-field (https://github.com/SocketCluster/sc-field - ```bower install sc-field --save```) +- sc-crud-rethink (https://github.com/SocketCluster/sc-crud-rethink - ```npm install sc-crud-rethink --save```) + +This sample app aims to demonstrate all the cutting edge features that one might want when building a realtime single page app including: - Authentication (via JWT tokens) @@ -27,86 +32,12 @@ make updates to the data in realtime. To run this sample: - Make sure you have Node.js installed (http://nodejs.org/) +- Make sure you have RethinkDB installed (https://www.rethinkdb.com/) - ```git clone https://github.com/SocketCluster/sc-sample-inventory.git``` - Navigate to the sc-sample-inventory/ directory - Run ```npm install``` (no arguments) - Make sure you have bower installed, if not: ```npm install -g bower``` - Run ```bower install``` (no arguments) +- Run ```sudo rethinkdb``` - Run ```node server``` - In your browser, go to ```http://localhost:8000/``` - - -## Back Story - -SocketCluster v2.0 and this proof-of-concept project are the culmination of more than 3 years of work towards my goal of building a -framework to power 'the next generation' of web apps. SocketCluster did not come out of nowhere - It is the successor to the following failed projects: - -1. jCombo (PHP/JavaScript full stack framework - https://github.com/jondubois/jcombo/graphs/contributors) -2. Nombo (Node.js/JavaScript full stack framework - https://github.com/jondubois/nombo/graphs/contributors) -3. SocketCluster v1.0 - -When I started working towards this goal in January 2012, I wanted to make a full-stack framework which would handle everything and allow -developers to build entire web apps from scratch. -Before the concept of 'realtime' had even entered my mind, the initial goal was simply to build a framework which would bring frontend and backend -logic closer together - This is what my first project jCombo was about; it allowed developers to call server-side PHP methods directly from client-side JavaScript code. -It also handled other things like script loading, templates, etc... - -Unfortunately, nobody really cared much about jCombo - I guess the PHP/JavaScript combination was awkward and it was my first project of this kind -so I couldn't really expect it to be a success - In any case, it was a great opportunity to develop my idea/vision of what the next generation of -web applications would be like. - -Some time after publishing jCombo on GitHub (and having completed a fair bit of work), I learned about Node.js. -At that point, I felt like I had wasted a lot of time trying to make client-side JavaScript play nicely with server-side PHP. -As much as I tried, I couldn't avoid the fact that Node.js was a tool which my project desperately needed. - -With the ability to run JavaScript on both the frontend and backend (using Node.js), I would finally be able to seamlessly exchange data between the client and -server. Socket.io was the module which really sold Node.js to me* - The idea of being able to push data directly to clients was a feature that I had thought -about but couldn't dream of implementing in PHP :p - -After playing around with Node.js some more, I decided to rewrite the entire backend part of jCombo in JavaScript (on top of Node.js). -I renamed the project to nCombo (then later to Nombo). It was only after I had finished porting all my PHP code to Node.js that -I found out about Meteor, then Derby, then SocketStream and all the other full-stack frameworks which were also trying to solve the same -problem as I was. This was back when the term 'Single Page App' started gaining traction. - -At that point, I started promoting Nombo on Reddit (/r/node) to see what the community thought of it. -Based on feedback from Reddit users, I learned the following things: - -- Nombo was a monolithic framework. -- Developers generally don't like monolithic frameworks - They prefer to use small specialized tools/modules. -- Nombo wasn't popular enough and neither was I so no developer/company would want to risk building their app on top of it. - -Around that time I also met up with an ex Google Wave engineer to discuss my project and he gave me a really important piece of advice; he -suggested that I should break Nombo up into smaller components (particularly the realtime part). And so I did just that. - -This is how SocketCluster started - I essentially just pulled it out of Nombo, cleaned up a fair bit and then published it as a stand-alone module. -After getting SC into a somewhat stable state, I ran some benchmark performance tests and posted the results -on http://reddit.com/r/node/ and then went to sleep at about 2am. The next day I woke up to find that the SocketCluster -repo had accumulated almost 600 GitHub stars overnight. It appears that someone saw my post on Reddit and then re-posted it to Hacker news where it had -made it to the front page (See https://news.ycombinator.com/item?id=7712766). - -It seems that breaking up my monolithic project into lighter stand-alone modules turned out to be a good idea after all - And this proved to be true -for many reasons. - -While I was doing all of this, some really good front-end frameworks (AngularJS, EmberJS, CanJS, Polymer, ReactJS...) were being developed. -One of the most innovative features which these frameworks were introducing was live data binding. For those who don't know, data binding -is a feature which allows you to declaratively add placeholders such as {{somePropertyName}} inside your frontend templates/views -to allow them to automagically update when the related properties are changed in your frontend code (without having to imperatively select specific elements). - -While I was getting familiar with data binding with CanJS and AngularJS, I spent a lot of time thinking about how I could extend SocketCluster to -leverage those features - It occurred to me that having Pub/Sub channels on the frontend would be a really powerful and scalable way to achieve -that - So I made pub/sub a central aspect of SC. - -The goal of client-side pub/sub in the context of SC is to allow frontend views to declaratively subscribe to data channels - Then, using data binding, -the view can passively consume/display the data from those channels in realtime as soon as it gets updated on the server. -Effectively, SC allows you to extend the data binding features of frontend frameworks beyond the client and all the way to your server and database. - -If you want to see how this all fits together in practice, follow the installation steps at the beginning of this page. - -The entry point for the backend server logic is worker.js. -All Polymer front-end code is inside the public/ directory. - -I hope you find this useful. - -Jon Dubois (https://twitter.com/jgrosdubois) - -\* SocketCluster v1.0 was based on Engine.io (the engine which powers Socket.io). diff --git a/package.json b/package.json index 685f619..8c53574 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "socketcluster-sample", "description": "A sample SocketCluster app", - "version": "1.0.1", + "version": "1.0.2", "contributors": [ { "name": "Jonathan Gros-Dubois", diff --git a/public/pages/page-category-list.html b/public/pages/page-category-list.html index 38be803..39e9182 100644 --- a/public/pages/page-category-list.html +++ b/public/pages/page-category-list.html @@ -72,7 +72,7 @@

Inventory Categories

- + Realtime collection
diff --git a/sc_modules/access-control.js b/sc_modules/access-control.js index 88b889b..9e031ed 100644 --- a/sc_modules/access-control.js +++ b/sc_modules/access-control.js @@ -1,6 +1,6 @@ module.exports.attach = function (scServer) { // Setup SocketCluster middleware for access control - + scServer.addMiddleware(scServer.MIDDLEWARE_EMIT, function (socket, event, data, next) { if (event == 'get' || event == 'set') { // If socket has a valid auth token, then allow emitting get or set events @@ -15,7 +15,7 @@ module.exports.attach = function (scServer) { next(); } }); - + scServer.addMiddleware(scServer.MIDDLEWARE_PUBLISH_IN, function (socket, channel, data, next) { // If socket has a valid auth token, then allow publishing to any channel if (socket.getAuthToken()) { @@ -25,7 +25,7 @@ module.exports.attach = function (scServer) { socket.emit('logout'); } }); - + scServer.addMiddleware(scServer.MIDDLEWARE_SUBSCRIBE, function (socket, channel, next) { // If socket has a valid auth token, then allow subscribing to any channel if (socket.getAuthToken()) { @@ -35,4 +35,4 @@ module.exports.attach = function (scServer) { socket.emit('logout'); } }); -}; \ No newline at end of file +}; diff --git a/sc_modules/authentication.js b/sc_modules/authentication.js index fa9298a..041e851 100644 --- a/sc_modules/authentication.js +++ b/sc_modules/authentication.js @@ -2,8 +2,8 @@ module.exports.attach = function (scServer, socket) { var tokenExpiresInMinutes = 10; var tokenRenewalIntervalInMilliseconds = Math.round(1000 * 60 * tokenExpiresInMinutes / 3); - - // Keep renewing the token (if there is one) at a predefined interval to make sure that + + // Keep renewing the token (if there is one) at a predefined interval to make sure that // it doesn't expire while the connection is active. var renewAuthTokenInterval = setInterval(function () { var currentToken = socket.getAuthToken(); @@ -11,14 +11,16 @@ module.exports.attach = function (scServer, socket) { socket.setAuthToken(currentToken, {expiresInMinutes: tokenExpiresInMinutes}); } }, tokenRenewalIntervalInMilliseconds); - + socket.once('disconnect', function () { clearInterval(renewAuthTokenInterval); }); - + var validateLoginDetails = function (loginDetails, respond) { - scServer.global.get(['User', loginDetails.username], function (err, storedDetails) { - if (storedDetails && storedDetails.password == loginDetails.password) { + scServer.thinky.r.table('User').filter({username: loginDetails.username}).run(function (err, results) { + console.log(2222, results); + console.log(3333, loginDetails.username); + if (results && results[0] && results[0].password == loginDetails.password) { var token = { username: loginDetails.username }; @@ -26,12 +28,12 @@ module.exports.attach = function (scServer, socket) { respond(); } else { // This is not really an error. - // We are simply rejecting the login - So we will + // We are simply rejecting the login - So we will // leave the first (error) argument as null. respond(null, 'Invalid username or password'); } }); }; - + socket.on('login', validateLoginDetails); -}; \ No newline at end of file +}; diff --git a/sc_modules/dummy-data.js b/sc_modules/dummy-data.js index 3091723..3b25f54 100644 --- a/sc_modules/dummy-data.js +++ b/sc_modules/dummy-data.js @@ -1,114 +1,63 @@ module.exports.attach = function (scServer, scCrudRethink) { /* - Note that we are using SC's default in-memory data store (scServer.global) for simplicity - but in a real scenario, you should use a proper database to store persistent data. + Add some dummy data to RethinkDB; */ - // TODO: Add data if it doesn't already exist - - // scCrudRethink.create({ - // type: 'Category', - // value: { - // "desc": "KKKKKK" , - // "name": "Smartphones2222" - // } - // }); - - // scCrudRethink.delete({ - // type: 'Category', - // id: "cbb69ddc-7ea0-4738-af31-e8f42d635f9b" - // }); - - var rethinkAdd = false; - - // Hardcoded dummy data - - var schema = { - Category: { - foreignKeys: { - products: 'Product' - } - } - }; - - scServer.global.set('Schema', schema); - - var categories = { - 1: { - name: 'Smartphones', - desc: 'Handheld mobile devices' - }, - 2: { - name: 'Tablets', - desc: 'Mobile tablet devices' - }, - 3: { - name: 'Desktops', - desc: 'Desktop computers' - }, - 4: { - name: 'Laptops', - desc: 'Laptops computers' - } - }; - - scServer.global.set('Category', categories); - - if (rethinkAdd) { - Object.keys(categories).forEach(function (id) { - var obj = categories[id]; - scCrudRethink.create({ - type: 'Category', - value: obj + scCrudRethink.read({ + type: 'User' + }, function (err, result) { + // If there is no User data, assume that we are starting with + // an empty database. + if (!result.data.length) { + var schema = { + Category: { + foreignKeys: { + products: 'Product' + } + } + }; + + var categories = { + 1: { + name: 'Smartphones', + desc: 'Handheld mobile devices' + }, + 2: { + name: 'Tablets', + desc: 'Mobile tablet devices' + }, + 3: { + name: 'Desktops', + desc: 'Desktop computers' + }, + 4: { + name: 'Laptops', + desc: 'Laptops computers' + } + }; + + Object.keys(categories).forEach(function (id) { + var obj = categories[id]; + scCrudRethink.create({ + type: 'Category', + value: obj + }); }); - }); - } - - // Hardcoded dummy data - var products = { - 1: { - name: 'Google NEXUS 6 32GB', - qty: 6, - price: 649.95, - desc: 'A smartphone by Google', - category: 'a' - }, - 2: { - name: 'Apple iPhone 6', - qty: 14, - price: 999.00, - desc: 'A smartphone by Apple', - category: 'a' - } - }; - - scServer.global.set('Product', products); - if (rethinkAdd) { - Object.keys(products).forEach(function (id) { - var obj = products[id]; - scCrudRethink.create({ - type: 'Product', - value: obj + var users = { + 'bob': { + username: 'bob', + password: 'password123' + } + }; + + Object.keys(users).forEach(function (id) { + var obj = users[id]; + scCrudRethink.create({ + type: 'User', + value: obj + }); }); - }); - } - - var users = { - 'bob': { - password: 'password123' } - }; - - scServer.global.set('User', users); - - if (rethinkAdd) { - Object.keys(users).forEach(function (id) { - var obj = users[id]; - scCrudRethink.create({ - type: 'User', - value: obj - }); - }); - } + }); }; diff --git a/worker.js b/worker.js index ee373e1..51a02f5 100644 --- a/worker.js +++ b/worker.js @@ -79,6 +79,7 @@ module.exports.run = function (worker) { }; var crud = scCrudRethink.attach(worker, crudOptions); + scServer.thinky = crud.thinky; // Add some dummy data to our store dummyData.attach(scServer, crud);