Skip to content

Commit

Permalink
v2: Move to unique username property + constraint.
Browse files Browse the repository at this point in the history
  • Loading branch information
aseemk committed Jun 12, 2015
1 parent 7a811e5 commit 6dd9c0e
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 97 deletions.
10 changes: 5 additions & 5 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,12 @@ 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.get('/users/:username', routes.users.show);
app.post('/users/:username', routes.users.edit);
app.del('/users/:username', routes.users.del);

app.post('/users/:id/follow', routes.users.follow);
app.post('/users/:id/unfollow', routes.users.unfollow);
app.post('/users/:username/follow', routes.users.follow);
app.post('/users/:username/unfollow', routes.users.unfollow);

http.createServer(app).listen(app.get('port'), function(){
console.log('Express server listening at: http://localhost:%d/', app.get('port'));
Expand Down
80 changes: 45 additions & 35 deletions models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,9 @@ var User = module.exports = function User(_node) {

// Public instance properties:

// TODO: Using native Neo4j IDs is now discouraged; switch to an indexed+unique
// property instead, e.g. `email` or `username` or etc.
Object.defineProperty(User.prototype, 'id', {
get: function () { return this._node._id; }
});

Object.defineProperty(User.prototype, 'name', {
get: function () { return this._node.properties['name']; }
// The user's username, e.g. 'aseemk'.
Object.defineProperty(User.prototype, 'username', {
get: function () { return this._node.properties['username']; }
});

// Private helpers:
Expand All @@ -37,10 +32,10 @@ Object.defineProperty(User.prototype, 'name', {
// instance properties), selects only whitelisted ones for editing, validates
// them, and translates them to the corresponding internal db properties.
function translate(props) {
// Today, the only property we have is `name`; it's the same; and it needs
// no validation. (Might want to validate things like length, Unicode, etc.)
// Today, the only property we have is `username`.
// TODO: Validate it. E.g. length, acceptable chars, etc.
return {
name: props.name,
username: props.username,
};
}

Expand All @@ -52,14 +47,13 @@ User.prototype.patch = function (props, callback) {
var safeProps = translate(props);

var query = [
'MATCH (user:User)',
'WHERE ID(user) = {id}',
'MATCH (user:User {username: {username}})',
'SET user += {props}',
'RETURN user',
].join('\n');

var params = {
id: this.id,
username: this.username,
props: safeProps,
};

Expand All @@ -72,7 +66,7 @@ User.prototype.patch = function (props, callback) {
if (err) return callback(err);

if (!results.length) {
err = new Error('User has been deleted! ID: ' + self.id);
err = new Error('User has been deleted! Username: ' + self.username);
return callback(err);
}

Expand All @@ -89,14 +83,13 @@ User.prototype.del = function (callback) {
// (Note that this'll still fail if there are any relationships attached
// of any other types, which is good because we don't expect any.)
var query = [
'MATCH (user:User)',
'WHERE ID(user) = {userId}',
'MATCH (user:User {username: {username}})',
'OPTIONAL MATCH (user) -[rel:follows]- (other)',
'DELETE user, rel',
].join('\n')

var params = {
userId: this.id
username: this.username,
};

db.cypher({
Expand All @@ -109,14 +102,14 @@ User.prototype.del = function (callback) {

User.prototype.follow = function (other, callback) {
var query = [
'MATCH (user:User) ,(other:User)',
'WHERE ID(user) = {userId} AND ID(other) = {otherId}',
'MATCH (user:User {username: {thisUsername}})',
'MATCH (other:User {username: {otherUsername}})',
'MERGE (user) -[rel:follows]-> (other)',
].join('\n')

var params = {
userId: this.id,
otherId: other.id,
thisUsername: this.username,
otherUsername: other.username,
};

db.cypher({
Expand All @@ -129,14 +122,15 @@ User.prototype.follow = function (other, callback) {

User.prototype.unfollow = function (other, callback) {
var query = [
'MATCH (user:User) -[rel:follows]-> (other:User)',
'WHERE ID(user) = {userId} AND ID(other) = {otherId}',
'MATCH (user:User {username: {thisUsername}})',
'MATCH (other:User {username: {otherUsername}})',
'MATCH (user) -[rel:follows]-> (other)',
'DELETE rel',
].join('\n')

var params = {
userId: this.id,
otherId: other.id,
thisUsername: this.username,
otherUsername: other.username,
};

db.cypher({
Expand All @@ -152,14 +146,14 @@ User.prototype.unfollow = function (other, callback) {
User.prototype.getFollowingAndOthers = function (callback) {
// Query all users and whether we follow each one or not:
var query = [
'MATCH (user:User), (other:User)',
'MATCH (user:User {username: {thisUsername}})',
'MATCH (other:User)',
'OPTIONAL MATCH (user) -[rel:follows]-> (other)',
'WHERE ID(user) = {userId}',
'RETURN other, COUNT(rel)', // COUNT(rel) is a hack for 1 or 0
].join('\n')

var params = {
userId: this.id,
thisUsername: this.username,
};

var user = this;
Expand All @@ -176,7 +170,7 @@ User.prototype.getFollowingAndOthers = function (callback) {
var other = new User(results[i]['other']);
var follows = results[i]['COUNT(rel)'];

if (user.id === other.id) {
if (user.username === other.username) {
continue;
} else if (follows) {
following.push(other);
Expand All @@ -191,15 +185,14 @@ User.prototype.getFollowingAndOthers = function (callback) {

// Static methods:

User.get = function (id, callback) {
User.get = function (username, callback) {
var query = [
'MATCH (user:User)',
'WHERE ID(user) = {id}',
'MATCH (user:User {username: {username}})',
'RETURN user',
].join('\n')

var params = {
id: parseInt(id, 10),
username: username,
};

db.cypher({
Expand All @@ -208,7 +201,7 @@ User.get = function (id, callback) {
}, function (err, results) {
if (err) return callback(err);
if (!results.length) {
err = new Error('No such user with ID: ' + id);
err = new Error('No such user with username: ' + username);
return callback(err);
}
var user = new User(results[0]['user']);
Expand Down Expand Up @@ -253,3 +246,20 @@ User.create = function (props, callback) {
callback(null, user);
});
};

// Static initialization:

// Register our unique username constraint.
// TODO: This is done async'ly (fire and forget) here for simplicity,
// but this would be better as a formal schema migration script or similar.
db.createConstraint({
label: 'User',
property: 'username',
}, function (err, constraint) {
if (err) throw err; // Failing fast for now, by crash the application.
if (constraint) {
console.log('(Registered unique usernames constraint.)');
} else {
// Constraint already present; no need to log anything.
}
})
34 changes: 17 additions & 17 deletions routes/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ exports.list = function (req, res, next) {
*/
exports.create = function (req, res, next) {
User.create({
name: req.body['name']
username: req.body['username']
}, function (err, user) {
if (err) return next(err);
res.redirect('/users/' + user.id);
res.redirect('/users/' + user.username);
});
};

/**
* GET /users/:id
* GET /users/:username
*/
exports.show = function (req, res, next) {
User.get(req.params.id, function (err, user) {
User.get(req.params.username, function (err, user) {
if (err) return next(err);
// TODO: Also fetch and show followers? (Not just follow*ing*.)
user.getFollowingAndOthers(function (err, following, others) {
Expand All @@ -46,23 +46,23 @@ exports.show = function (req, res, next) {
};

/**
* POST /users/:id
* POST /users/:username
*/
exports.edit = function (req, res, next) {
User.get(req.params.id, function (err, user) {
User.get(req.params.username, function (err, user) {
if (err) return next(err);
user.patch(req.body, function (err) {
if (err) return next(err);
res.redirect('/users/' + user.id);
res.redirect('/users/' + user.username);
});
});
};

/**
* DELETE /users/:id
* DELETE /users/:username
*/
exports.del = function (req, res, next) {
User.get(req.params.id, function (err, user) {
User.get(req.params.username, function (err, user) {
if (err) return next(err);
user.del(function (err) {
if (err) return next(err);
Expand All @@ -72,32 +72,32 @@ exports.del = function (req, res, next) {
};

/**
* POST /users/:id/follow
* POST /users/:username/follow
*/
exports.follow = function (req, res, next) {
User.get(req.params.id, function (err, user) {
User.get(req.params.username, function (err, user) {
if (err) return next(err);
User.get(req.body.user.id, function (err, other) {
User.get(req.body.otherUsername, function (err, other) {
if (err) return next(err);
user.follow(other, function (err) {
if (err) return next(err);
res.redirect('/users/' + user.id);
res.redirect('/users/' + user.username);
});
});
});
};

/**
* POST /users/:id/unfollow
* POST /users/:username/unfollow
*/
exports.unfollow = function (req, res, next) {
User.get(req.params.id, function (err, user) {
User.get(req.params.username, function (err, user) {
if (err) return next(err);
User.get(req.body.user.id, function (err, other) {
User.get(req.body.otherUsername, function (err, other) {
if (err) return next(err);
user.unfollow(other, function (err) {
if (err) return next(err);
res.redirect('/users/' + user.id);
res.redirect('/users/' + user.username);
});
});
});
Expand Down
Loading

0 comments on commit 6dd9c0e

Please sign in to comment.