Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

init

  • Loading branch information...
commit 238b608a91829a2d4eb5e289202a8c21658a0dda 0 parents
skamarrudin authored
1  .gitignore
@@ -0,0 +1 @@
+/node_modules/
1  Procfile
@@ -0,0 +1 @@
+web: node app.js
49 app.js
@@ -0,0 +1,49 @@
+
+/**
+ * Module dependencies.
+ */
+
+var express = require('express')
+ , routes = require('./routes');
+
+var app = module.exports = express.createServer();
+
+// Configuration
+
+app.configure(function(){
+ app.set('views', __dirname + '/views');
+ app.set('view engine', 'jade');
+ app.use(express.bodyParser());
+ app.use(express.methodOverride());
+ app.use(require('stylus').middleware({ src: __dirname + '/public' }));
+ app.use(app.router);
+ app.use(express.static(__dirname + '/public'));
+});
+
+app.configure('development', function(){
+ app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
+});
+
+app.configure('production', function(){
+ app.use(express.errorHandler());
+});
+
+app.helpers({
+ title: 'Node-Neo4j Template' // default title
+});
+
+// Routes
+
+app.get('/', routes.site.index);
+
+app.get('/users', routes.users.list);
+app.post('/users', routes.users.create);
+app.get('/users/:id', routes.users.show);
+app.post('/users/:id', routes.users.edit);
+app.del('/users/:id', routes.users.del);
+
+app.post('/users/:id/follow', routes.users.follow);
+app.post('/users/:id/unfollow', routes.users.unfollow);
+
+app.listen(process.env.PORT || 3000);
+console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env);
178 models/user.js
@@ -0,0 +1,178 @@
+// user.js
+// User model logic.
+
+var neo4j = require('neo4j');
+var db = new neo4j.GraphDatabase(process.env.NEO4J_URL || 'http://localhost:7474');
+
+// constants:
+
+var INDEX_NAME = 'nodes';
+var INDEX_KEY = 'type';
+var INDEX_VAL = 'user';
+
+var FOLLOWS_REL = 'follows';
+
+// private constructor:
+
+var User = module.exports = function User(_node) {
+ // all we'll really store is the node; the rest of our properties will be
+ // derivable or just pass-through properties (see below).
+ this._node = _node;
+}
+
+// pass-through node properties:
+
+function proxyProperty(prop, isData) {
+ Object.defineProperty(User.prototype, prop, {
+ get: function () {
+ if (isData) {
+ return this._node.data[prop];
+ } else {
+ return this._node[prop];
+ }
+ },
+ set: function (value) {
+ if (isData) {
+ this._node.data[prop] = value;
+ } else {
+ this._node[prop] = value;
+ }
+ }
+ });
+}
+
+proxyProperty('id');
+proxyProperty('exists');
+
+proxyProperty('name', true);
+
+// private instance methods:
+
+User.prototype._getFollowingRel = function (other, callback) {
+ var query = [
+ 'START user=node({userId}), other=node({otherId})',
+ 'MATCH (user) -[rel?:FOLLOWS_REL]-> (other)',
+ 'RETURN rel'
+ ].join('\n')
+ .replace('FOLLOWS_REL', FOLLOWS_REL);
+
+ var params = {
+ userId: this.id,
+ otherId: other.id,
+ };
+
+ db.query(query, params, function (err, results) {
+ if (err) return callback(err);
+ var rel = results[0] && results[0]['rel'];
+ callback(null, rel);
+ });
+};
+
+// public instance methods:
+
+User.prototype.save = function (callback) {
+ this._node.save(function (err) {
+ callback(err);
+ });
+};
+
+User.prototype.del = function (callback) {
+ this._node.del(function (err) {
+ callback(err);
+ }, true); // true = yes, force it (delete all relationships)
+};
+
+User.prototype.follow = function (other, callback) {
+ this._node.createRelationshipTo(other._node, 'follows', {}, function (err, rel) {
+ callback(err);
+ });
+};
+
+User.prototype.unfollow = function (other, callback) {
+ this._getFollowingRel(other, function (err, rel) {
+ if (err) return callback(err);
+ if (!rel) return callback(null);
+ rel.del(function (err) {
+ callback(err);
+ });
+ });
+};
+
+// calls callback w/ (err, following, others) where following is an array of
+// users this user follows, and others is all other users minus him/herself.
+User.prototype.getFollowingAndOthers = function (callback) {
+ // query all users and whether we follow each one or not:
+ var query = [
+ 'START user=node({userId}), other=node:INDEX_NAME(INDEX_KEY="INDEX_VAL")',
+ 'MATCH (user) -[rel?:FOLLOWS_REL]-> (other)',
+ 'RETURN other, COUNT(rel)' // COUNT(rel) is a hack for 1 or 0
+ ].join('\n')
+ .replace('INDEX_NAME', INDEX_NAME)
+ .replace('INDEX_KEY', INDEX_KEY)
+ .replace('INDEX_VAL', INDEX_VAL)
+ .replace('FOLLOWS_REL', FOLLOWS_REL);
+
+ var params = {
+ userId: this.id,
+ };
+
+ var user = this;
+ db.query(query, params, function (err, results) {
+ if (err) return callback(err);
+
+ var following = [];
+ var others = [];
+
+ for (var i = 0; i < results.length; i++) {
+ var other = new User(results[i]['other']);
+ var follows = results[i]['count(rel)'];
+ // XXX neo4j bug: returned names are always lowercase!
+ // TODO FIXME when updating to the next version of neo4j.
+
+ if (user.id === other.id) {
+ continue;
+ } else if (follows) {
+ following.push(other);
+ } else {
+ others.push(other);
+ }
+ }
+
+ callback(null, following, others);
+ });
+};
+
+// static methods:
+
+User.get = function (id, callback) {
+ db.getNodeById(id, function (err, node) {
+ if (err) return callback(err);
+ callback(null, new User(node));
+ });
+};
+
+User.getAll = function (callback) {
+ db.getIndexedNodes(INDEX_NAME, INDEX_KEY, INDEX_VAL, function (err, nodes) {
+ // if (err) return callback(err);
+ // XXX FIXME the index might not exist in the beginning, so special-case
+ // this error detection. warning: this is super brittle!!
+ if (err) return callback(null, []);
+ var users = nodes.map(function (node) {
+ return new User(node);
+ });
+ callback(null, users);
+ });
+};
+
+// creates the user and persists (saves) it to the db, incl. indexing it:
+User.create = function (data, callback) {
+ var node = db.createNode(data);
+ var user = new User(node);
+ node.save(function (err) {
+ if (err) return callback(err);
+ node.index(INDEX_NAME, INDEX_KEY, INDEX_VAL, function (err) {
+ if (err) return callback(err);
+ callback(null, user);
+ });
+ });
+};
22 package.json
@@ -0,0 +1,22 @@
+{ "name": "NodeNeo4jTemplate"
+ , "description": "A sample Node.js + Neo4j app."
+ , "version": "0.0.1"
+ , "author": "Aseem Kishore <aseem.kishore@gmail.com>"
+ , "private": true
+ , "dependencies":
+ { "express": "~2.5.8"
+ , "jade": "~0.26.1"
+ , "neo4j": "~0.2.12"
+ , "stylus": "~0.27.1"
+ }
+ , "devDependencies":
+ { "node-dev": "~0.2.3"
+ }
+ , "engines":
+ { "node": "0.6.x"
+ , "npm": "1.1.x"
+ }
+ , "scripts":
+ { "start": "node-dev app.js"
+ }
+}
11 public/stylesheets/style.css
@@ -0,0 +1,11 @@
+body {
+ padding: 50px;
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
+}
+a {
+ color: #00b7ff;
+}
+strong a {
+ padding: 0 0.25em;
+ background-color: #ffa;
+}
8 public/stylesheets/style.styl
@@ -0,0 +1,8 @@
+body
+ padding: 50px
+ font: 14px "Lucida Grande", Helvetica, Arial, sans-serif
+a
+ color: #00B7FF
+strong a
+ padding: 0 0.25em
+ background-color: #ffa;
57 readme.md
@@ -0,0 +1,57 @@
+# Node-Neo4j Template
+
+This is a template app showing the use of [Neo4j][] from Node.js. It uses the
+[node-neo4j][] library, available on npm as `neo4j`.
+
+The app is a simple social network manager: it lets you add and remove users
+and "follows" relationships between them.
+
+This app supports deploying to Heroku, and a demo is in fact running live at
+[http://nodeneo4jtemplate.herokuapp.com/](http://nodeneo4jtemplate.herokuapp.com/).
+
+So try it out, browse the code, and fork this project to get a head start on
+creating your own Node-Neo4j app. Enjoy!
+
+
+## Installation
+
+```bash
+# Install the required dependencies
+npm install
+
+# Install a local Neo4j instance
+curl "http://dist.neo4j.org/neo4j-community-1.6-unix.tar.gz" --O "db-unix.tar.gz"
+tar -zxvf db-unix.tar.gz 2> /dev/null
+rm db-unix.tar.gz
+```
+
+
+## Usage
+
+```bash
+# Start the local Neo4j instance
+neo4j-community-1.6/bin/neo4j start
+
+# Run the app!
+npm start
+```
+
+The app will now be accessible at [http://localhost:3000/](http://localhost:3000/).
+
+The UI is admittedly quite crappy, but hopefully it shows the functionality.
+(Anyway, this project is really about the code! =P)
+
+
+## Miscellany
+
+- MIT license.
+- Questions/comments/etc. are welcome.
+- As an exercise, I built this without using [CoffeeScript][coffeescript] or
+ [Streamline][streamline]. What a gigantic pain! Never again. =P
+
+
+[Neo4j]: http://www.neo4j.org/
+[node-neo4j]: https://github.com/thingdom/node-neo4j
+
+[coffeescript]: http://www.coffeescript.org/
+[streamline]: https://github.com/Sage/streamlinejs
3  routes/index.js
@@ -0,0 +1,3 @@
+// convenience wrapper around all other files:
+exports.site = require('./site');
+exports.users = require('./users');
8 routes/site.js
@@ -0,0 +1,8 @@
+
+/*
+ * GET home page.
+ */
+
+exports.index = function(req, res){
+ res.render('index');
+};
91 routes/users.js
@@ -0,0 +1,91 @@
+// users.js
+// Routes to CRUD users.
+
+var User = require('../models/user');
+
+// GET /users
+exports.list = function (req, res, next) {
+ User.getAll(function (err, users) {
+ if (err) return next(err);
+ res.render('users', {
+ users: users
+ });
+ });
+};
+
+// POST /users
+exports.create = function (req, res, next) {
+ User.create({
+ name: req.body['name']
+ }, function (err, user) {
+ if (err) return next(err);
+ res.redirect('/users/' + user.id);
+ });
+};
+
+// GET /users/:id
+exports.show = function (req, res, next) {
+ User.get(req.params.id, function (err, user) {
+ if (err) return next(err);
+ // TODO also fetch and show followers?
+ user.getFollowingAndOthers(function (err, following, others) {
+ if (err) return next(err);
+ res.render('user', {
+ user: user,
+ following: following,
+ others: others
+ });
+ });
+ });
+};
+
+// POST /users/:id
+exports.edit = function (req, res, next) {
+ User.get(req.params.id, function (err, user) {
+ if (err) return next(err);
+ user.name = req.body['name'];
+ user.save(function (err) {
+ if (err) return next(err);
+ res.redirect('/users/' + user.id);
+ });
+ });
+};
+
+// DELETE /users/:id
+exports.del = function (req, res, next) {
+ User.get(req.params.id, function (err, user) {
+ if (err) return next(err);
+ user.del(function (err) {
+ if (err) return next(err);
+ res.redirect('/users');
+ });
+ });
+};
+
+// POST /users/:id/follow
+exports.follow = function (req, res, next) {
+ User.get(req.params.id, function (err, user) {
+ if (err) return next(err);
+ User.get(req.body.user.id, function (err, other) {
+ if (err) return next(err);
+ user.follow(other, function (err) {
+ if (err) return next(err);
+ res.redirect('/users/' + user.id);
+ });
+ });
+ });
+};
+
+// POST /users/:id/unfollow
+exports.unfollow = function (req, res, next) {
+ User.get(req.params.id, function (err, user) {
+ if (err) return next(err);
+ User.get(req.body.user.id, function (err, other) {
+ if (err) return next(err);
+ user.unfollow(other, function (err) {
+ if (err) return next(err);
+ res.redirect('/users/' + user.id);
+ });
+ });
+ });
+};
19 views/index.jade
@@ -0,0 +1,19 @@
+h1= title
+
+p
+ | This is a template app showing the use of&nbsp;
+ a(href='http://www.neo4j.org/', target='_blank') Neo4j
+ | from Node.js. It uses the&nbsp;
+ a(href='https://github.com/thingdom/node-neo4j', target='_blank') node-neo4j
+ | library, available on npm as&nbsp;
+ code neo4j
+ .
+
+p.
+ This app is a simple social network manager: it lets you add and remove
+ users and "follows" relationships between them.
+
+p
+ strong
+ | Get started:&nbsp;
+ a(href='/users') View all users
14 views/layout.jade
@@ -0,0 +1,14 @@
+!!!
+html
+ head
+ title= title
+ link(rel='stylesheet', href='/stylesheets/style.css')
+ body!= body
+
+// GitHub ribbon! via https://github.com/blog/273-github-ribbons
+a(href='https://github.com/aseemk/node-neo4j-template')
+ img(
+ style='position: absolute; top: 0; right: 0; border: 0;'
+ src='https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png'
+ alt='Fork me on GitHub'
+ )
39 views/user.jade
@@ -0,0 +1,39 @@
+h1 #{user.name}
+
+p
+ a(href='/users') Back to everyone
+
+if following.length
+ p #{user.name} is following #{following.length} users:
+ // TODO should say 'user' if only one! ;)
+ ul.users
+ for other in following
+ li.user
+ form(action='/users/#{user.id}/unfollow', method='POST')
+ a(href='/users/#{other.id}') #{other.name}
+ input(type='hidden', name='user[id]', value='#{other.id}')
+ input(type='submit', class='unfollow', value='x')
+else
+ p #{user.name} isn't following anyone currently.
+
+if others.length
+ form(action='/users/#{user.id}/follow', method='POST')
+ p Add someone for #{user.name} to follow:
+ label
+ select(name='user[id]', required)
+ option(value='')
+ for user in others
+ option(value='#{user.id}') #{user.name}
+ input(type='submit', value='Follow')
+else
+ p There's no one else left for #{user.name} to follow!
+
+form(action='/users/#{user.id}', method='POST')
+ p Edit this user:
+ input(type='text', name='name', placeholder='#{user.name}', required)
+ input(type='submit', value='Update')
+
+form(action='/users/#{user.id}', method='POST', onsubmit='return confirm("Are you sure?");')
+ p And if you're feeling destructive…
+ input(type='hidden', name='_method', value='DELETE')
+ input(type='submit', value='Delete User')
15 views/users.jade
@@ -0,0 +1,15 @@
+h1 Users
+
+if users.length
+ p Here are the current users:
+ ul.users
+ for user in users
+ li.user
+ a(href='/users/#{user.id}') #{user.name}
+else
+ p There are no users currently.
+
+form(action='', method='POST')
+ p Create a new user:
+ input(type='text', name='name', placeholder='Name', required)
+ input(type='submit', value='Create')
Please sign in to comment.
Something went wrong with that request. Please try again.