This commit is contained in:
Daniel Saewitz 2013-02-21 14:18:53 -05:00
parent b92145f5a3
commit ddff29ccf0
11 changed files with 234 additions and 72 deletions

View file

@ -26,7 +26,9 @@
"nodetime": "*",
"resolve": "~0.2.3",
"request": "~2.14.0",
"querystring": "~0.1.0"
"querystring": "~0.1.0",
"nock": "~0.15.2",
"superagent": "~0.12.4"
},
"private": true,
"subdomain": "habitrpg",

View file

@ -67,7 +67,9 @@ process.on('uncaughtException', function (error) {
});
require('coffee-script') // remove intermediate compilation requirement
require('./src/server').listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0');
module.exports = server = require('./src/server')
server.listen(process.env.PORT || 3000, process.env.IP || '0.0.0.0');
// Note: removed "up" module, which is default for development (but interferes with and production + PaaS)
// Restore to 5310bb0 if I want it back (see https://github.com/codeparty/derby/issues/165#issuecomment-10405693)

View file

@ -137,7 +137,7 @@ module.exports.updateUser = (model) ->
batch = new BatchUpdate(model)
user = batch.user
obj = batch.obj()
tasks = obj.tasks
tasks = obj?.tasks
# Remove corrupted tasks
_.each tasks, (task, key) ->
@ -172,49 +172,49 @@ module.exports.BatchUpdate = BatchUpdate = (model) ->
updates = {}
{
user: user
user: user
obj: ->
obj ?= model.get 'users.'+user.get('id')
return obj
obj: ->
obj ?= model.get 'users.'+user.get('id')
return obj
startTransaction: ->
# start a batch transaction - nothing between now and @commit() will be set immediately
transactionInProgress = true
model._dontPersist = true
@obj()
startTransaction: ->
# start a batch transaction - nothing between now and @commit() will be set immediately
transactionInProgress = true
model._dontPersist = true
@obj()
###
Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued
but not actually set to the model. It also modifies userObj in case you need to access properties manually later.
If transaction not in progress, it just runs standard model.set()
###
set: (path, val) ->
updates[path] = val if transactionInProgress
user.set(path, val)
###
Handles updating the user model. If this is an en-mass operation (eg, server cron), changes are queued
but not actually set to the model. It also modifies userObj in case you need to access properties manually later.
If transaction not in progress, it just runs standard model.set()
###
set: (path, val) ->
updates[path] = val if transactionInProgress
user.set(path, val)
###
Hack to get around dom bindings being lost if parent objects are replaced whole-sale
eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok
###
setStats: (stats) ->
stats ?= obj.stats
that = @
_.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key]
###
Hack to get around dom bindings being lost if parent objects are replaced whole-sale
eg, user.set('stats', {hp:50, exp:10...}) will break dom bindings, but user.set('stats.hp',50) is ok
###
setStats: (stats) ->
stats ?= obj.stats
that = @
_.each Object.keys(stats), (key) -> that.set "stats.#{key}", stats[key]
# queue: (path, val) ->
# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790
# arr = path.split('.')
# arr.reduce (curr, next, index) ->
# if (arr.length - 1) == index
# curr[next] = val
# curr[next]
# , obj
# queue: (path, val) ->
# # Special function for setting object properties by string dot-notation. See http://stackoverflow.com/a/6394168/362790
# arr = path.split('.')
# arr.reduce (curr, next, index) ->
# if (arr.length - 1) == index
# curr[next] = val
# curr[next]
# , obj
commit: ->
model._dontPersist = false
# some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js
user.set "update__", updates
transactionInProgress = false
updates = {}
commit: ->
model._dontPersist = false
# some hackery in our own branched racer-db-mongo, see findAndModify of lefnire/racer-db-mongo#habitrpg index.js
user.set "update__", updates
transactionInProgress = false
updates = {}
}

View file

@ -21,17 +21,19 @@ router.get '/status', (req, res) ->
status: 'up'
router.get '/user', (req, res) ->
console.log 'hi'
{ uid, token } = req.query
return res.json 500, NO_TOKEN_OR_UID unless uid || token
model = req.getModel()
query = model.query('users').withIdAndToken(uid, token)
model.fetch query, (err, user) ->
query.fetch (err, user) ->
self = user.get()
return res.json 500, err: err if err
return res.json 500, NO_USER_FOUND if !user || _.isEmpty(user)
return res.json 500, NO_USER_FOUND if !self || _.isEmpty(self)
res.json user
res.json self
router.get '/user/calendar.ics', (req, res) ->
#return next() #disable for now

View file

@ -13,6 +13,31 @@ router.get '/:uid/up/:score?', (req, res) -> res.send(500, deprecatedMessage)
router.get '/:uid/down/:score?', (req, res) -> res.send(500, deprecatedMessage)
router.post '/users/:uid/tasks/:taskId/:direction', (req, res) -> res.send(500, deprecatedMessage)
router.get '/v1/users/:uid/tasks/:taskId/:direction', (req, res) ->
{uid, taskId, direction} = req.params
{apiToken} = req.query
{title, service, icon} = req.body
console.log {params:req.params, body:req.body} if process.env.NODE_ENV == 'development'
# Send error responses for improper API call
return res.send(500, 'request body "apiToken" required') unless apiToken
return res.send(500, ':uid required') unless uid
return res.send(500, ':taskId required') unless taskId
return res.send(500, ":direction must be 'up' or 'down'") unless direction in ['up','down']
model = req.getModel()
req._isServer = true
model.fetch model.query('users').withIdAndToken(uid, apiToken), (err, result) ->
return res.send(500, err) if err
user = result.at(0)
userObj = user.get()
if _.isEmpty(userObj)
return res.send(500, "User with uid=#{uid}, token=#{apiToken} not found. Make sure you're not using your username, but your User Id")
model.ref('_user', user)
res.send(user)
router.post '/v1/users/:uid/tasks/:taskId/:direction', (req, res) ->
{uid, taskId, direction} = req.params
{apiToken, title, service, icon} = req.body

View file

@ -17,9 +17,9 @@ middleware = require './middleware'
racer.io.set('transports', ['xhr-polling'])
racer.ioClient.set('reconnection limit', 300000) # max reconect timeout to 5 minutes
racer.set('bundleTimeout', 40000)
#unless process.env.NODE_ENV == 'production'
# racer.use(racer.logPlugin)
# derby.use(derby.logPlugin)
unless process.env.NODE_ENV == 'production'
racer.use(racer.logPlugin)
derby.use(derby.logPlugin)
## SERVER CONFIGURATION ##
@ -28,7 +28,7 @@ server = http.createServer expressApp
module.exports = server
derby.use require('racer-db-mongo')
store = derby.createStore
module.exports.habitStore = store = derby.createStore
db: {type: 'Mongo', uri: process.env.NODE_DB_URI, safe:true}
listen: server

View file

@ -1,7 +1,6 @@
module.exports.splash = (req, res, next) ->
# This was an API call, not a page load
return next() if req.is('json')
return next() if /^\/api/.test req.path
if !req.session.userId? and !req.query?.play?
res.redirect('/splash.html')
else

View file

@ -54,12 +54,12 @@ userAccess = (store) ->
###
REST = (store) ->
store.query.expose "users", "withIdAndToken", (uid, token) ->
@where('id').equals(uid)
@byId(uid)
.where('apiToken').equals(token)
.one
store.queryAccess "users", "withIdAndToken", (id, token, accept, err) ->
return accept(true) if id && token
store.queryAccess "users", "withIdAndToken", (uid, token, accept, err) ->
return accept(true) if uid && token
accept(false) # only user has id & token
@ -90,4 +90,4 @@ partySystem = (store) ->
store.writeAccess "*", "parties.*", ->
accept = arguments[arguments.length-2]
accept(true)
accept(true)

View file

@ -1,11 +1,16 @@
assert = require 'assert'
{BrowserModel: Model} = require 'racer/test/util/model'
derby = require 'derby'
racer = require 'racer'
_ = require 'underscore'
moment = require 'moment'
request = require 'request'
request = require 'superagent'
qs = require 'querystring'
app = require '../server'
store = app.habitStore
# Custom modules
scoring = require '../src/app/scoring'
character = require '../src/app/character'
@ -75,27 +80,55 @@ modificationsLookup = (direction, options = {}) ->
describe 'API', ->
model = null
user = null
params = null
describe 'Without token or user id', ->
it '/api/v1/user', (done) ->
request.get { uri: "#{baseURL}/user" }, (err, res, body) ->
console.log "#{baseURL}/user", body
assert.ok !err
assert.equal res.statusCode, 500
assert.ok body.err
done()
request(app)
.get("/api/v1/user")
.set('Accept', 'application/json')
.on('error', (err) ->
console.log 'err', err
)
.end (res) ->
assert.equal res.statusCode, 500
assert.ok JSON.parse(res.text).err
done()
describe 'With token and user id', ->
before ->
model = new Model
model.set '_user', character.newUserObject()
scoring.setModel model
before (done) ->
model = store.createModel()
store.flush()
uid = model.id()
user = character.newUserObject()
user.apiToken = derby.uuid()
model.set "users.#{uid}", user
user = model.get("users.#{uid}")
params =
uid: user.id
token: user.apiToken
done()
after (done) ->
store.flush done
it '/api/v1/user', (done) ->
user = model.get '_user'
request.get { uri: "#{baseURL}/user?#{qs.stringify(UID_AND_TOKEN)}" }, (err, res, body) ->
assert.ok !err
assert.equal res.statusCode, 200
done()
console.log params
request(app)
.get("/api/v1/user")
.set('Accept', 'application/json')
.query(params)
.on('error', (err) ->
console.log 'err', err
)
.end (res) ->
console.log 'test', res.body
assert.ok !res.body.err
assert.equal res.statusCode, 200
assert.ok res.body
done()

25
test/request/local.coffee Normal file
View file

@ -0,0 +1,25 @@
Request = require './request'
qs = require 'querystring'
makeUrl = (path, params) ->
if typeof params is "object"
params = qs.stringify(params)
else
params = ''
return "http://localhost:3000/#{path}?#{params}"
# url, params (optional), callback
get = (obj, callback) ->
obj.params ||= {}
obj.path ||= ''
requestUrl = makeUrl obj.path, obj.params
request = new Request requestUrl
request.done (res) ->
callback null, JSON.parse(res)
request.fire()
module.exports = { get }

View file

@ -0,0 +1,74 @@
http = require 'http'
# ### Request
#
# The Request class is just a wrapper
# around the `http.request` function,
# providing a cleaner and more reusable
# interface.
#
# `constructor(options)`
#
# `options` can be anything `http.request` takes.
#
# Callbacks can be queued up by calling `request.done`
# and passing it a callback function to be invoked when the
# http request has completed
#
# Example:
#
# callback = (json) ->
# doSomethingWithJSON( json )
#
# request = new Request('http://api.phish.net/api.js?')
# request.done (response) ->
# callback JSON.parse(response)[0]
#
# request.fire(data) # data refers to the post data
#
class Request
constructor: (@options) ->
@callbacks = {}
fire: (data) ->
request = http.request(@options, @_handler)
request.on 'error', (err) =>
for callback in @callbacks['fail']
callback(err)
# Send data with request
if data
request.write data
request.end()
this
done: (callback) ->
push.call(this, 'done', callback)
this
fail: (callback) ->
push.call(this, 'fail', callback)
this
###########
# PRIVATE #
###########
_handler: (response) =>
data = ''
response.on 'data', (chunk) ->
data += chunk
response.on 'end', =>
for callback in @callbacks['done']
callback(data)
push = (type, callback) ->
@callbacks[type] ?= []
@callbacks[type].push callback
module.exports = Request