Skip to content

Commit

Permalink
Replace iron:router with simple:json-routes
Browse files Browse the repository at this point in the history
iron:router was causing some bugs and inconveniences for users.
simple:json-routes does only the server-side routing parts, without
introducing client-side code from iron:router.

See additional discussion on the previous PR #41
  • Loading branch information
Sashko Stubailo authored and kahmali committed Jul 2, 2015
1 parent 7afb2e4 commit adff4c3
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 54 deletions.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
### Project ###

## Autogenerated Javascript files (since we use coffescript)
*.js
*.map

## Package config file needs to remain in JS for now ##
Expand Down
32 changes: 32 additions & 0 deletions lib/iron-router-error-to-response.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// We need a function that treats thrown errors exactly like Iron Router would.
// This file is written in JavaScript to enable copy-pasting Iron Router code.

// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L3
var env = process.env.NODE_ENV || 'development';

// Taken from: https://github.com/iron-meteor/iron-router/blob/9c369499c98af9fd12ef9e68338dee3b1b1276aa/lib/router_server.js#L47
ironRouterSendErrorToResponse = function (err, req, res) {
if (res.statusCode < 400)
res.statusCode = 500;

if (err.status)
res.statusCode = err.status;

if (env === 'development')
msg = (err.stack || err.toString()) + '\n';
else
//XXX get this from standard dict of error messages?
msg = 'Server error.';

console.error(err.stack || err.toString());

if (res.headersSent)
return req.socket.destroy();

res.setHeader('Content-Type', 'text/html');
res.setHeader('Content-Length', Buffer.byteLength(msg));
if (req.method === 'HEAD')
return res.end();
res.end(msg);
return;
}
5 changes: 0 additions & 5 deletions lib/restivus.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class @Restivus
token: Accounts._hashLoginToken @request.headers['x-auth-token']
onLoggedIn: -> {}
onLoggedOut: -> {}
useClientRouter: true
defaultHeaders:
'Content-Type': 'application/json'
enableCors: true
Expand Down Expand Up @@ -46,10 +45,6 @@ class @Restivus
if _.last(@config.apiPath) isnt '/'
@config.apiPath = @config.apiPath + '/'

# Disable Iron Router on the client if it's not needed
if not @config.useClientRouter and Meteor.isClient
Router.options.autoStart = false

# Add any existing routes to the API now that it's configured
_.each @routes, (route) -> route.addToApi()

Expand Down
71 changes: 42 additions & 29 deletions lib/route.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,57 @@ class @Route

# Setup endpoints on route using Iron Router
fullPath = @api.config.apiPath + @path
Router.route fullPath,
where: 'server'
action: ->

_.each @endpoints, (options, method) ->
JsonRoutes.add method, fullPath, (req, res, next) ->
# Add parameters in the URL and request body to the endpoint context
# TODO: Decide whether or not to nullify the copied objects. Makes sense to do it, right?
@urlParams = @params
@queryParams = @params.query
@bodyParams = @request.body

# Add function to endpoint context for indicating a response has been initiated manually
@done = =>
@_responseInitiated = true
@urlParams = req.params
@queryParams = req.query
@bodyParams = req.body

# Run the requested endpoint
responseData = null
method = @request.method
if self.endpoints[method.toLowerCase()]
# Add the endpoint's resolved configuration options to its context
_.extend this, self.endpoints[method.toLowerCase()]
responseData = self._callEndpoint this, self.endpoints[method.toLowerCase()]
else
responseData = {statusCode: 404, body: {status: "error", message:'API endpoint not found'}}

if responseData is null or responseData is undefined
throw new Error "Cannot return null or undefined from an endpoint: #{method} #{fullPath}"
if @response.headersSent and not @_responseInitiated
throw new Error "Must call this.done() after handling endpoint response manually: #{method} #{fullPath}"
# Add the endpoint's resolved configuration options to its context
endpointContext = {};
_.extend endpointContext, options

# Add function to endpoint context for indicating a response has been initiated manually
responseInitiated = false
doneFunc = ->
responseInitiated = true

endpointContext =
urlParams: req.params
queryParams: req.query
bodyParams: req.body
request: req
response: res
done: doneFunc

try
responseData = self._callEndpoint endpointContext, options

if responseData is null or responseData is undefined
throw new Error "Cannot return null or undefined from an endpoint: #{method} #{fullPath}"
if res.headersSent and not responseInitiated
throw new Error "Must call this.done() after handling endpoint response manually: #{method} #{fullPath}"
catch error
# Do exactly what Iron Router would have done, to avoid changing the API
ironRouterSendErrorToResponse(error, req, res);
return

if @_responseInitiated
if responseInitiated
# Ensure the response is properly completed
@response.end()
res.end()
return

# Generate and return the http response, handling the different endpoint response types
if responseData.body and (responseData.statusCode or responseData.headers)
self._respond this, responseData.body, responseData.statusCode, responseData.headers
self._respond res, responseData.body, responseData.statusCode, responseData.headers
else
self._respond this, responseData
self._respond res, responseData


###
Expand Down Expand Up @@ -190,7 +203,7 @@ class @Route
###
Respond to an HTTP request
###
_respond: (endpointContext, body, statusCode=200, headers={}) ->
_respond: (response, body, statusCode=200, headers={}) ->
# Override any default headers that have been provided (keys are normalized to be case insensitive)
# TODO: Consider only lowercasing the header keys we need normalized, like Content-Type
defaultHeaders = @_lowerCaseKeys @api.config.defaultHeaders
Expand All @@ -206,9 +219,9 @@ class @Route

# Send response
sendResponse = ->
endpointContext.response.writeHead statusCode, headers
endpointContext.response.write body
endpointContext.response.end()
response.writeHead statusCode, headers
response.write body
response.end()
if statusCode in [401, 403]
# Hackers can measure the response time to determine things like whether the 401 response was
# caused by bad user id vs bad password.
Expand Down
5 changes: 2 additions & 3 deletions package.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ Package.onUse(function (api) {
api.use('check');
api.use('coffeescript');
api.use('underscore');
api.use('iron:router@1.0.6');
api.use('accounts-base@1.2.0');

// Package files
api.use('simple:json-routes@1.0.2');
api.addFiles('lib/restivus.coffee', 'server');
api.addFiles('lib/iron-router-error-to-response.js', 'server');
api.addFiles('lib/route.coffee', 'server');
api.addFiles('lib/auth.coffee', 'server');

Expand Down
23 changes: 7 additions & 16 deletions test/api_tests.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,19 @@ Meteor.startup ->


describe 'A collection route', ->
it 'should be able to exclude endpoints using just the excludedEndpoints option', (test, next) ->
it 'should be able to exclude endpoints using just the excludedEndpoints option', (test) ->
Restivus.addCollection new Mongo.Collection('tests2'),
excludedEndpoints: ['get', 'getAll']

# Since the endpoints don't exist, they just return the Meteor HTML code
result = HTTP.get 'http://localhost:3000/api/v1/tests2/10'
test.isTrue /__meteor_runtime_config__/.test(result.content)

HTTP.get 'http://localhost:3000/api/v1/tests2/10', (error, result) ->
response = JSON.parse result.content
test.isTrue error
test.equal result.statusCode, 404
test.equal response.status, 'error'
test.equal response.message, 'API endpoint not found'

HTTP.get 'http://localhost:3000/api/v1/tests2/', (error, result) ->
response = JSON.parse result.content
test.isTrue error
test.equal result.statusCode, 404
test.equal response.status, 'error'
test.equal response.message, 'API endpoint not found'
next()
result = HTTP.get 'http://localhost:3000/api/v1/tests2/'
test.isTrue /__meteor_runtime_config__/.test(result.content)

context 'with the default autogenerated endpoints', ->
Restivus.addCollection new Mongo.Collection('testautogen')
Restivus.addCollection new Mongo.Collection('testAutogen')
testId = null

it 'should support a POST on api/collection', (test) ->
Expand Down

0 comments on commit adff4c3

Please sign in to comment.