├── .gitignore ├── .jshint ├── .npmignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── examples ├── account │ ├── config │ │ └── routes.json │ ├── lib │ │ └── api_modules │ │ │ ├── index.js │ │ │ └── users.js │ ├── package.json │ ├── public │ │ ├── css │ │ │ ├── bootstrap.min.css │ │ │ └── main.css │ │ ├── img │ │ │ ├── glyphicons-halflings-white.png │ │ │ └── glyphicons-halflings.png │ │ ├── index.html │ │ ├── js │ │ │ ├── async.min.js │ │ │ ├── handlebars-1.0.0.beta.6.js │ │ │ ├── main.js │ │ │ ├── mootools-more-1.4.0.1.js │ │ │ ├── mootools-yui-compressed.js │ │ │ └── utils.js │ │ └── partials │ │ │ └── user_row.html │ └── server.js └── instagram │ ├── config │ └── routes.json │ ├── lib │ └── api_modules │ │ ├── index.js │ │ └── photos.js │ ├── package.json │ ├── public │ ├── css │ │ ├── bootstrap.min.css │ │ └── main.css │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ └── glyphicons-halflings.png │ ├── index.html │ ├── js │ │ ├── async.min.js │ │ ├── handlebars-1.0.0.beta.6.js │ │ ├── main.js │ │ ├── mootools-more-1.4.0.1.js │ │ ├── mootools-yui-compressed.js │ │ └── utils.js │ └── partials │ │ └── photo.html │ └── server.js ├── index.js ├── lib ├── apiserver.js └── middleware │ ├── file-trasport.js │ ├── httpauth.js │ ├── index.js │ ├── multipart-parser.js │ └── payload-parser.js ├── package.json └── test ├── apiserver-test.js ├── fixtures ├── class-module.js ├── httpauth-module.js ├── multipart-module.js ├── object-module.js └── payload-module.js ├── middleware ├── httpauth-test.js ├── multipart-parser-test.js └── payload-parser-test.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | examples/instagram/public/uploads/ 2 | examples/instagram/tmp/ 3 | lib-cov/* -------------------------------------------------------------------------------- /.jshint: -------------------------------------------------------------------------------- 1 | jshint indent:2, boss:true, browser:true, curly:true, debug:true, devel:true, eqeqeq: true, es5:true, expr:true, nodejs:true, newcap:true, passfail:true, triling:true, undef:true, white:true 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | examples/ 3 | test/ 4 | lib-cov/ 5 | coverage.html 6 | README.md -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Kilian Ciuffolo, me@nailik.org 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REPORTER = spec 2 | 3 | test: 4 | @NODE_ENV=test ./node_modules/.bin/mocha test/*-test.js test/*/*-test.js $(OPT) --reporter $(REPORTER) 5 | 6 | test-bail: 7 | @rm -rf lib-cov 8 | $(MAKE) test OPT=--bail 9 | 10 | test-cov: 11 | @APISERVER_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html 12 | @open -g coverage.html 13 | 14 | .PHONY: test test-bail test-cov -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apiserver [![build status](https://secure.travis-ci.org/kilianc/node-apiserver.png?branch=master)](http://travis-ci.org/kilianc/node-apiserver) 2 | 3 | A ready to go, modular, multi transport, streaming friendly, JSON(P) API Server. 4 | 5 | ## Why use ApiServer and not [restify](https://github.com/mcavage/node-restify) or [express](https://github.com/visionmedia/express)? 6 | 7 | Strong competitors I guess. 8 | 9 | __Express__ targets web applications providing support for templates, views, and all the facilities the you probably need if you're writing a web app. __Restify__ let you "build "strict" API services" but it's too big and it concentrates on server to server API, that will not be consumed by your browser. 10 | 11 | __ApiServer__ is rad. It is a slim, fast, minimal API framework, built to provide you a flexible API consumable both in the browser and from other apps. It ships with JSON, JSONP [__(GET/POST)__](https://github.com/kilianc/node-json-transport) transports and a powerful [fast routing engine](https://github.com/kilianc/node-apiserver-router) OOTB. The source code is small, heavily tested and decoupled. Your API source will be well organized in context objects, allowing you to keep it in a meaningful maintainable way. 12 | 13 | ### Killer features 14 | 15 | * Streaming JSON(P) transport GET/POST browser friendly 16 | * Fast routing system with cached routes 17 | * Implicit route parameters [(rails like)](http://guides.rubyonrails.org/routing.html) 18 | * Fully configurable custom routing 19 | * API modules as Objects/Classes 20 | * Payload paused by default 21 | * Transports decoupled from the core 22 | * Router decoupled from the core 23 | * Compatible with [senchalabs/Connect](https://github.com/senchalabs/Connect) middleware 24 | 25 | ## Installation 26 | 27 | ⚡ npm install apiserver 28 | 29 | ```js 30 | var ApiServer = require('apiserver') 31 | ``` 32 | 33 | # Quick look 34 | 35 | The example below is intended to be a small sneak peek of the ApiServer API, modules and routes should be moved to separate files. 36 | 37 | ```js 38 | 39 | var ApiServer = require('apiserver') 40 | 41 | var apiServer = new ApiServer({ port: 8080 }) 42 | 43 | // middleware 44 | apiServer.use(/^\/admin\//, ApiServer.httpAuth({ 45 | realm: 'ApiServer Example', 46 | encode: true, 47 | credentials: ['admin:apiserver'] 48 | })) 49 | apiServer.use(ApiServer.payloadParser()) 50 | 51 | // modules 52 | apiServer.addModule('1', 'fooModule', { 53 | // only functions exposed 54 | options: { 55 | opt1: 'opt1', 56 | opt2: 'opt2', 57 | opt3: 'opt3' 58 | }, 59 | foo: { 60 | get: function (request, response) { 61 | response.serveJSON({ 62 | id: request.querystring.id, 63 | verbose: request.querystring.verbose, 64 | method: 'GET', 65 | options: this.options 66 | }) 67 | }, 68 | post: function (request, response) { 69 | request.resume() 70 | request.once('end', function () { 71 | response.serveJSON({ 72 | id: request.querystring.id, 73 | verbose: request.querystring.verbose, 74 | method: 'POST', 75 | payload: request.body // thanks to payloadParser 76 | }) 77 | }) 78 | } 79 | }, 80 | bar: function (request, response) { 81 | response.serveJSON({ foo: 'bar', pow: this._pow(5), method: '*/' + request.method }) 82 | }, 83 | // never exposed due to the initial underscore 84 | _pow: function (n) { 85 | return n * n 86 | } 87 | }) 88 | 89 | // custom routing 90 | apiServer.router.addRoutes([ 91 | ['/foo', '1/fooModule#foo'], 92 | ['/foo/:id/:verbose', '1/fooModule#foo'], 93 | ['/foo_verbose/:id', '1/fooModule#foo', { 'verbose': true }], 94 | ['/bar', '1/fooModule#bar', {}, true] // will keep default routing too 95 | ]) 96 | 97 | // events 98 | apiServer.on('requestStart', function (pathname, time) { 99 | console.info(' ☉ :: start :: %s', pathname) 100 | }).on('requestEnd', function (pathname, time) { 101 | console.info(' ☺ :: end :: %s in %dms', pathname, time) 102 | }).on('error', function (pathname, err) { 103 | console.info(' ☹ :: error :: %s (%s)', pathname, err.message) 104 | }).on('timeout', function (pathname) { 105 | console.info(' ☂ :: timedout :: %s', pathname) 106 | }) 107 | 108 | apiServer.listen() 109 | ``` 110 | 111 | Server will respond to 112 | 113 | GET, POST http://localhost:8080/foo 114 | GET, POST http://localhost:8080/foo/5/true 115 | GET, POST http://localhost:8080/foo_verbose/5 116 | * http://localhost:8080/bar 117 | * http://localhost:8080/1/foo_module/bar 118 | 119 | For full and detailed examples look at the [examples folder](https://github.com/kilianc/node-apiserver/tree/master/examples) 120 | 121 | ## Table Of Contents 122 | * [Methods](#class-method-constructor) 123 | * [#()](#class-method-constructor) 124 | * [#addModule(version, moduleName, apiModule)](#class-method-addmodule) 125 | * [#use([route], middleware)](#class-method-use) 126 | * [#listen([port], [callback])](#class-method-listen) 127 | * [#close([callback])](#class-method-close) 128 | * [Modules](#modules) 129 | * [Interface](#modules-interface) 130 | * [Examples](#modules-examples) 131 | * [Middleware](#middleware) 132 | * [Interface](#middleware-interface) 133 | * [Transports](#transports) 134 | * [Router](#router) 135 | * [Interface](#router-interface) 136 | * [Bundled Middleware](#bundled-middleware) 137 | * [JSONTransport](#jsontransport) 138 | * [httpAuth](#httpauth) 139 | * [mutipartParser](#mutipartparser) 140 | * [payloadParser](#payloadparser) 141 | * [How to contribute](#how-to-contribute) 142 | * [License](#license) 143 | 144 | 145 | # Class Methods 146 | 147 | ## Class Method: constructor 148 | 149 | All options will be also passed to the the default transport (JSONTransport) [constructor](https://github.com/kilianc/node-json-transport#syntax), then add here your transport configuration. 150 | 151 | ### Syntax: 152 | 153 | ```js 154 | new ApiServer([options]) 155 | ``` 156 | 157 | ### Available Options: 158 | 159 | * __port__ - (`Number|String`: defaults to 8080) the server binding port 160 | * __server__ - (`http(s).Server`: defaults http.Server) 161 | * __timeout__ - (`Number`: defaults to 15000) milliseconds to wait before arbitrary closing the response 162 | * __router__ - (`Object`: defaults to the standard [router](#router)) the routes manager conforms to the [router interface](#router-interface) 163 | * __standardHeaders__ - (`Object`: below the default) response headers defaults, can be overwritten by the [transport](#transports) 164 | 165 | ```js 166 | { 167 | 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', 168 | 'expires': 0, 169 | 'pragma': 'no-cache', 170 | 'x-server': 'ApiServer v' + ApiServer.version + ' raging on nodejs ' + process.version 171 | } 172 | ``` 173 | 174 | ### Example: 175 | 176 | ```js 177 | var https = require('https'), 178 | ApiServer = require('apiserver') 179 | 180 | apiserver = new ApiServer({ 181 | port: 80, 182 | server: https.createServer(), 183 | standardHeaders: { 184 | 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', 185 | 'x-awesome-field': 'awezing value' 186 | }, 187 | timeout: 2000, 188 | indent: ' ', // transport 189 | domain: '.myservice.com', // transport 190 | defaultRoute: '/:version/:module/:method' // router 191 | }) 192 | ``` 193 | 194 | ## Class Method: addModule 195 | 196 | Adds a new [module](#modules) to to the current API set. It triggers the `router.update` method. 197 | 198 | ### Syntax: 199 | 200 | ```js 201 | ApiServer.prototype.addModule(apiVersion, moduleName, apiModule) 202 | ``` 203 | 204 | ### Arguments: 205 | 206 | * __apiVersion__ - (`String`) the version of the API you want to add your module to, it will be the part of the url 207 | * __moduleName__ - (`String`) the name of the module, this will be the second part of your derived routes, after a [case conversion](#modules) 208 | * __apiModule__ - (`Object`) the module object conform to the [modules interface](#module-interface) 209 | 210 | ### Examples: 211 | 212 | ```js 213 | var apiserver = new ApiServer() 214 | apiserver.addModule('v1', 'user', userModule) 215 | apiserver.addModule('v1', 'pages', pageModule) 216 | apiserver.addModule('v2', 'user', userModule2) 217 | ``` 218 | 219 | ## Class Method: use 220 | 221 | Adds a middleware object to the [middleware chain](#middleware-chain). It triggers the `router.update` method. 222 | 223 | Each middleware is associated to a `RegExp` used to test the API end-point route. If the route matches the `RegExp` the middleware will be a part of the chain and will be executed. 224 | 225 | Read more about middleware [here](#middleware). 226 | 227 | ### Syntax: 228 | 229 | ```js 230 | ApiServer.prototype.use([route], middleware) 231 | ``` 232 | 233 | ### Arguments: 234 | 235 | * __route__ - (`RegExp`: defaults to `/./`) regular expression that the route should match 236 | * __middleware__ - (`Object`) the middleware object conforms to the [middleware interface](#middleware-interface) 237 | 238 | ### Examples: 239 | 240 | ```js 241 | var apiserver = new ApiServer() 242 | apiserver.use(new MyMiddleWare({ foo: 'bar', bar: true })) 243 | apiserver.use(/(signin|signup)/, ApiServer.payloadParser()) 244 | apiserver.use(/^\/v1\/files\/upload$/, ApiServer.multipartParser()) 245 | ``` 246 | 247 | ## Class Method: listen 248 | Bind the server to a port 249 | 250 | ### Syntax: 251 | 252 | ```js 253 | ApiServer.prototype.listen([port], [callback]) 254 | ``` 255 | 256 | ### Arguments: 257 | 258 | * __port__ - (`Number|String`) overwrite the constructor __port__ parameter 259 | * __callback__ - (`Function`) called when the port is actually bound to the server 260 | 261 | ### Example: 262 | _From this point on, all the examples will take the require statements as assumption_ 263 | 264 | ```js 265 | var apiserver = new ApiServer() 266 | apiserver.listen(80, function (err) { 267 | if (err) { 268 | console.error('Something terrible happened: %s', err.message) 269 | } else { 270 | console.log('Successful bound to port %s', this.port) 271 | } 272 | }) 273 | ``` 274 | 275 | ## Class Method: close 276 | Unbind the server from the current port 277 | 278 | ### Syntax: 279 | 280 | ```js 281 | ApiServer.prototype.close([callback]) 282 | ``` 283 | 284 | ### Arguments: 285 | 286 | * __callback__ - (`Function`) called when the port is actually unbound from the server 287 | 288 | ### Example: 289 | 290 | ```js 291 | var apiserver = new ApiServer() 292 | apiserver.listen(80, onListen) 293 | 294 | function onListen(err) { 295 | if (err) { 296 | console.error('Something terrible happened: %s', err.message) 297 | } else { 298 | setTimeout(function () { 299 | apiserver.close(onClose) 300 | }, 5000) 301 | } 302 | } 303 | 304 | function onClose() { 305 | console.log('port unbound correctly') 306 | } 307 | ``` 308 | 309 | # Class Events 310 | 311 | ## Class Event: requestStart 312 | 313 | Emitted when an API endpoint got hit. 314 | 315 | ### Event data 316 | 317 | ```js 318 | apiserver.on('requestStart', function (url, requestTime) { 319 | 320 | }) 321 | ``` 322 | 323 | * __url__ (`String`) - the `request.url` 324 | * __requestTime__ (`Number`) - when the API method was requested 325 | 326 | ## Class Event: requestEnd 327 | 328 | Emitted when an API method closes the response, even with `response.end`. 329 | 330 | ### Event data 331 | 332 | ```js 333 | apiserver.on('requestEnd', function (url, responseTime) { 334 | 335 | }) 336 | ``` 337 | 338 | * __url__ (`String`) - the `request.url` 339 | * __responseTime__ (`Number`) - how log the API method took for closing the response 340 | 341 | ## Class Event: timeout 342 | 343 | Emitted when an API method exceed the maximum allowed time ([see `timeout` option](#class-method-constructor)), before closing the response. 344 | 345 | ### Event data 346 | 347 | ```js 348 | apiserver.on('timeout', function (url) { 349 | 350 | }) 351 | ``` 352 | 353 | * __url__ (`String`) - the `request.url` 354 | 355 | ## Class Event: error 356 | 357 | Emitted when a __sync__ error is triggered during the [middleware chain](#middleware-chain) execution, can be both your API, a transport or a simple middleware. 358 | 359 | _You still have to deal with async errors_ 360 | 361 | ### Event data 362 | 363 | ```js 364 | apiserver.on('error', function (url, err) { 365 | 366 | }) 367 | ``` 368 | 369 | * __url__ (`String`) - the `request.url` 370 | * __err__ (`Error`) - the error which triggered the event 371 | 372 | # Modules 373 | 374 | A module is a set of API __end-points__ grouped in the same __context__: 375 | 376 | * __context__: a simple object 377 | * __end-point__: function/method accessible by the object and scoped within the object 378 | 379 | ## Modules Interface 380 | 381 | Each module method (API end-point) must implement this interface and expect request and response parameters 382 | 383 | ```js 384 | function (request, response) 385 | ``` 386 | 387 | The request object is ["extendend"](https://github.com/kilianc/node-apiserver/blob/master/lib/apiserver.js#L99) ootb with the following members _(aliases in round brackets)_: 388 | 389 | * __requestedAt (at)__: timestamp of the request 390 | * __parsedUrl__: a parsed version of the request url with `url.parse` 391 | * __pathname (path)__: the pathname that corresponds to the end-point route 392 | * __querystring (qs)__: the querystring object parsed with [visionmedia/node-querystring](https://github.com/visionmedia/node-querystring) 393 | 394 | As you can see, there is no callback to call, you have to deal directly with the response. 395 | 396 | Take a look at your [transport documentation](#transports) and use the right method that ships within the response object. You can also roughly close and write to the response __stream__ in an edge case. 397 | 398 | ## Modules Examples 399 | 400 | ### Object literal 401 | 402 | ```js 403 | var apiserver = new ApiServer() 404 | ``` 405 | 406 | ```js 407 | var userModule = { 408 | signin: function (request, response) { 409 | // rough approach 410 | response.writeHead(200) 411 | response.end('ok') 412 | }, 413 | signout: function (request, response) { 414 | // JSON transport 415 | response.serveJSON({ foo: 'bar' }) 416 | } 417 | } 418 | ``` 419 | 420 | ```js 421 | apiserver.addModule('v1', 'user', userModule) 422 | ``` 423 | 424 | ### Class 425 | 426 | ```js 427 | var apiserver = new ApiServer() 428 | ``` 429 | 430 | ```js 431 | function UserModule(options) { 432 | this.database = options.database 433 | this.serviceName = options.serviceName 434 | } 435 | 436 | UserModule.prototype.signin = function (request, response) { 437 | var self = this 438 | self.database.searchUser(request.querystring.username, function (err) { 439 | if (err) { 440 | response.serveJSON({ success: false, err: err.message }) 441 | } else { 442 | response.serveJSON({ success: true, message: 'welcome to ' + self.serviceName }) 443 | } 444 | }) 445 | } 446 | 447 | UserModule.prototype.signout = function (request, response) { 448 | // you can use the response as usual 449 | // a redirect for example 450 | response.writeHead(302, { 451 | 'location': 'http://example.org/logout_suceesful' 452 | }) 453 | response.end() 454 | } 455 | ``` 456 | 457 | ```js 458 | var database = /* your db object*/ 459 | apiserver.addModule('v1', 'user', new UserModule(database, 'My Awesome Service')) 460 | ``` 461 | 462 | # Middleware 463 | 464 | The concept of middleware is not new at all, you can find the same pattern in [senchalabs/Connect](https://github.com/senchalabs/Connect) in [mcavage/node-restify](https://github.com/mcavage/node-restify) and in many others. A [middleware](http://en.wikipedia.org/wiki/Middleware) is a piece of software that adds (or patches) a feature into another software. Usually there is a common interface to implement, because the caller software, in this case our __ApiServer__, should know how to interact with the middleware. 465 | 466 | _You should check out the [source code](https://github.com/kilianc/node-apiserver/tree/master/lib/middleware) for a large understanding, middleware is relatively easy to code._ 467 | 468 | ## Middleware Chain 469 | 470 | The __ApiServer__ uses [kilianc/node-fnchain](https://github.com/kilianc/node-fnchain) to [execute all the active middleware](https://github.com/kilianc/node-apiserver/blob/master/lib/apiserver.js#L132) and reach the API method (that actually is the last ring of the chain). This means that the order of the execution depends on the order you activated the middleware. 471 | 472 | Each middleware can both exit with an error or explicitly stop the chain (not reaching your API method). This is useful in case of a precondition check (auth, sessions, DoS attack filter...), or just because you packed some shared code as middleware which must be executed before your API method. 473 | 474 | At the middleware execution level, the response object is already patched with the default transport methods, so you can use these methods to write and close the response. Is a good practice to leave at the top of the chain the extra transports middleware. 475 | 476 | ```js 477 | // constructor adds the default transport automatically 478 | var apiserver = new ApiServer() 479 | 480 | // let's ad first our custom transports 481 | apiserver.use(/\.xml$/, myXMLTransport()) 482 | apiserver.use(/\.csv$/, myCSVTransport()) 483 | apiserver.use(/\.yml$/, myYAMLTransport()) 484 | 485 | // now activate our middleware 486 | apiserver.use(/form/, ApiServer.payloadParser()) 487 | apiserver.use(/upload/, ApiServer.multipartParser()) 488 | 489 | ... 490 | ``` 491 | 492 | __The request payload (the `data` event) is paused by default and can be resumed calling `request.resume()` at any level of execution: middleware, module, transport.__ Why? Because you should explicitly accept or refuse a payload, this way you will save memory not buffering useless data. 493 | 494 | Take a look at both the [pause](http://nodejs.org/api/all.html#all_request_pause) and [resume](http://nodejs.org/api/all.html#all_request_resume) official docs. 495 | 496 | _ApiServer is using [this patch](https://github.com/kilianc/node-buffered-request) to provide a robust buffered pause resume method, so you don't have do deal with the flying chunks after the pause call_ 497 | 498 | ## Middleware Interface 499 | 500 | Each middleware must implement this interface. 501 | 502 | ```js 503 | module.exports = function (options) { 504 | return function (request, response, next) { 505 | // do sometihng async and when you're done call the callback 506 | options.count++ 507 | next() 508 | } 509 | } 510 | ``` 511 | 512 | A middleware basically, is a function that returns another function, this one must declare 3 paramaters: 513 | 514 | * __request__: the server request already extended by the server 515 | * __response__: the server response already extended by the transports 516 | * __next__: a callback in the following form `function (err, stop)` 517 | 518 | The `next` callback expects 2 parameters: 519 | 520 | * __err__ - (`Error`) an error object that will throw a server error event and will close the response 521 | * __stop__ - (`Boolean`) a flag that stops the internal chain, that means that your API method will never be called and your middleware should be able to correctly close the response. At this point you already have all the transports available, and you can freely use them. 522 | 523 | ## Transports 524 | 525 | A transport is a particular middleware that "extends" the response object. It can provide new methods that allow you to serve your data to the client in different ways. 526 | 527 | Usually this is how you send data back to the client: 528 | 529 | ```js 530 | function (request, response) { 531 | response.writeHead(200, { 532 | 'content-type': 'application/json' 533 | }) 534 | response.end(JSON.stringify({ foo: 'bar' })) 535 | }) 536 | ``` 537 | 538 | This is for example how the default [JSONTransport](https://github.com/kilianc/node-json-transport) simplify the process 539 | 540 | ```js 541 | function (request, response) { 542 | response.serveJSON({ foo: 'bar' }) 543 | }) 544 | ``` 545 | 546 | Basically what a transport does, is to wrap your data around a meaningful format (JSON, JSONP, HTML, XML, CSV, ...) understandable by your clients. It takes care of all the small things that the raw response needs (headers, status codes, buffering, ...) 547 | 548 | Transports must be at the top of the middleware chain, in order to allow other middleware to use them. 549 | 550 | [JSONTransport](https://github.com/kilianc/node-json-transport) is the default one, is attached before the middleware chain execution and then is available at every level of execution. You don't need to allocate it directly, the server itself will allocate the transport passing as options the __ApiServer__ [constructor](#class-method-constructor) options object. 551 | 552 | ### Example 553 | 554 | ```js 555 | module.exports = function (options) { 556 | function serve(request, response, data, options) { 557 | response.writeHead(200, { 558 | 'content-type': 'application/' 559 | }) 560 | response.end(.stringify(data)) 561 | } 562 | return function (request, response) { 563 | // attach some new method to the response 564 | response.serve = serve.bind(this, request, response) 565 | } 566 | } 567 | ``` 568 | 569 | where `` is the formatting method of your data. 570 | 571 | # Router 572 | 573 | Apiserver uses [apiserver-router](https://github.com/kilianc/node-apiserver-router) as default router, a fast routing system with integrated caching. It basically translates your API methods names in routes, doing some convenient case conversion. Also, it supports [(rails like)](http://guides.rubyonrails.org/routing.html) custom routes and implicit route parameters. 574 | 575 | You can change the default behavior passing a custom router as `router` option in the ApiServer [constructor](#class-method-constructor). 576 | 577 | ## Example 578 | 579 | ```js 580 | function UserModule(options) { 581 | this.options = options 582 | } 583 | 584 | // will be translated into /1/random_photo_module/create_album 585 | UserModule.prototype.createAlbum = function (request, response) { ... } 586 | 587 | // will be translated into /1/random_photo_module/upload_photo 588 | // in this case we will overwrite the default path with a custom one 589 | UserModule.prototype.uploadPhoto = { 590 | post: function (request, response) { ... } 591 | } 592 | 593 | // private method, skipped by the router 594 | UserModule.prototype._checkFileExtension = function (request, response) { ... } 595 | 596 | ``` 597 | 598 | ```js 599 | apiserver.addModule('1', 'randomPhotoModule', new UserModule()) 600 | apiserver.router.addRoute('/photo/:caption', '1/randomPhotoModule#uploadPhoto') 601 | ``` 602 | 603 | N.B. the `moduleName` also will be translated 604 | 605 | ## Router Interface 606 | 607 | Your custom router must implement the following interface. 608 | 609 | ```js 610 | function Router () { 611 | ... 612 | } 613 | 614 | Router.prototype.update = function (modules, middlewareList) { 615 | ... 616 | } 617 | 618 | Router.prototype.get = function (request) { 619 | ... 620 | } 621 | ``` 622 | 623 | The `get` method must return the the [middleware chain](#middleware-chain) associated with the `request` parameter, and eventually extend the `request` with new data (ex. implicit route parameters). 624 | 625 | # Bundled Middleware 626 | 627 | ## JSONTransport 628 | 629 | [JSONTransport](https://github.com/kilianc/node-json-transport) is the default transport bundled with ApiServer and we can call it the real __killer feature__. 630 | 631 | It provides JSON and JSONP that work with both GET / POST methods. 632 | 633 | ### Examples 634 | 635 | ```js 636 | // decontextualized API method 637 | function (request, response) { 638 | response.serveJSON({ foo: 'bar' }) 639 | }) 640 | ``` 641 | 642 | ```js 643 | // decontextualized API method 644 | function (request, response) { 645 | response.serveJSON(['foo','bar', ...], { 646 | httpStatusCode: 404, 647 | httpStatusMessage: 'maybe.. you\'re lost', 648 | headers: { 649 | 'x-value': 'foo' 650 | } 651 | }) 652 | }) 653 | ``` 654 | ```js 655 | // decontextualized API method 656 | function (request, response) { 657 | var count = 3 658 | var interval = setInterval(function () { 659 | if (count === 0) { 660 | clearInterval(interval) 661 | response.streamJSON() 662 | } else { 663 | count-- 664 | response.streamJSON({ foo: 'bar' }) 665 | } 666 | }, 200) 667 | }) 668 | ``` 669 | 670 | yields 671 | 672 | ```js 673 | [ 674 | { "foo": "bar" }, 675 | { "foo": "bar" }, 676 | { "foo": "bar" } 677 | ] 678 | ``` 679 | 680 | Read the full docs [here](https://github.com/kilianc/node-json-transport) 681 | 682 | ## payloadParser 683 | 684 | The payload parser automatically __buffers__ the payload and parse it. It only works with __PUT POST OPTIONS__ http methods, because they are the only that can carryout a payload by specs definition. 685 | 686 | Two kinds of payload can be parsed: 687 | 688 | * `application/x-www-form-urlencoded` 689 | * `application/json` 690 | 691 | The following attributes will be attached to the request object: 692 | 693 | * __body__: an object containing the parsed data 694 | * __rawBody__: the raw payload as binary [buffer](http://nodejs.org/api/all.html#all_buffer) 695 | * __parseError__: can be `null` or `Error` in case of parse error 696 | 697 | ### Syntax 698 | 699 | ```js 700 | ApiServer.payloadParser() 701 | ``` 702 | 703 | ### Example 704 | 705 | ```js 706 | var apiserver = new ApiServer() 707 | apiserver.use(/1\/my_module\/my_method_api$/, ApiServer.payloadParser()) 708 | apiserver.addModule('1', 'myModule', { 709 | 'my_method_api': function (request, response) { 710 | request.resume() 711 | request.once('end', function () { 712 | if (request.parseError) { 713 | // :( 714 | console.error(request.parseError.message) 715 | } else { 716 | request.body // an object containing the parsed data 717 | request.rawBody // contains a binary buffer with your payload 718 | } 719 | }) 720 | } 721 | }) 722 | ``` 723 | 724 | ## multipartParser 725 | 726 | The multipart-parser the attach the payload to a [felixge/node-formidable](https://github.com/felixge/node-formidable) `IncomingForm` object. It only works with __PUT POST OPTIONS__ http methods, because they are the only that can carryout a payload by specs definition. 727 | 728 | Only a `multipart/form-data` payload is parsed and the following attribute will be attached to the request object: 729 | 730 | * __form__ an IncomingForm object, [read how to deal with it](https://github.com/felixge/node-formidable#formidablefile) 731 | 732 | The following attributes will be attached to the request object, *after the IncomingForm end event*: 733 | 734 | * __body__: an object containing the parsed data 735 | * __files__: array of uploaded files [instaceof formidable.File](https://github.com/felixge/node-formidable#formidablefile) 736 | * __parseError__: can be `null` or `Error` in case of parse error 737 | 738 | ### Syntax 739 | 740 | ```js 741 | ApiServer.multipartParser() 742 | ``` 743 | 744 | ### Example 745 | 746 | ```js 747 | var apiserver = new ApiServer() 748 | apiserver.use(/1\/my_module\/my_method_api$/, ApiServer.multipartParser()) 749 | apiserver.addModule('1', 'myModule', { 750 | 'my_method_api': function (request, response) { 751 | var fields = Object.create(null) 752 | request.resume() 753 | request.form.on('field', function (name, value) { 754 | fields[name] = value 755 | }) 756 | request.form.on('file', function (name, file) { 757 | fields[name] = fs.readFileSync(file.path, 'utf8') 758 | }) 759 | request.form.once('end', function () { 760 | // do something with your data 761 | }) 762 | }, 763 | 'my_smarter_api': function (request, response) { 764 | request.resume() 765 | request.form.once('end', function () { 766 | // do something with your data 767 | request.body // fields 768 | request.files // files 769 | request.parseError // error 770 | }) 771 | } 772 | }) 773 | ``` 774 | 775 | ## httpAuth 776 | 777 | The httpauth middleware acts as an auth precondition, checking the `authorization` headers sent with the request. 778 | 779 | If the request doesn't pass the authorization check, httpAuth [will close the response](https://github.com/kilianc/node-apiserver/blob/refactor/lib/middleware/httpauth.js#L34) using the standard JSONTransport: 780 | 781 | ```js 782 | response.serveJSON(null, { 783 | httpStatusCode: 401, 784 | headers: { 'www-authenticate': 'Basic realm=\'' + realm + '\'' } 785 | }) 786 | ``` 787 | 788 | This will trigger a user/password prompt in your browser 789 | 790 | ### Syntax 791 | 792 | ```js 793 | ApiServer.httpAuth([options]) 794 | ``` 795 | 796 | ### Options 797 | * __realm__: (`String`) the name of your service, this is used by the browser when it prompts for username and password 798 | * __credentials__ - (`Array`) a list of strings (credentials), if your client is a browser you must use the form _username:password_ 799 | * __encode__: (`Boolean`: defaults to false) set to true if your client is a browser (will base64 encode) 800 | 801 | ### Example 802 | 803 | ```js 804 | var apiserver = new ApiServer() 805 | apiserver.use(/1\/admin\/.+/, ApiServer.httpAuth({ 806 | realm: 'signin please', 807 | credentials: ['foo:password','bar:password', ...], 808 | encode: true // we suppose that at the other end of the wire we have a browser 809 | })) 810 | apiserver.addModule('1', 'admin', { 811 | 'protectedApi': function (request, response) { 812 | // this will executed only if you provide valid credentials 813 | } 814 | }) 815 | ``` 816 | 817 | # How to contribute 818 | 819 | __ApiServer__ follows the awesome [Vincent Driessen](http://nvie.com/about/) [branching model](http://nvie.com/posts/a-successful-git-branching-model/). 820 | 821 | * You must add a new feature on his own topic branch 822 | * You must contribute to hot-fixing directly into the master branch (and pull-request to it) 823 | 824 | ApiServer follows (more or less) the [Felix's Node.js Style Guide](http://nodeguide.com/style.html), your contribution must be consistent with this style. 825 | 826 | The test suite is written on top of [visionmedia/mocha](http://visionmedia.github.com/mocha/) and it took hours of hard work. Please use the tests to check if your contribution is breaking some part of the library and add new tests for each new feature. 827 | 828 | ⚡ npm test 829 | 830 | and for your test coverage 831 | 832 | ⚡ make test-cov 833 | 834 | ## License 835 | 836 | _This software is released under the MIT license cited below_. 837 | 838 | Copyright (c) 2010 Kilian Ciuffolo, me@nailik.org. All Rights Reserved. 839 | 840 | Permission is hereby granted, free of charge, to any person 841 | obtaining a copy of this software and associated documentation 842 | files (the 'Software'), to deal in the Software without 843 | restriction, including without limitation the rights to use, 844 | copy, modify, merge, publish, distribute, sublicense, and/or sell 845 | copies of the Software, and to permit persons to whom the 846 | Software is furnished to do so, subject to the following 847 | conditions: 848 | 849 | The above copyright notice and this permission notice shall be 850 | included in all copies or substantial portions of the Software. 851 | 852 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 853 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 854 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 855 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 856 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 857 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 858 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 859 | OTHER DEALINGS IN THE SOFTWARE. 860 | -------------------------------------------------------------------------------- /examples/account/config/routes.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["/signin", "1/users#signin"], 3 | ["/signup", "1/users#signup"], 4 | ["/list/:limit/:page", "1/users#list", { "limit": 20, "page": 1 }], 5 | ["/users/:id", "1/users#get"] 6 | ] -------------------------------------------------------------------------------- /examples/account/lib/api_modules/index.js: -------------------------------------------------------------------------------- 1 | exports.Users = require("./users") -------------------------------------------------------------------------------- /examples/account/lib/api_modules/users.js: -------------------------------------------------------------------------------- 1 | var ObjectId = require('mongodb').ObjectID 2 | 3 | var Users = module.exports = function (options) { 4 | var self = this 5 | options = (options !== null && options !== undefined && options.constructor === Object) ? options : {} 6 | Object.keys(options).forEach(function (key) { 7 | if (!self.__proto__.hasOwnProperty(key)) { 8 | self[key] = options[key] 9 | } 10 | }) 11 | } 12 | 13 | Users.prototype.signup = { 14 | post: function (request, response) { 15 | var self = this 16 | request.resume() 17 | request.once('end', function () { 18 | if (!self._isEmail(request.body.email)) { 19 | response.serveJSON({ success: false, error: 'invalid email ' + request.body.email }) 20 | return 21 | } 22 | if (!self._isPassword(request.body.password)) { 23 | response.serveJSON({ success: false, error: 'invalid password' }) 24 | return 25 | } 26 | self.collections.users.save({ 27 | email: request.body.email, 28 | password: request.body.password 29 | }, function (err, document) { 30 | response.serveJSON({ success: true, user: document }) 31 | }) 32 | }) 33 | } 34 | } 35 | 36 | Users.prototype.signin = { 37 | post: function (request, response) { 38 | var self = this 39 | request.resume() 40 | request.once('end', function () { 41 | self.collections.users.findOne({ email: request.body.email, password: request.body.password }, function (err, document) { 42 | if (err) { 43 | response.serveJSON({ success: false, error: err.message }, { httpStatusCode: 404 }) 44 | return 45 | } 46 | if (!document) { 47 | response.serveJSON({ success: false, error: 'user not found' }, { httpStatusCode: 404 }) 48 | return 49 | } 50 | response.serveJSON({ success: true }, { 51 | headers: { 52 | 'set-cookie': 'session_id=' + document._id.toHexString() 53 | } 54 | }) 55 | }) 56 | }) 57 | } 58 | } 59 | 60 | Users.prototype.list = { 61 | get: function (request, response) { 62 | var limit = Number(request.querystring.limit) 63 | var page = Number(request.querystring.page) 64 | var stream = this.collections.users.find({}, { limit: limit, skip: (page-1) * limit }).stream() 65 | stream.on('data', function(item) { 66 | response.streamJSON(item) 67 | }) 68 | stream.on('close', function() { 69 | response.streamJSON() 70 | }) 71 | } 72 | } 73 | 74 | Users.prototype.get = { 75 | get: function (request, response) { 76 | this.collections.users.findOne({ _id: new ObjectId(request.querystring.id) }, function (err, document) { 77 | if (err || !document) { 78 | response.serveJSON({ success: false, error: err.message }) 79 | return 80 | } 81 | response.serveJSON({ success: true, user: document }) 82 | }) 83 | } 84 | } 85 | 86 | Users.prototype.delete = { 87 | post: function (request, response) { 88 | var self = this 89 | request.resume() 90 | request.once('end', function () { 91 | self.collections.users.remove({ _id: new ObjectId(request.body.id) }, { safe: true }, function (err, document) { 92 | if (err) { 93 | response.serveJSON({ success: false, error: err.message }) 94 | return 95 | } 96 | response.serveJSON({ success: true, id: request.body.id }) 97 | }) 98 | }) 99 | } 100 | } 101 | Users.prototype.delete.delete = Users.prototype.delete.post 102 | 103 | Users.prototype._isEmail = function (email) { 104 | return /^([a-z0-9_\.\-])+\@(([a-z0-9\-])+\.)+([a-z0-9]{2,4})+$/.test(email) 105 | } 106 | 107 | Users.prototype._isPassword = function (email) { 108 | return /^.{6,22}$/.test(email) 109 | } -------------------------------------------------------------------------------- /examples/account/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apiserver-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "console-trace": "https://github.com/LearnBoost/console-trace/tarball/master", 7 | "apiserver": "0.2.x", 8 | "mongodb": "1.0.x", 9 | "request": "2.9.x", 10 | "connect": "2.3.x", 11 | "colors": "" 12 | } 13 | } -------------------------------------------------------------------------------- /examples/account/public/css/main.css: -------------------------------------------------------------------------------- 1 | iframe { display: none } 2 | section { padding-top: 5px } 3 | .btn-success { margin-right: 10px } 4 | .btn-info { margin-right: 10px } -------------------------------------------------------------------------------- /examples/account/public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilianc/node-apiserver/82ef85cb86075e7f6f125d4fd539f217334ae0b6/examples/account/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /examples/account/public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilianc/node-apiserver/82ef85cb86075e7f6f125d4fd539f217334ae0b6/examples/account/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /examples/account/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ApiServer Examples / account 14 | 15 | 16 | 17 |
18 | 21 |
22 |
23 |
24 | Signup 25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
idemailpassword
55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/account/public/js/async.min.js: -------------------------------------------------------------------------------- 1 | /*global setTimeout: false, console: false */(function(){var a={},b=this,c=b.async;typeof module!="undefined"&&module.exports?module.exports=a:b.async=a,a.noConflict=function(){return b.async=c,a};var d=function(a,b){if(a.forEach)return a.forEach(b);for(var c=0;cd?1:0};d(null,e(b.sort(c),function(a){return a.value}))})},a.auto=function(a,b){b=b||function(){};var c=g(a);if(!c.length)return b(null);var e={},h=[],i=function(a){h.unshift(a)},j=function(a){for(var b=0;b 0) { 43 | for(var i=0, j=context.length; i 0) { 60 | for(var i=0, j=context.length; i 2) { 235 | expected.push("'" + this.terminals_[p] + "'"); 236 | } 237 | var errStr = ""; 238 | if (this.lexer.showPosition) { 239 | errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + this.terminals_[symbol] + "'"; 240 | } else { 241 | errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1?"end of input":"'" + (this.terminals_[symbol] || symbol) + "'"); 242 | } 243 | this.parseError(errStr, {text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected}); 244 | } 245 | } 246 | if (action[0] instanceof Array && action.length > 1) { 247 | throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); 248 | } 249 | switch (action[0]) { 250 | case 1: 251 | stack.push(symbol); 252 | vstack.push(this.lexer.yytext); 253 | lstack.push(this.lexer.yylloc); 254 | stack.push(action[1]); 255 | symbol = null; 256 | if (!preErrorSymbol) { 257 | yyleng = this.lexer.yyleng; 258 | yytext = this.lexer.yytext; 259 | yylineno = this.lexer.yylineno; 260 | yyloc = this.lexer.yylloc; 261 | if (recovering > 0) 262 | recovering--; 263 | } else { 264 | symbol = preErrorSymbol; 265 | preErrorSymbol = null; 266 | } 267 | break; 268 | case 2: 269 | len = this.productions_[action[1]][1]; 270 | yyval.$ = vstack[vstack.length - len]; 271 | yyval._$ = {first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column}; 272 | r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); 273 | if (typeof r !== "undefined") { 274 | return r; 275 | } 276 | if (len) { 277 | stack = stack.slice(0, -1 * len * 2); 278 | vstack = vstack.slice(0, -1 * len); 279 | lstack = lstack.slice(0, -1 * len); 280 | } 281 | stack.push(this.productions_[action[1]][0]); 282 | vstack.push(yyval.$); 283 | lstack.push(yyval._$); 284 | newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; 285 | stack.push(newState); 286 | break; 287 | case 3: 288 | return true; 289 | } 290 | } 291 | return true; 292 | } 293 | };/* Jison generated lexer */ 294 | var lexer = (function(){ 295 | 296 | var lexer = ({EOF:1, 297 | parseError:function parseError(str, hash) { 298 | if (this.yy.parseError) { 299 | this.yy.parseError(str, hash); 300 | } else { 301 | throw new Error(str); 302 | } 303 | }, 304 | setInput:function (input) { 305 | this._input = input; 306 | this._more = this._less = this.done = false; 307 | this.yylineno = this.yyleng = 0; 308 | this.yytext = this.matched = this.match = ''; 309 | this.conditionStack = ['INITIAL']; 310 | this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0}; 311 | return this; 312 | }, 313 | input:function () { 314 | var ch = this._input[0]; 315 | this.yytext+=ch; 316 | this.yyleng++; 317 | this.match+=ch; 318 | this.matched+=ch; 319 | var lines = ch.match(/\n/); 320 | if (lines) this.yylineno++; 321 | this._input = this._input.slice(1); 322 | return ch; 323 | }, 324 | unput:function (ch) { 325 | this._input = ch + this._input; 326 | return this; 327 | }, 328 | more:function () { 329 | this._more = true; 330 | return this; 331 | }, 332 | pastInput:function () { 333 | var past = this.matched.substr(0, this.matched.length - this.match.length); 334 | return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, ""); 335 | }, 336 | upcomingInput:function () { 337 | var next = this.match; 338 | if (next.length < 20) { 339 | next += this._input.substr(0, 20-next.length); 340 | } 341 | return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, ""); 342 | }, 343 | showPosition:function () { 344 | var pre = this.pastInput(); 345 | var c = new Array(pre.length + 1).join("-"); 346 | return pre + this.upcomingInput() + "\n" + c+"^"; 347 | }, 348 | next:function () { 349 | if (this.done) { 350 | return this.EOF; 351 | } 352 | if (!this._input) this.done = true; 353 | 354 | var token, 355 | match, 356 | col, 357 | lines; 358 | if (!this._more) { 359 | this.yytext = ''; 360 | this.match = ''; 361 | } 362 | var rules = this._currentRules(); 363 | for (var i=0;i < rules.length; i++) { 364 | match = this._input.match(this.rules[rules[i]]); 365 | if (match) { 366 | lines = match[0].match(/\n.*/g); 367 | if (lines) this.yylineno += lines.length; 368 | this.yylloc = {first_line: this.yylloc.last_line, 369 | last_line: this.yylineno+1, 370 | first_column: this.yylloc.last_column, 371 | last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length} 372 | this.yytext += match[0]; 373 | this.match += match[0]; 374 | this.matches = match; 375 | this.yyleng = this.yytext.length; 376 | this._more = false; 377 | this._input = this._input.slice(match[0].length); 378 | this.matched += match[0]; 379 | token = this.performAction.call(this, this.yy, this, rules[i],this.conditionStack[this.conditionStack.length-1]); 380 | if (token) return token; 381 | else return; 382 | } 383 | } 384 | if (this._input === "") { 385 | return this.EOF; 386 | } else { 387 | this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(), 388 | {text: "", token: null, line: this.yylineno}); 389 | } 390 | }, 391 | lex:function lex() { 392 | var r = this.next(); 393 | if (typeof r !== 'undefined') { 394 | return r; 395 | } else { 396 | return this.lex(); 397 | } 398 | }, 399 | begin:function begin(condition) { 400 | this.conditionStack.push(condition); 401 | }, 402 | popState:function popState() { 403 | return this.conditionStack.pop(); 404 | }, 405 | _currentRules:function _currentRules() { 406 | return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules; 407 | }, 408 | topState:function () { 409 | return this.conditionStack[this.conditionStack.length-2]; 410 | }, 411 | pushState:function begin(condition) { 412 | this.begin(condition); 413 | }}); 414 | lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) { 415 | 416 | var YYSTATE=YY_START 417 | switch($avoiding_name_collisions) { 418 | case 0: 419 | if(yy_.yytext.slice(-1) !== "\\") this.begin("mu"); 420 | if(yy_.yytext.slice(-1) === "\\") yy_.yytext = yy_.yytext.substr(0,yy_.yyleng-1), this.begin("emu"); 421 | if(yy_.yytext) return 14; 422 | 423 | break; 424 | case 1: return 14; 425 | break; 426 | case 2: this.popState(); return 14; 427 | break; 428 | case 3: return 24; 429 | break; 430 | case 4: return 16; 431 | break; 432 | case 5: return 20; 433 | break; 434 | case 6: return 19; 435 | break; 436 | case 7: return 19; 437 | break; 438 | case 8: return 23; 439 | break; 440 | case 9: return 23; 441 | break; 442 | case 10: yy_.yytext = yy_.yytext.substr(3,yy_.yyleng-5); this.popState(); return 15; 443 | break; 444 | case 11: return 22; 445 | break; 446 | case 12: return 34; 447 | break; 448 | case 13: return 33; 449 | break; 450 | case 14: return 33; 451 | break; 452 | case 15: return 36; 453 | break; 454 | case 16: /*ignore whitespace*/ 455 | break; 456 | case 17: this.popState(); return 18; 457 | break; 458 | case 18: this.popState(); return 18; 459 | break; 460 | case 19: yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2).replace(/\\"/g,'"'); return 28; 461 | break; 462 | case 20: return 30; 463 | break; 464 | case 21: return 30; 465 | break; 466 | case 22: return 29; 467 | break; 468 | case 23: return 33; 469 | break; 470 | case 24: yy_.yytext = yy_.yytext.substr(1, yy_.yyleng-2); return 33; 471 | break; 472 | case 25: return 'INVALID'; 473 | break; 474 | case 26: return 5; 475 | break; 476 | } 477 | }; 478 | lexer.rules = [/^[^\x00]*?(?=(\{\{))/,/^[^\x00]+/,/^[^\x00]{2,}?(?=(\{\{))/,/^\{\{>/,/^\{\{#/,/^\{\{\//,/^\{\{\^/,/^\{\{\s*else\b/,/^\{\{\{/,/^\{\{&/,/^\{\{![\s\S]*?\}\}/,/^\{\{/,/^=/,/^\.(?=[} ])/,/^\.\./,/^[\/.]/,/^\s+/,/^\}\}\}/,/^\}\}/,/^"(\\["]|[^"])*"/,/^true(?=[}\s])/,/^false(?=[}\s])/,/^[0-9]+(?=[}\s])/,/^[a-zA-Z0-9_$-]+(?=[=}\s\/.])/,/^\[[^\]]*\]/,/^./,/^$/]; 479 | lexer.conditions = {"mu":{"rules":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26],"inclusive":false},"emu":{"rules":[2],"inclusive":false},"INITIAL":{"rules":[0,1,26],"inclusive":true}};return lexer;})() 480 | parser.lexer = lexer; 481 | return parser; 482 | })(); 483 | if (typeof require !== 'undefined' && typeof exports !== 'undefined') { 484 | exports.parser = handlebars; 485 | exports.parse = function () { return handlebars.parse.apply(handlebars, arguments); } 486 | exports.main = function commonjsMain(args) { 487 | if (!args[1]) 488 | throw new Error('Usage: '+args[0]+' FILE'); 489 | if (typeof process !== 'undefined') { 490 | var source = require('fs').readFileSync(require('path').join(process.cwd(), args[1]), "utf8"); 491 | } else { 492 | var cwd = require("file").path(require("file").cwd()); 493 | var source = cwd.join(args[1]).read({charset: "utf-8"}); 494 | } 495 | return exports.parser.parse(source); 496 | } 497 | if (typeof module !== 'undefined' && require.main === module) { 498 | exports.main(typeof process !== 'undefined' ? process.argv.slice(1) : require("system").args); 499 | } 500 | }; 501 | ; 502 | // lib/handlebars/compiler/base.js 503 | Handlebars.Parser = handlebars; 504 | 505 | Handlebars.parse = function(string) { 506 | Handlebars.Parser.yy = Handlebars.AST; 507 | return Handlebars.Parser.parse(string); 508 | }; 509 | 510 | Handlebars.print = function(ast) { 511 | return new Handlebars.PrintVisitor().accept(ast); 512 | }; 513 | 514 | Handlebars.logger = { 515 | DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3, level: 3, 516 | 517 | // override in the host environment 518 | log: function(level, str) {} 519 | }; 520 | 521 | Handlebars.log = function(level, str) { Handlebars.logger.log(level, str); }; 522 | ; 523 | // lib/handlebars/compiler/ast.js 524 | (function() { 525 | 526 | Handlebars.AST = {}; 527 | 528 | Handlebars.AST.ProgramNode = function(statements, inverse) { 529 | this.type = "program"; 530 | this.statements = statements; 531 | if(inverse) { this.inverse = new Handlebars.AST.ProgramNode(inverse); } 532 | }; 533 | 534 | Handlebars.AST.MustacheNode = function(params, hash, unescaped) { 535 | this.type = "mustache"; 536 | this.id = params[0]; 537 | this.params = params.slice(1); 538 | this.hash = hash; 539 | this.escaped = !unescaped; 540 | }; 541 | 542 | Handlebars.AST.PartialNode = function(id, context) { 543 | this.type = "partial"; 544 | 545 | // TODO: disallow complex IDs 546 | 547 | this.id = id; 548 | this.context = context; 549 | }; 550 | 551 | var verifyMatch = function(open, close) { 552 | if(open.original !== close.original) { 553 | throw new Handlebars.Exception(open.original + " doesn't match " + close.original); 554 | } 555 | }; 556 | 557 | Handlebars.AST.BlockNode = function(mustache, program, close) { 558 | verifyMatch(mustache.id, close); 559 | this.type = "block"; 560 | this.mustache = mustache; 561 | this.program = program; 562 | }; 563 | 564 | Handlebars.AST.InverseNode = function(mustache, program, close) { 565 | verifyMatch(mustache.id, close); 566 | this.type = "inverse"; 567 | this.mustache = mustache; 568 | this.program = program; 569 | }; 570 | 571 | Handlebars.AST.ContentNode = function(string) { 572 | this.type = "content"; 573 | this.string = string; 574 | }; 575 | 576 | Handlebars.AST.HashNode = function(pairs) { 577 | this.type = "hash"; 578 | this.pairs = pairs; 579 | }; 580 | 581 | Handlebars.AST.IdNode = function(parts) { 582 | this.type = "ID"; 583 | this.original = parts.join("."); 584 | 585 | var dig = [], depth = 0; 586 | 587 | for(var i=0,l=parts.length; i": ">", 646 | '"': """, 647 | "'": "'", 648 | "`": "`" 649 | }; 650 | 651 | var badChars = /&(?!\w+;)|[<>"'`]/g; 652 | var possible = /[&<>"'`]/; 653 | 654 | var escapeChar = function(chr) { 655 | return escape[chr] || "&"; 656 | }; 657 | 658 | Handlebars.Utils = { 659 | escapeExpression: function(string) { 660 | // don't escape SafeStrings, since they're already safe 661 | if (string instanceof Handlebars.SafeString) { 662 | return string.toString(); 663 | } else if (string == null || string === false) { 664 | return ""; 665 | } 666 | 667 | if(!possible.test(string)) { return string; } 668 | return string.replace(badChars, escapeChar); 669 | }, 670 | 671 | isEmpty: function(value) { 672 | if (typeof value === "undefined") { 673 | return true; 674 | } else if (value === null) { 675 | return true; 676 | } else if (value === false) { 677 | return true; 678 | } else if(Object.prototype.toString.call(value) === "[object Array]" && value.length === 0) { 679 | return true; 680 | } else { 681 | return false; 682 | } 683 | } 684 | }; 685 | })();; 686 | // lib/handlebars/compiler/compiler.js 687 | Handlebars.Compiler = function() {}; 688 | Handlebars.JavaScriptCompiler = function() {}; 689 | 690 | (function(Compiler, JavaScriptCompiler) { 691 | Compiler.OPCODE_MAP = { 692 | appendContent: 1, 693 | getContext: 2, 694 | lookupWithHelpers: 3, 695 | lookup: 4, 696 | append: 5, 697 | invokeMustache: 6, 698 | appendEscaped: 7, 699 | pushString: 8, 700 | truthyOrFallback: 9, 701 | functionOrFallback: 10, 702 | invokeProgram: 11, 703 | invokePartial: 12, 704 | push: 13, 705 | assignToHash: 15, 706 | pushStringParam: 16 707 | }; 708 | 709 | Compiler.MULTI_PARAM_OPCODES = { 710 | appendContent: 1, 711 | getContext: 1, 712 | lookupWithHelpers: 2, 713 | lookup: 1, 714 | invokeMustache: 3, 715 | pushString: 1, 716 | truthyOrFallback: 1, 717 | functionOrFallback: 1, 718 | invokeProgram: 3, 719 | invokePartial: 1, 720 | push: 1, 721 | assignToHash: 1, 722 | pushStringParam: 1 723 | }; 724 | 725 | Compiler.DISASSEMBLE_MAP = {}; 726 | 727 | for(var prop in Compiler.OPCODE_MAP) { 728 | var value = Compiler.OPCODE_MAP[prop]; 729 | Compiler.DISASSEMBLE_MAP[value] = prop; 730 | } 731 | 732 | Compiler.multiParamSize = function(code) { 733 | return Compiler.MULTI_PARAM_OPCODES[Compiler.DISASSEMBLE_MAP[code]]; 734 | }; 735 | 736 | Compiler.prototype = { 737 | compiler: Compiler, 738 | 739 | disassemble: function() { 740 | var opcodes = this.opcodes, opcode, nextCode; 741 | var out = [], str, name, value; 742 | 743 | for(var i=0, l=opcodes.length; i 0) { 1128 | this.source[1] = this.source[1] + ", " + locals.join(", "); 1129 | } 1130 | 1131 | // Generate minimizer alias mappings 1132 | if (!this.isChild) { 1133 | var aliases = [] 1134 | for (var alias in this.context.aliases) { 1135 | this.source[1] = this.source[1] + ', ' + alias + '=' + this.context.aliases[alias]; 1136 | } 1137 | } 1138 | 1139 | if (this.source[1]) { 1140 | this.source[1] = "var " + this.source[1].substring(2) + ";"; 1141 | } 1142 | 1143 | // Merge children 1144 | if (!this.isChild) { 1145 | this.source[1] += '\n' + this.context.programs.join('\n') + '\n'; 1146 | } 1147 | 1148 | if (!this.environment.isSimple) { 1149 | this.source.push("return buffer;"); 1150 | } 1151 | 1152 | var params = this.isChild ? ["depth0", "data"] : ["Handlebars", "depth0", "helpers", "partials", "data"]; 1153 | 1154 | for(var i=0, l=this.environment.depths.list.length; i this.stackVars.length) { this.stackVars.push("stack" + this.stackSlot); } 1406 | return "stack" + this.stackSlot; 1407 | }, 1408 | 1409 | popStack: function() { 1410 | return "stack" + this.stackSlot--; 1411 | }, 1412 | 1413 | topStack: function() { 1414 | return "stack" + this.stackSlot; 1415 | }, 1416 | 1417 | quotedString: function(str) { 1418 | return '"' + str 1419 | .replace(/\\/g, '\\\\') 1420 | .replace(/"/g, '\\"') 1421 | .replace(/\n/g, '\\n') 1422 | .replace(/\r/g, '\\r') + '"'; 1423 | } 1424 | }; 1425 | 1426 | var reservedWords = ( 1427 | "break else new var" + 1428 | " case finally return void" + 1429 | " catch for switch while" + 1430 | " continue function this with" + 1431 | " default if throw" + 1432 | " delete in try" + 1433 | " do instanceof typeof" + 1434 | " abstract enum int short" + 1435 | " boolean export interface static" + 1436 | " byte extends long super" + 1437 | " char final native synchronized" + 1438 | " class float package throws" + 1439 | " const goto private transient" + 1440 | " debugger implements protected volatile" + 1441 | " double import public let yield" 1442 | ).split(" "); 1443 | 1444 | var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; 1445 | 1446 | for(var i=0, l=reservedWords.length; i2083){this.fireEvent("error",f);}Request.JSONP.request_map["request_"+b]=function(){this.success(arguments,b);}.bind(this);var a=this.getScript(f).inject(c.injectScript); 32 | this.fireEvent("request",[f,a]);if(c.timeout){this.timeout.delay(c.timeout,this);}return this;},getScript:function(a){if(!this.script){this.script=new Element("script",{type:"text/javascript",async:true,src:a}); 33 | }return this.script;},success:function(b,a){if(!this.running){return;}this.clear().fireEvent("complete",b).fireEvent("success",b).callChain();},cancel:function(){if(this.running){this.clear().fireEvent("cancel"); 34 | }return this;},isRunning:function(){return !!this.running;},clear:function(){this.running=false;if(this.script){this.script.destroy();this.script=null; 35 | }return this;},timeout:function(){if(this.running){this.running=false;this.fireEvent("timeout",[this.script.get("src"),this.script]).fireEvent("failure").cancel(); 36 | }return this;}});Request.JSONP.counter=0;Request.JSONP.request_map={}; -------------------------------------------------------------------------------- /examples/account/public/js/utils.js: -------------------------------------------------------------------------------- 1 | function loadTemplate(url, callback) { 2 | new Request({ 3 | url: url, 4 | onComplete: function (html) { 5 | var template = Handlebars.compile(html) 6 | var tabular = html.match(//g) 7 | callback(null, function (data) { 8 | return new Element(tabular ? 'tbody' : 'div', { html: template(data || {}) }).getFirst() 9 | }) 10 | } 11 | }).get() 12 | } 13 | 14 | function get(url, data, callback) { 15 | var args = Array.prototype.slice.call(arguments) 16 | callback = args.pop() 17 | new Request.JSONP({ 18 | url: url, 19 | data: data, 20 | onComplete: function (response) { 21 | callback(null, response) 22 | }, 23 | onTimeout: function () { 24 | callback(new Error('Request timed out: ' + url)) 25 | } 26 | }).send() 27 | } 28 | 29 | var __requestsMap__ = {} 30 | function post(url, fields, callback) { 31 | var requestId = new Date().getTime() 32 | var args = Array.prototype.slice.call(arguments) 33 | callback = args.pop() 34 | 35 | if ($(fields)) { 36 | var enctype = fields.get('enctype') 37 | if (enctype != 'multipart/form-data') { 38 | enctype = 'x-www-form-urlencoded' 39 | } 40 | var inputs = fields.getElements('input').map(function (input) { 41 | input.clone(true, true).inject(input.erase('id'), 'before') 42 | return input.dispose() 43 | }) 44 | } else { 45 | var inputs = [] 46 | Object.keys(fields).forEach(function (fieldName) { 47 | inputs.push(new Element('input', { 48 | name: fieldName, 49 | value: data[fieldName] 50 | })) 51 | }) 52 | } 53 | 54 | if (url.match(/\?/)) { 55 | url += '&callback=__requestsMap__["' + requestId + '"]' 56 | } else { 57 | url += '?callback=__requestsMap__["' + requestId + '"]' 58 | } 59 | 60 | var form = new Element('form', { 61 | 'enctype': enctype, 62 | 'method': 'post', 63 | 'action': url, 64 | 'target': requestId, 65 | 'style': 'display: none' 66 | }).adopt(inputs).inject(document.body) 67 | 68 | var iframe = new Element('iframe', { 69 | id: requestId, 70 | name: requestId, 71 | styles: { display: 'none' } 72 | }).inject(document.body) 73 | 74 | __requestsMap__[requestId] = function (response) { 75 | callback(null, response) 76 | // cleanup 77 | delete __requestsMap__[requestId] 78 | iframe.dispose().destroy() 79 | form.dispose().destroy() 80 | } 81 | 82 | form.submit() 83 | } -------------------------------------------------------------------------------- /examples/account/public/partials/user_row.html: -------------------------------------------------------------------------------- 1 | 2 | {{_id}} 3 | {{email}} 4 | {{password}} 5 | 6 | signin 7 | info 8 | delete 9 | 10 | -------------------------------------------------------------------------------- /examples/account/server.js: -------------------------------------------------------------------------------- 1 | require('console-trace')({ always: true, right: true, colors: true }) 2 | 3 | var ApiServer = require('apiserver'), 4 | ApiModules = require('./lib/api_modules'), 5 | routes = require('./config/routes'), 6 | jsonRequest = require('request'), 7 | colors = require('colors'), 8 | connect = require('connect'), 9 | mongodb = require('mongodb') 10 | 11 | var mongodbServer = new mongodb.Server('localhost', 27017, { auto_reconnect: true, poolSize: 5 }) 12 | var mongodbDb = new mongodb.Db('apiserver-example-account', mongodbServer, { native_parser: false }) 13 | 14 | mongodbDb.open(function (err, mongodbClient) { 15 | if (err) { 16 | console.error('\n ☹ Cannot connect to mongodb: %s\n'.red, err.message) 17 | return 18 | } 19 | var collections = { 20 | users: new mongodb.Collection(mongodbClient, 'users') 21 | } 22 | 23 | // Static server allocation 24 | connect().use(connect.static(__dirname + '/public')).listen(8000, function () { 25 | console.info('\n ✈ Static server listening at http://localhst:8000'.green) 26 | }) 27 | 28 | // ApiServer allocation 29 | var apiServer = new ApiServer({ 30 | timeout: 1000, 31 | domain: 'localhost' 32 | }) 33 | 34 | // middleware 35 | apiServer.use(/^\/list$/, ApiServer.httpAuth({ 36 | realm: 'ApiServer Example', 37 | encode: true, 38 | credentials: ['admin:apiserver'] 39 | })) 40 | apiServer.use(/^\/(signin)|(signup)|(delete)$/, ApiServer.payloadParser()) 41 | apiServer.use(/^\/(signin)|(signup)|(delete)$/, function (request, response, next) { 42 | // you can write inline middleware 43 | next() 44 | }) 45 | 46 | // modules and routing 47 | apiServer.addModule('1', 'users', new ApiModules.Users({ collections: collections })) 48 | apiServer.router.addRoutes(routes) 49 | 50 | // events 51 | apiServer.on('requestStart', function (pathname, time) { 52 | console.info(' ☉ :: start :: %s'.grey, pathname) 53 | }).on('requestEnd', function (pathname, time) { 54 | console.info(' ☺ :: end :: %s in %dms'.grey, pathname, time) 55 | }).on('error', function (pathname, err) { 56 | console.info(' ☹ :: error :: %s (%s)'.red, pathname, err.message) 57 | }).on('timeout', function (pathname) { 58 | console.info(' ☂ :: timedout :: %s'.yellow, pathname) 59 | }) 60 | 61 | apiServer.listen(8080, function () { 62 | console.info(' ✈ ApiServer listening at http://localhst:8080\n'.green) 63 | }) 64 | }) -------------------------------------------------------------------------------- /examples/instagram/config/routes.json: -------------------------------------------------------------------------------- 1 | [ 2 | ["/photos", "1/photos#index", { "limit": 20, "page": 1 }], 3 | ["/photos/:id", "1/photos#photo"] 4 | ] -------------------------------------------------------------------------------- /examples/instagram/lib/api_modules/index.js: -------------------------------------------------------------------------------- 1 | exports.Photos = require("./photos") -------------------------------------------------------------------------------- /examples/instagram/lib/api_modules/photos.js: -------------------------------------------------------------------------------- 1 | var ObjectId = require('mongodb').ObjectID, 2 | fs = require('fs'), 3 | crypto = require('crypto'), 4 | path = require('path') 5 | 6 | var Photos = module.exports = function (options) { 7 | var self = this 8 | options = (options !== null && options !== undefined && options.constructor === Object) ? options : {} 9 | Object.keys(options).forEach(function (key) { 10 | if (!self.__proto__.hasOwnProperty(key)) { 11 | self[key] = options[key] 12 | } 13 | }) 14 | } 15 | 16 | Photos.prototype.index = { 17 | get: function (request, response) { 18 | var limit = Number(request.querystring.limit) 19 | var page = Number(request.querystring.page) 20 | this.collections.photos.find({}, { sort: { _id: -1 }, limit: limit, skip: (page-1) * limit }).stream().on('data', function(item) { 21 | response.streamJSON(item) 22 | }).once('close', function() { 23 | response.streamJSON() 24 | }) 25 | }, 26 | post: function (request, response) { 27 | var self = this 28 | var photo = {} 29 | request.resume() 30 | request.form.uploadDir = self.uploadTempPath 31 | request.form.once('end', function () { 32 | var file = request.files.photo 33 | photo.filename = getUniqueFilename(file.name) 34 | photo.title = request.body.title 35 | photo.caption = request.body.caption 36 | photo.likes = 0 37 | fs.rename(file.path, self.uploadPath + '/' + photo.filename, function (err) { 38 | if (err) { 39 | throw err 40 | } 41 | self.collections.photos.save(photo, function (err, document) { 42 | response.serveJSON({ success: true, photo: photo }) 43 | }) 44 | }) 45 | }) 46 | } 47 | } 48 | 49 | Photos.prototype.photo = { 50 | get: function (request, response) { 51 | this.collections.photos.findOne({ _id: new ObjectId(request.querystring.id) }, function (err, document) { 52 | if (!err && !document) { 53 | err = new Error('document ' + request.querystring.id + ' not found') 54 | } 55 | if (err) { 56 | response.serveJSON({ success: false, error: err.msg || err.message }) 57 | return 58 | } 59 | response.serveJSON({ success: true, user: document }) 60 | }) 61 | }, 62 | delete: function (request, response) { 63 | var self = this 64 | request.resume() 65 | request.once('end', function () { 66 | self.collections.photos.findAndRemove({ _id: new ObjectId(request.querystring.id) }, [], { safe: true }, function (err, document) { 67 | if (!err && !document) { 68 | err = new Error('document ' + request.querystring.id + ' not found') 69 | } 70 | if (err) { 71 | response.serveJSON({ success: false, error: err.msg || err.message }) 72 | return 73 | } 74 | fs.unlink(self.uploadPath + '/' + document.filename, function () { 75 | response.serveJSON({ success: true, id: request.body.id }) 76 | }) 77 | }) 78 | }) 79 | }, 80 | put: function (request, response) { 81 | var self = this 82 | request.resume() 83 | request.once('end', function () { 84 | self.collections.photos.findAndModify({ _id: new ObjectId(request.querystring.id) }, [], { $inc: { likes: 1 } }, { new: true }, function (err, document) { 85 | if (!err && !document) { 86 | err = new Error('document ' + request.querystring.id + ' not found') 87 | } 88 | if (err) { 89 | response.serveJSON({ success: false, error: err.msg || err.message }) 90 | return 91 | } 92 | response.serveJSON({ success: true, photo: document }) 93 | }) 94 | }) 95 | }, 96 | // fix for browsers that only allow GET / POST 97 | post: function (request, response) { 98 | if (request.querystring.action === 'delete') { 99 | this.photo.delete.apply(this, arguments) 100 | } else if (request.querystring.action === 'put') { 101 | this.photo.put.apply(this, arguments) 102 | } else { 103 | response.serveJSON() 104 | } 105 | } 106 | } 107 | 108 | function getUniqueFilename(filename) { 109 | var extename = path.extname(filename) 110 | var hash = crypto.createHash('sha1') 111 | hash.update(new Buffer(new Date().getTime().toString())) 112 | return hash.digest('hex') + extename 113 | } -------------------------------------------------------------------------------- /examples/instagram/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apiserver-example", 3 | "version": "0.0.0", 4 | "private": true, 5 | "dependencies": { 6 | "console-trace": "https://github.com/LearnBoost/console-trace/tarball/master", 7 | "apiserver": "0.2.x", 8 | "mongodb": "1.0.x", 9 | "request": "2.9.x", 10 | "connect": "2.3.x", 11 | "colors": "" 12 | } 13 | } -------------------------------------------------------------------------------- /examples/instagram/public/css/main.css: -------------------------------------------------------------------------------- 1 | .thumbnail p { margin-top: 15px } 2 | input[type="file"] { position: absolute; top: -100px } -------------------------------------------------------------------------------- /examples/instagram/public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilianc/node-apiserver/82ef85cb86075e7f6f125d4fd539f217334ae0b6/examples/instagram/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /examples/instagram/public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kilianc/node-apiserver/82ef85cb86075e7f6f125d4fd539f217334ae0b6/examples/instagram/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /examples/instagram/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ApiServer Examples / account 14 | 15 | 16 | 17 |
18 | 21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 |

Type the title of the photo and then press upload

29 |
30 |
31 | 32 |
33 |
34 |
    35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /examples/instagram/public/js/async.min.js: -------------------------------------------------------------------------------- 1 | /*global setTimeout: false, console: false */(function(){var a={},b=this,c=b.async;typeof module!="undefined"&&module.exports?module.exports=a:b.async=a,a.noConflict=function(){return b.async=c,a};var d=function(a,b){if(a.forEach)return a.forEach(b);for(var c=0;cd?1:0};d(null,e(b.sort(c),function(a){return a.value}))})},a.auto=function(a,b){b=b||function(){};var c=g(a);if(!c.length)return b(null);var e={},h=[],i=function(a){h.unshift(a)},j=function(a){for(var b=0;b2083){this.fireEvent("error",f);}Request.JSONP.request_map["request_"+b]=function(){this.success(arguments,b);}.bind(this);var a=this.getScript(f).inject(c.injectScript); 32 | this.fireEvent("request",[f,a]);if(c.timeout){this.timeout.delay(c.timeout,this);}return this;},getScript:function(a){if(!this.script){this.script=new Element("script",{type:"text/javascript",async:true,src:a}); 33 | }return this.script;},success:function(b,a){if(!this.running){return;}this.clear().fireEvent("complete",b).fireEvent("success",b).callChain();},cancel:function(){if(this.running){this.clear().fireEvent("cancel"); 34 | }return this;},isRunning:function(){return !!this.running;},clear:function(){this.running=false;if(this.script){this.script.destroy();this.script=null; 35 | }return this;},timeout:function(){if(this.running){this.running=false;this.fireEvent("timeout",[this.script.get("src"),this.script]).fireEvent("failure").cancel(); 36 | }return this;}});Request.JSONP.counter=0;Request.JSONP.request_map={}; -------------------------------------------------------------------------------- /examples/instagram/public/js/utils.js: -------------------------------------------------------------------------------- 1 | function loadTemplate(url, callback) { 2 | new Request({ 3 | url: url, 4 | onComplete: function (html) { 5 | var template = Handlebars.compile(html) 6 | var tabular = html.match(//g) 7 | callback(null, function (data) { 8 | return new Element(tabular ? 'tbody' : 'div', { html: template(data || {}) }).getFirst() 9 | }) 10 | } 11 | }).get() 12 | } 13 | 14 | function get(url, data, callback) { 15 | var args = Array.prototype.slice.call(arguments) 16 | callback = args.pop() 17 | new Request.JSONP({ 18 | url: url, 19 | data: data, 20 | onComplete: function (response) { 21 | callback(null, response) 22 | }, 23 | onTimeout: function () { 24 | callback(new Error('Request timed out: ' + url)) 25 | } 26 | }).send() 27 | } 28 | 29 | var __requestsMap__ = {} 30 | function post(url, fields, callback) { 31 | var requestId = new Date().getTime() 32 | var args = Array.prototype.slice.call(arguments) 33 | callback = args.pop() 34 | 35 | if ($(fields)) { 36 | var enctype = fields.get('enctype') 37 | if (enctype != 'multipart/form-data') { 38 | enctype = 'x-www-form-urlencoded' 39 | } 40 | var inputs = fields.getElements('input').map(function (input) { 41 | input.clone(true, true).inject(input.erase('id'), 'before') 42 | return input.dispose() 43 | }) 44 | } else { 45 | var inputs = [] 46 | Object.keys(fields).forEach(function (fieldName) { 47 | inputs.push(new Element('input', { 48 | name: fieldName, 49 | value: data[fieldName] 50 | })) 51 | }) 52 | } 53 | 54 | if (url.match(/\?/)) { 55 | url += '&callback=__requestsMap__["' + requestId + '"]' 56 | } else { 57 | url += '?callback=__requestsMap__["' + requestId + '"]' 58 | } 59 | 60 | var form = new Element('form', { 61 | 'enctype': enctype, 62 | 'method': 'post', 63 | 'action': url, 64 | 'target': requestId, 65 | 'style': 'display: none' 66 | }).adopt(inputs).inject(document.body) 67 | 68 | var iframe = new Element('iframe', { 69 | id: requestId, 70 | name: requestId, 71 | styles: { display: 'none' } 72 | }).inject(document.body) 73 | 74 | __requestsMap__[requestId] = function (response) { 75 | callback(null, response) 76 | // cleanup 77 | delete __requestsMap__[requestId] 78 | iframe.dispose().destroy() 79 | form.dispose().destroy() 80 | } 81 | 82 | form.submit() 83 | } -------------------------------------------------------------------------------- /examples/instagram/public/partials/photo.html: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 | 4 | 5 | 6 |
    7 |
    {{title}}
    8 |

    {{description}}

    9 |

    10 | 11 | delete 12 |

    13 |
    14 |
    15 |
  • -------------------------------------------------------------------------------- /examples/instagram/server.js: -------------------------------------------------------------------------------- 1 | require('console-trace')({ always: true, right: true, colors: true }) 2 | 3 | var ApiServer = require('apiserver'), 4 | ApiModules = require('./lib/api_modules'), 5 | routes = require('./config/routes'), 6 | jsonRequest = require('request'), 7 | colors = require('colors'), 8 | connect = require('connect'), 9 | mongodb = require('mongodb') 10 | 11 | var mongodbServer = new mongodb.Server('localhost', 27017, { auto_reconnect: true, poolSize: 5 }) 12 | var mongodbDb = new mongodb.Db('apiserver-example-instagram', mongodbServer, { native_parser: false }) 13 | 14 | mongodbDb.open(function (err, mongodbClient) { 15 | if (err) { 16 | console.error('\n ☹ Cannot connect to mongodb: %s\n'.red, err.message) 17 | return 18 | } 19 | var collections = { 20 | photos: new mongodb.Collection(mongodbClient, 'photos') 21 | } 22 | 23 | // Static server allocation 24 | connect().use(connect.static(__dirname + '/public')).listen(8000, function () { 25 | console.info('\n ✈ Static server listening at http://localhst:8000'.green) 26 | }) 27 | 28 | // ApiServer allocation 29 | var apiServer = new ApiServer({ 30 | timeout: 1000, 31 | domain: 'localhost' 32 | }) 33 | 34 | // middleware 35 | apiServer.use(/^\/list$/, ApiServer.httpAuth({ 36 | realm: 'ApiServer Example', 37 | encode: true, 38 | credentials: ['admin:apiserver'] 39 | })) 40 | apiServer.use(ApiServer.payloadParser()) 41 | apiServer.use(ApiServer.multipartParser()) 42 | 43 | // modules and routing 44 | apiServer.addModule('1', 'photos', new ApiModules.Photos({ 45 | collections: collections, 46 | // in my case /tmp is on a different partition 47 | // so I moved the tmp folder in order to avoid files moving. 48 | // cleaning tmp folder is up to you 49 | uploadTempPath: process.cwd() + '/tmp', 50 | uploadPath: process.cwd() + '/public/uploads' 51 | })) 52 | apiServer.router.addRoutes(routes) 53 | 54 | // events 55 | apiServer.on('requestStart', function (pathname, time) { 56 | console.info(' ☉ :: start :: %s'.grey, pathname) 57 | }).on('requestEnd', function (pathname, time) { 58 | console.info(' ☺ :: end :: %s in %dms'.grey, pathname, time) 59 | }).on('error', function (pathname, err) { 60 | console.info(' ☹ :: error :: %s (%s)'.red, pathname, err.message) 61 | }).on('timeout', function (pathname) { 62 | console.info(' ☂ :: timedout :: %s'.yellow, pathname) 63 | }) 64 | 65 | apiServer.listen(8080, function () { 66 | console.info(' ✈ ApiServer listening at http://localhst:8080\n'.green) 67 | }) 68 | }) -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = process.env.APISERVER_COV 2 | ? require('./lib-cov/apiserver') 3 | : require('./lib/apiserver') -------------------------------------------------------------------------------- /lib/apiserver.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | parse = require('url').parse, 3 | qs = require('qs'), 4 | util = require('util'), 5 | events = require('events'), 6 | middleware = require('./middleware'), 7 | Router = require('apiserver-router'), 8 | Chain = require('fnchain') 9 | 10 | var ApiServer = module.exports = function (options) { 11 | var self = this 12 | ApiServer.super_.call(this) 13 | 14 | options = (options !== null && options !== undefined && options.constructor === Object) ? options : {} 15 | options.timeout = !options.timeout || options.timeout < 0 ? 15000 : options.timeout 16 | options.standardHeaders = options.standardHeaders || { 17 | 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', 18 | 'expires': 0, 19 | 'pragma': 'no-cache', 20 | 'x-server': 'ApiServer v' + ApiServer.version + ' raging on nodejs ' + process.version 21 | } 22 | options.port = options.port || 8080 23 | options.server = options.server || http.createServer() 24 | options.router = options.router || new Router(options) 25 | 26 | Object.keys(options).forEach(function (key) { 27 | if (!self.__proto__.hasOwnProperty(key)) { 28 | self[key] = options[key] 29 | } 30 | }) 31 | 32 | this.JSONTransport = middleware.JSONTransport(this, options) 33 | this.middlewareList = [] 34 | this.activeApiModules = {} 35 | this.server.on('request', onRequest.bind(this)) 36 | } 37 | 38 | module.exports.version = require('../package').version 39 | 40 | util.inherits(module.exports, events.EventEmitter) 41 | 42 | ApiServer.prototype.addModule = function (apiVersion, moduleName, module) { 43 | if (this.activeApiModules[apiVersion] === undefined) { 44 | this.activeApiModules[apiVersion] = Object.create(null) 45 | } 46 | this.activeApiModules[apiVersion][moduleName] = module 47 | this.router.update(this.activeApiModules, this.middlewareList) 48 | return this 49 | } 50 | 51 | ApiServer.prototype.use = function () { 52 | var args = Array.prototype.slice.call(arguments) 53 | var middleware = args.pop() 54 | var route = args.pop() || /./ 55 | this.middlewareList.push({ 56 | route: route, 57 | handle: middleware 58 | }) 59 | this.router.update(this.activeApiModules, this.middlewareList) 60 | return this 61 | } 62 | 63 | ApiServer.prototype.listen = function () { 64 | var arguments = Array.prototype.slice.call(arguments) 65 | var port = this.port, hostname, callback 66 | arguments.forEach(function (arg) { 67 | if (typeof arg === 'function') { 68 | callback = arg 69 | return 70 | } 71 | if (typeof arg === 'string' && isNaN(Number(arg))) { 72 | hostname = arg 73 | return 74 | } 75 | if (!isNaN(Number(arg))) { 76 | port = arg 77 | return 78 | } 79 | }) 80 | this.server.listen(port, hostname, callback) 81 | } 82 | 83 | ApiServer.prototype.close = function (callback) { 84 | callback && this.server.once('close', callback) 85 | this.server.close() 86 | } 87 | 88 | // export middleware 89 | Object.keys(middleware).forEach(function (middlewareName) { 90 | module.exports[middlewareName] = middleware[middlewareName] 91 | }) 92 | 93 | // private 94 | function onRequest(request, response) { 95 | var self = this 96 | var end = response.end 97 | var parsed = parse(request.url, true) 98 | 99 | request.requestedAt = request.at = new Date().getTime() 100 | request.parsedUrl = parse(request.url, true) 101 | request.pathname = request.path = request.parsedUrl.pathname.replace(/\/\/+/g, '/') 102 | request.querystring = request.qs = qs.parse(request.parsedUrl.search.replace(/^\?/, '')) 103 | request.pause() 104 | request.timeout = setInterval(function () { 105 | response.writeHead(408) 106 | end.call(response) 107 | clearInterval(request.timeout) 108 | self.emit('timeout', request.url) 109 | }, this.timeout) 110 | 111 | response.end = function () { 112 | clearInterval(request.timeout) 113 | end.apply(this, arguments) 114 | self.emit('requestEnd', request.url, new Date().getTime() - request.requestedAt) 115 | } 116 | 117 | this.emit('requestStart', request.url, request.requestedAt) 118 | this.JSONTransport(request, response) 119 | 120 | var executionChain = this.router.get(request) 121 | 122 | if (!executionChain) { 123 | return response.serveJSON({ 124 | success: false, 125 | reason: request.pathname + ' api not found' 126 | }, { 127 | httpStatusCode: 404, 128 | }) 129 | } 130 | 131 | new Chain(executionChain, function (err) { 132 | if (err) { 133 | response.serveJSON({ 134 | success: false, 135 | reason: 'something went wrong: ' + err, 136 | stack: err.stack 137 | }, { 138 | httpStatusCode: 500 139 | }) 140 | self.emit('error', request.url, err) 141 | } 142 | }).call(request, response) 143 | } 144 | -------------------------------------------------------------------------------- /lib/middleware/file-trasport.js: -------------------------------------------------------------------------------- 1 | var url = require('url'), 2 | fs = require('fs'), 3 | mime = require('mime') 4 | 5 | module.exports = function () { 6 | } 7 | 8 | module.exports.prototype.attach = function (request, response) { 9 | response.serveFile = serveFile.bind(this, request, response) 10 | } 11 | 12 | // private 13 | 14 | function serveFile(request, response, path, params) { 15 | params = normalizeParams(params) 16 | fs.stat(path, function (err, stat) { 17 | if (err || !stats.isFile()) { 18 | response.writeHead(404) 19 | response.end() 20 | } else { 21 | fillObject(params.headers, this.standardHeaders) 22 | params.headers['content-legth'] = stat.size 23 | params.headers['content-type'] = mimetypes.getContentTypeFromPath(path) 24 | response.writeHead(params.httpStatusCode, params.httpStatusMessage, params.headers) 25 | fs.createReadStream(path, params).pipe(response) 26 | } 27 | }) 28 | } 29 | 30 | function normalizeParams (params) { 31 | params = params || {} 32 | params.headers = params.headers || {} 33 | params.httpStatusCode = isNaN(params.httpStatusCode) ? 200 : params.httpStatusCode 34 | params.httpStatusMessage = params.httpStatusMessage || '' 35 | return params; 36 | } 37 | 38 | function fillObject (targetObj, fillObj) { 39 | Object.keys(fillObj).forEach(function (key) { 40 | if (!targetObj.hasOwnProperty(key)) { 41 | targetObj[key] = fillObj[key] 42 | } 43 | }) 44 | } -------------------------------------------------------------------------------- /lib/middleware/httpauth.js: -------------------------------------------------------------------------------- 1 | module.exports = function (options) { 2 | options = (options !== null && options !== undefined && options.constructor === Object) ? options : {} 3 | options.realm = options.realm || 'Please signin.' 4 | options.credentials = options.credentials || [] 5 | 6 | if (options.encode) { 7 | options.credentials.forEach(function (credential, i) { 8 | options.credentials[i] = 'Basic ' + new Buffer(credential, 'utf8').toString('base64') 9 | }) 10 | } 11 | 12 | return function (request, response, next) { 13 | if (request.headers.authorization === undefined) { 14 | onAuthFailed(response, options.realm) 15 | return next(null, true) 16 | } 17 | var allowed = false 18 | for (var i = 0; i < options.credentials.length && allowed === false; i++) { 19 | allowed |= options.credentials[i] === request.headers.authorization 20 | } 21 | if (!allowed) { 22 | onAuthFailed(response, options.realm) 23 | } 24 | next(null, !allowed) 25 | } 26 | } 27 | 28 | function onAuthFailed(response, realm) { 29 | response.serveJSON(null, { 30 | httpStatusCode: 401, 31 | headers: { 'www-authenticate': 'Basic realm=\'' + realm + '\'' } 32 | }) 33 | } -------------------------------------------------------------------------------- /lib/middleware/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | JSONTransport: require('json-transport'), 3 | httpAuth: require('./httpauth'), 4 | payloadParser: require('./payload-parser'), 5 | multipartParser: require('./multipart-parser') 6 | } -------------------------------------------------------------------------------- /lib/middleware/multipart-parser.js: -------------------------------------------------------------------------------- 1 | var formidable = require('formidable') 2 | 3 | module.exports = function () { 4 | return function (request, response, next) { 5 | if (!request.method.match(/(PUT|POST|OPTIONS)/)) { 6 | return next() 7 | } 8 | if (request.headers['content-type'].match(/multipart\/form-data/)) { 9 | request.form = new formidable.IncomingForm() 10 | request.form.parse(request, function (err, fields, files) { 11 | request.body = fields 12 | request.files = files 13 | request.parseError = err 14 | }) 15 | return next() 16 | } 17 | next() 18 | } 19 | } -------------------------------------------------------------------------------- /lib/middleware/payload-parser.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'), 2 | BufferJoiner = require('bufferjoiner') 3 | 4 | module.exports = function () { 5 | return function (request, response, next) { 6 | if (!request.method.match(/(PUT|POST|OPTIONS)/)) { 7 | return next() 8 | } 9 | request.rawBody = new BufferJoiner() 10 | request.on('data', function (chunk) { 11 | request.rawBody.add(chunk) 12 | }) 13 | request.once('end', function () { 14 | request.rawBody = request.rawBody.join() 15 | if (!request.headers['content-type']) return 16 | if (request.headers['content-type'].match(/application\/x-www-form-urlencoded/)) { 17 | request.body = querystring.parse(request.rawBody.toString('utf8')) 18 | } else if (request.headers['content-type'].match(/application\/json/)) { 19 | try { 20 | request.body = JSON.parse(request.rawBody.toString('utf8')) 21 | } catch(e) { 22 | request.parseError = e 23 | request.body = Object.create(null) 24 | } 25 | } 26 | }) 27 | next() 28 | } 29 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Kilian Ciuffolo (http://nailik.org)", 3 | "name": "apiserver", 4 | "description": "A ready to go, modular, JSON(P) API Server.", 5 | "keywords": [ 6 | "api", 7 | "server", 8 | "rest", 9 | "json", 10 | "jsonp" 11 | ], 12 | "version": "0.3.1", 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/kilianc/node-apiserver.git" 16 | }, 17 | "main": "./", 18 | "scripts": { 19 | "test": "make test" 20 | }, 21 | "engines": { 22 | "node": ">= v0.6.x" 23 | }, 24 | "dependencies": { 25 | "apiserver-router": "0.2.x", 26 | "bufferjoiner": "^0.1.3", 27 | "fnchain": "0.1.1", 28 | "formidable": "^1.0.15", 29 | "json-transport": "0.1.x", 30 | "mime": "^1.2.11", 31 | "qs": "^0.4.2" 32 | }, 33 | "devDependencies": { 34 | "mocha": "1.0.x", 35 | "should": "0.6.1", 36 | "request": "2.9.x" 37 | } 38 | } -------------------------------------------------------------------------------- /test/apiserver-test.js: -------------------------------------------------------------------------------- 1 | var http = require('http'), 2 | should = require('should'), 3 | Assertion = should.Assertion, 4 | jsonreq = require('request').defaults({ json: true }), 5 | ApiServer = require('../'), 6 | lib = process.env.APISERVER_COV ? 'lib-cov' : 'lib', 7 | middleware = require('../' + lib + '/middleware'), 8 | objectModule = require('./fixtures/object-module'), 9 | classModule = new (require('./fixtures/class-module'))(), 10 | version = require('../package').version 11 | 12 | var apiserver 13 | var defaultPort = 9000 14 | var customPort = 8080 15 | 16 | describe('ApiServer', function () { 17 | describe('should exports', function () { 18 | it('the right module version', function () { 19 | ApiServer.version.should.be.equal(version) 20 | }) 21 | it('all the middleware', function () { 22 | Object.keys(middleware).forEach(function (middlewareName) { 23 | ApiServer[middlewareName].should.be.equal(middleware[middlewareName]) 24 | }) 25 | }) 26 | }) 27 | describe('#()', function () { 28 | it('should use default values as options', function () { 29 | apiserver = new ApiServer() 30 | apiserver.timeout.should.be.equal(15000) 31 | apiserver.standardHeaders.should.be.eql({ 32 | 'cache-control': 'max-age=0, no-cache, no-store, must-revalidate', 33 | 'expires': 0, 34 | 'pragma': 'no-cache', 35 | 'x-server': 'ApiServer v' + ApiServer.version + ' raging on nodejs ' + process.version, 36 | 'access-control-allow-origin': '*.default.lan', 37 | 'access-control-allow-headers': 'X-Requested-With' 38 | }) 39 | }) 40 | it('should allocate a new http server', function () { 41 | apiserver.should.have.property('server') 42 | apiserver.server.should.be.an.instanceof(http.Server) 43 | }) 44 | it('should attach a request handler to the http server', function () { 45 | apiserver.server.listeners('request').should.have.length(1) 46 | }) 47 | it('should normalize the timeout option', function () { 48 | apiserver = new ApiServer({ timeout: 1000 }) 49 | apiserver.timeout.should.be.equal(1000) 50 | apiserver = new ApiServer({ timeout: -10 }) 51 | apiserver.timeout.should.be.equal(15000) 52 | apiserver = new ApiServer({ timeout: NaN }) 53 | apiserver.timeout.should.be.equal(15000) 54 | apiserver = new ApiServer({ timeout: null }) 55 | apiserver.timeout.should.be.equal(15000) 56 | }) 57 | it('should have JSONParser as default transport', function () { 58 | apiserver = new ApiServer() 59 | apiserver.JSONTransport.should.be.an.instanceof(Function) 60 | }) 61 | it('should pass options to the JSONParser', function () { 62 | var options = { foo: 'bar' } 63 | apiserver = new ApiServer(options) 64 | options.should.have.property('domain') 65 | }) 66 | it('should store extra transports', function () { 67 | var fakeTransport = { } 68 | apiserver = new ApiServer({ transports: [fakeTransport] }) 69 | apiserver.transports.should.include(fakeTransport) 70 | }) 71 | }) 72 | describe('#listen()', function () { 73 | before(function () { 74 | apiserver = new ApiServer({ port: defaultPort }) 75 | }) 76 | afterEach(function () { 77 | apiserver.close() 78 | }) 79 | it('should listen to the port ' + defaultPort + ' by default', function (done) { 80 | apiserver.listen(function () { 81 | jsonreq.get('http://localhost:' + defaultPort, done) 82 | }) 83 | }) 84 | it('should listen to the port ' + defaultPort + ' by default without providing a callback', function (done) { 85 | apiserver.server.once('listening', function () { 86 | jsonreq.get('http://localhost:' + defaultPort, done) 87 | }) 88 | apiserver.listen() 89 | }) 90 | it('should listen to custom ports', function (done) { 91 | apiserver.listen(8000, function () { 92 | jsonreq.get('http://localhost:8000', done) 93 | }) 94 | }) 95 | it('should listen to custom ports as string', function (done) { 96 | apiserver.listen('8000', function () { 97 | jsonreq.get('http://localhost:8000', done) 98 | }) 99 | }) 100 | it('should listen to custom ports without providing a callback', function (done) { 101 | apiserver.server.once('listening', function () { 102 | jsonreq.get('http://localhost:8000', done) 103 | }) 104 | apiserver.listen(8000) 105 | }) 106 | it('should listen to custom ports as string without providing a callback', function (done) { 107 | apiserver.server.once('listening', function () { 108 | jsonreq.get('http://localhost:8000', done) 109 | }) 110 | apiserver.listen('8000') 111 | }) 112 | it('should listen to custom hostname', function (done) { 113 | apiserver.listen('localhost', function () { 114 | jsonreq.get('http://localhost:' + defaultPort, done) 115 | }) 116 | }) 117 | it('should listen to custom hostname without providing a callback', function (done) { 118 | apiserver.server.once('listening', function () { 119 | jsonreq.get('http://localhost:' + defaultPort, done) 120 | }) 121 | apiserver.listen('localhost') 122 | }) 123 | it('should listen to custom hostname and custom port', function (done) { 124 | apiserver.listen(customPort, 'localhost', function () { 125 | jsonreq.get('http://localhost:' + customPort, done) 126 | }) 127 | }) 128 | it('should listen to custom hostname and custom port as string', function (done) { 129 | apiserver.listen(customPort.toString(), 'localhost', function () { 130 | jsonreq.get('http://localhost:' + customPort, done) 131 | }) 132 | }) 133 | it('should listen to custom ports and hostname without providing a callback', function (done) { 134 | apiserver.server.once('listening', function () { 135 | jsonreq.get('http://localhost:' + customPort, done) 136 | }) 137 | apiserver.listen(customPort, 'localhost') 138 | }) 139 | it('should listen to custom ports as string and hostname without providing a callback', function (done) { 140 | apiserver.server.once('listening', function () { 141 | jsonreq.get('http://localhost:' + customPort, done) 142 | }) 143 | apiserver.listen(customPort.toString(), 'localhost') 144 | }) 145 | }) 146 | describe('#use()', function () { 147 | it('should correctly store middleware', function () { 148 | var middleware = function () {} 149 | var route = /.+/ 150 | apiserver = new ApiServer() 151 | apiserver.use(route, middleware) 152 | apiserver.middlewareList.should.have.length(1) 153 | apiserver.middlewareList[0].should.eql({ 154 | route: route, 155 | handle: middleware 156 | }) 157 | }) 158 | it('should correctly store middleware / default params', function () { 159 | var middleware = function () {} 160 | apiserver = new ApiServer() 161 | apiserver.use(middleware) 162 | apiserver.middlewareList.should.have.length(1) 163 | apiserver.middlewareList[0].route.toString().should.equal('/./') 164 | apiserver.middlewareList[0].handle.should.equal(middleware) 165 | }) 166 | it('should trigger router.update', function (done) { 167 | apiserver = new ApiServer({ router: { update: done.bind(null, null) } }) 168 | apiserver.use(/./, { hadle: function () {} }) 169 | }) 170 | }) 171 | describe('#addModule()', function () { 172 | it('should correctly store modules / object', function () { 173 | apiserver = new ApiServer() 174 | apiserver.addModule('v1', 'module_name', objectModule) 175 | apiserver.activeApiModules.should.have.property('v1') 176 | ;['successApi','errorApi','get'].forEach(function (method) { 177 | apiserver.activeApiModules['v1']['module_name'][method].should.be.instanceof(Function) 178 | }) 179 | ;['timeout'].forEach(function (method) { 180 | apiserver.activeApiModules['v1']['module_name'][method].get.should.be.instanceof(Function) 181 | }) 182 | }) 183 | it('should correctly store modules / class', function () { 184 | apiserver = new ApiServer() 185 | apiserver.addModule('v1', 'module_name', classModule) 186 | apiserver.activeApiModules.should.have.property('v1') 187 | ;['successApi','errorApi','get'].forEach(function (method) { 188 | apiserver.activeApiModules['v1']['module_name'][method].should.be.instanceof(Function) 189 | }) 190 | ;['timeout'].forEach(function (method) { 191 | apiserver.activeApiModules['v1']['module_name'][method].get.should.be.instanceof(Function) 192 | }) 193 | }) 194 | it('should trigger router.update', function (done) { 195 | apiserver = new ApiServer({ router: { update: done.bind(null, null) } }) 196 | apiserver.addModule('v1', 'module_name', objectModule) 197 | }) 198 | }) 199 | describe('Events', function () { 200 | before(function (done) { 201 | apiserver = new ApiServer({ timeout: 300 }) 202 | apiserver.addModule('v1', 'test', objectModule) 203 | apiserver.listen(defaultPort, done) 204 | }) 205 | after(function () { 206 | apiserver.close() 207 | }) 208 | it('should emit requestEnd event', function (done) { 209 | apiserver.once('requestEnd', function (url, time) { 210 | should.exist(url) 211 | should.exist(time) 212 | url.should.be.equal('/v1/test/am_a_public_api') 213 | time.should.be.a('number') 214 | done() 215 | }) 216 | jsonreq.get('http://localhost:' + defaultPort + '/v1/test/am_a_public_api') 217 | }) 218 | it('should emit error event on exceptions', function (done) { 219 | apiserver.once('error', function (url, err) { 220 | should.exist(err) 221 | should.exist(url) 222 | err.should.be.instanceof(Error) 223 | url.should.be.equal('/v1/test/error_api') 224 | done() 225 | }) 226 | jsonreq.get('http://localhost:' + defaultPort + '/v1/test/error_api') 227 | }) 228 | it('should emit requestEnd event on exceptions', function (done) { 229 | apiserver.once('requestEnd', function (url, time) { 230 | should.exist(url) 231 | should.exist(time) 232 | url.should.be.equal('/v1/test/am_a_public_api') 233 | time.should.be.a('number') 234 | done() 235 | }) 236 | jsonreq.get('http://localhost:' + defaultPort + '/v1/test/am_a_public_api') 237 | }) 238 | it('should emit timeout event on timeout', function (done) { 239 | apiserver.once('timeout', function (url) { 240 | should.exist(url) 241 | url.should.be.equal('/v1/test/timeout') 242 | done() 243 | }) 244 | jsonreq.get('http://localhost:' + defaultPort + '/v1/test/timeout', function () {}) 245 | }) 246 | }) 247 | describe('Routing', function () { 248 | before(function (done) { 249 | apiserver = new ApiServer() 250 | apiserver.addModule('v1', 'test', objectModule) 251 | apiserver.listen(defaultPort, done) 252 | }) 253 | after(function () { 254 | apiserver.close() 255 | }) 256 | it('should success calling an API', function (done) { 257 | jsonreq.get('http://localhost:' + defaultPort + '/v1/test/success_api', function (err, response, body) { 258 | response.statusCode.should.be.equal(200) 259 | body.should.be.eql({ success: true }) 260 | done(err) 261 | }) 262 | }) 263 | it('should fail calling a throwing exception API', function (done) { 264 | apiserver.once('error', function () {}) //shut up mocha! 265 | jsonreq.get('http://localhost:' + defaultPort + '/v1/test/error_api', function (err, response, body) { 266 | response.statusCode.should.be.equal(500) 267 | body.should.have.property('success') 268 | body.success.should.be.equal(false) 269 | done(err) 270 | }) 271 | }) 272 | }) 273 | describe('Querystring', function () { 274 | before(function (done) { 275 | apiserver = new ApiServer() 276 | apiserver.addModule('v1', 'test', objectModule) 277 | apiserver.listen(defaultPort, done) 278 | }) 279 | after(function () { 280 | apiserver.close() 281 | }) 282 | it('should give back my querystring / GET', function (done) { 283 | jsonreq.get({ 284 | uri: 'http://localhost:' + defaultPort + '/v1/test/get', 285 | qs: { foo: 'bar', bar: 'foo' } 286 | }, function (err, response, body) { 287 | body.should.be.eql({ foo: 'bar', bar: 'foo' }) 288 | done(err) 289 | }) 290 | }) 291 | it('should give back my querystring / POST', function (done) { 292 | jsonreq.post({ 293 | uri: 'http://localhost:' + defaultPort + '/v1/test/get', 294 | qs: { foo: 'bar', bar: 'foo' } 295 | }, function (err, response, body) { 296 | body.should.be.eql({ foo: 'bar', bar: 'foo' }) 297 | done(err) 298 | }) 299 | }) 300 | }) 301 | describe('Timeout', function () { 302 | before(function (done) { 303 | apiserver = new ApiServer({ timeout: 200 }) 304 | apiserver.addModule('v1', 'test', objectModule) 305 | apiserver.listen(defaultPort, done) 306 | }) 307 | after(function () { 308 | apiserver.close() 309 | }) 310 | it('should close the response following the timeout option', function (done) { 311 | jsonreq.get({ 312 | uri: 'http://localhost:' + defaultPort + '/v1/test/timeout', 313 | qs: { foo: 'bar', bar: 'foo' } 314 | }, function (err, response, body) { 315 | response.statusCode.should.be.equal(408) 316 | done() 317 | }) 318 | }) 319 | }) 320 | }) -------------------------------------------------------------------------------- /test/fixtures/class-module.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | 3 | } 4 | 5 | module.exports.prototype = { 6 | successApi: function (request, response) { 7 | response.serveJSON({ success: true }) 8 | }, 9 | errorApi: function (request, response) { 10 | throw new Error('Aww') 11 | }, 12 | 'get': function (request, response) { 13 | response.serveJSON(request.querystring) 14 | }, 15 | 'timeout': { 16 | get: function (request, response) { 17 | // trigger timout 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /test/fixtures/httpauth-module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | amAPrivateApi: function (request, response) { 3 | response.serveJSON({ success: true }) 4 | } 5 | } -------------------------------------------------------------------------------- /test/fixtures/multipart-module.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | module.exports = { 4 | 'multipart': function (request, response) { 5 | request.resume() 6 | var fields = Object.create(null) 7 | request.form.on('field', function (name, value) { 8 | fields[name] = value 9 | }) 10 | request.form.on('file', function (name, file) { 11 | fields[name] = fs.readFileSync(file.path, 'utf8') 12 | }) 13 | request.form.once('end', function () { 14 | response.serveJSON(fields) 15 | }) 16 | }, 17 | 'multipartEnd': function (request, response) { 18 | request.resume() 19 | request.form.once('end', function () { 20 | response.serveJSON({ fields: request.body, files: Object.keys(request.files), err: request.parseError }) 21 | }) 22 | }, 23 | 'skip': function (request, response) { 24 | request.resume() 25 | request.once('end', function () { 26 | response.serveJSON(request.form) 27 | }) 28 | } 29 | } -------------------------------------------------------------------------------- /test/fixtures/object-module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | successApi: function (request, response) { 3 | response.serveJSON({ success: true }) 4 | }, 5 | errorApi: function (request, response) { 6 | throw new Error('Aww') 7 | }, 8 | 'get': function (request, response) { 9 | response.serveJSON(request.querystring) 10 | }, 11 | 'timeout': { 12 | get: function (request, response) { 13 | // trigger timout 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /test/fixtures/payload-module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'json': function (request, response) { 3 | request.resume() 4 | request.once('end', function () { 5 | response.serveJSON(request.body) 6 | }) 7 | }, 8 | 'form': function (request, response) { 9 | request.resume() 10 | request.once('end', function () { 11 | response.serveJSON(request.body) 12 | }) 13 | }, 14 | 'skip': function (request, response) { 15 | request.resume() 16 | request.once('end', function () { 17 | response.serveJSON(request.body) 18 | }) 19 | }, 20 | 'parse_error': function (request, response) { 21 | request.resume() 22 | request.once('end', function () { 23 | response.serveJSON(request.parseError) 24 | }) 25 | }, 26 | 'empty': function (request, response) { 27 | request.resume() 28 | request.once('end', function () { 29 | response.serveJSON(request.body) 30 | }) 31 | } 32 | } -------------------------------------------------------------------------------- /test/middleware/httpauth-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | jsonreq = require('request').defaults({ json: true }), 3 | ApiServer = require('../../'), 4 | testModule = require('../fixtures/httpauth-module') 5 | 6 | var apiserver 7 | var defaultPort = 8080 8 | var credentials = ['foo:bar'] 9 | 10 | describe('middleware/HTTPAuth', function () { 11 | before(function (done) { 12 | apiserver = new ApiServer() 13 | apiserver.addModule('v1', 'auth', testModule) 14 | apiserver.use(/private/, ApiServer.httpAuth({ credentials: credentials, encode: true })) 15 | apiserver.listen(done) 16 | }) 17 | after(function () { 18 | apiserver.close() 19 | }) 20 | it('should use a custom realm if provided', function () { 21 | var realm = 'Stay Away.' 22 | var options = { realm: realm } 23 | var httpAuth = ApiServer.httpAuth(options) 24 | options.should.have.property('realm') 25 | options.realm.should.be.eql(realm) 26 | }) 27 | it('should encode credentials if required', function () { 28 | credentials.should.be.eql(['Basic Zm9vOmJhcg==']) 29 | }) 30 | it('should ask for auth', function (done) { 31 | jsonreq.get('http://localhost:' + defaultPort + '/v1/auth/am_a_private_api', function (err, response, body) { 32 | response.statusCode.should.be.equal(401) 33 | response.headers.should.have.property('www-authenticate') 34 | done(err) 35 | }) 36 | }) 37 | it('should accept credentials', function (done) { 38 | jsonreq.get({ 39 | headers: { 'authorization': 'Basic ' + new Buffer('foo:bar', 'utf8').toString('base64') }, 40 | uri: 'http://localhost:' + defaultPort + '/v1/auth/am_a_private_api' 41 | }, function (err, response, body) { 42 | response.statusCode.should.be.equal(200) 43 | body.should.be.eql({ success: true }) 44 | done(err) 45 | }) 46 | }) 47 | it('should refuse credentials', function (done) { 48 | jsonreq.get({ 49 | headers: { 'authorization': 'Basic ' + new Buffer('foo:wrong', 'utf8').toString('base64') }, 50 | uri: 'http://localhost:' + defaultPort + '/v1/auth/am_a_private_api' 51 | }, function (err, response, body) { 52 | response.statusCode.should.be.equal(401) 53 | response.headers.should.have.property('www-authenticate') 54 | done(err) 55 | }) 56 | }) 57 | }) -------------------------------------------------------------------------------- /test/middleware/multipart-parser-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | jsonreq = require('request'), 3 | ApiServer = require('../../'), 4 | testModule = require('../fixtures/multipart-module') 5 | 6 | var apiserver 7 | var defaultPort = 9000 8 | var customPort = 8080 9 | var requestBody = '--AaB03x\r\n'+ 10 | 'content-disposition: form-data; name="foo"\r\n'+ 11 | '\r\n'+ 12 | 'bar\r\n'+ 13 | '--AaB03x\r\n'+ 14 | 'content-disposition: form-data; name="bar"\r\n'+ 15 | '\r\n'+ 16 | 'foo\r\n'+ 17 | '--AaB03x\r\n'+ 18 | 'content-disposition: form-data; name="quote"; filename="quote.txt"\r\n'+ 19 | 'Content-Type: text/plain\r\n'+ 20 | '\r\n'+ 21 | 'no detail is too small\r\n'+ 22 | '--AaB03x--' 23 | 24 | describe('middleware/MultipartParser', function () { 25 | describe('should skip', function () { 26 | before(function (done) { 27 | apiserver = new ApiServer() 28 | apiserver.addModule('v1', 'test', testModule) 29 | apiserver.use(/./, ApiServer.multipartParser()) 30 | apiserver.listen(defaultPort, done) 31 | }) 32 | after(function () { 33 | apiserver.close() 34 | }) 35 | ;['GET','HEAD','DELETE', 'TRACE',/* 'CONNECT',*/ 'PATCH'].forEach(function (httpMethod) { 36 | it(httpMethod, function (done) { 37 | jsonreq({ 38 | method: httpMethod, 39 | uri: 'http://localhost:' + defaultPort + '/v1/test/skip', 40 | json: false, 41 | }, function (err, response, body) { 42 | should.not.exist(body) 43 | done(err) 44 | }) 45 | }) 46 | }) 47 | it('!multipart/form-data', function (done) { 48 | jsonreq.post({ 49 | uri: 'http://localhost:' + defaultPort + '/v1/test/skip', 50 | headers: { 'content-type': 'multipart/x-foo-bar; boundary=AaB03x' }, 51 | body: 52 | '--AaB03x\r\n'+ 53 | 'content-disposition: form-data; name="foo"\r\n'+ 54 | '\r\n'+ 55 | 'bar\r\n'+ 56 | '--AaB03x\r\n'+ 57 | 'content-disposition: form-data; name="bar"\r\n'+ 58 | '\r\n'+ 59 | 'foo\r\n'+ 60 | '--AaB03x\r\n'+ 61 | 'content-disposition: form-data; name="pics"; filename="file.txt"\r\n'+ 62 | 'Content-Type: text/plain\r\n'+ 63 | '\r\n'+ 64 | 'no detail is too small\r\n'+ 65 | '--AaB03x--' 66 | }, function (err, response, body) { 67 | should.not.exist(body) 68 | done(err) 69 | }) 70 | }) 71 | }) 72 | describe('should handle', function () { 73 | before(function (done) { 74 | apiserver = new ApiServer() 75 | apiserver.addModule('v1', 'test', testModule) 76 | apiserver.use(/./, ApiServer.multipartParser()) 77 | apiserver.listen(defaultPort, done) 78 | }) 79 | after(function () { 80 | apiserver.close() 81 | }) 82 | it('multipart/form-data', function (done) { 83 | jsonreq.post({ 84 | uri: 'http://localhost:' + defaultPort + '/v1/test/multipart', 85 | headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' }, 86 | body: requestBody 87 | }, function (err, response, body) { 88 | body = JSON.parse(body) 89 | body.should.be.eql({ foo: 'bar', bar: 'foo', quote: 'no detail is too small' }) 90 | done(err) 91 | }) 92 | }) 93 | }) 94 | describe('end listener', function () { 95 | before(function (done) { 96 | apiserver = new ApiServer() 97 | apiserver.addModule('v1', 'test', testModule) 98 | apiserver.use(/./, ApiServer.multipartParser()) 99 | apiserver.listen(defaultPort, done) 100 | }) 101 | after(function () { 102 | apiserver.close() 103 | }) 104 | it('should attach files and fields to the request', function (done) { 105 | jsonreq.post({ 106 | uri: 'http://localhost:' + defaultPort + '/v1/test/multipart_end', 107 | headers: { 'content-type': 'multipart/form-data; boundary=AaB03x' }, 108 | body: requestBody 109 | }, function (err, response, body) { 110 | body = JSON.parse(body) 111 | body.fields.should.be.eql({ foo: 'bar', bar: 'foo' }) 112 | body.files.should.be.eql(['quote']) 113 | should.not.exist(body.parseError) 114 | done(err) 115 | }) 116 | }) 117 | }) 118 | }) -------------------------------------------------------------------------------- /test/middleware/payload-parser-test.js: -------------------------------------------------------------------------------- 1 | var should = require('should'), 2 | Assertion = should.Assertion, 3 | request = require('request'), 4 | jsonreq = require('request').defaults({ json: true }), 5 | ApiServer = require('../../'), 6 | testModule = require('../fixtures/payload-module') 7 | 8 | var apiserver 9 | var defaultPort = 9000 10 | var customPort = 8080 11 | 12 | describe('middleware/PayloadParser', function () { 13 | describe('should skip', function () { 14 | before(function (done) { 15 | apiserver = new ApiServer() 16 | apiserver.addModule('v1', 'test', testModule) 17 | apiserver.use(/./, ApiServer.payloadParser()) 18 | apiserver.listen(defaultPort, done) 19 | }) 20 | after(function () { 21 | apiserver.close() 22 | }) 23 | ;['GET','HEAD','DELETE', 'TRACE',/* 'CONNECT',*/ 'PATCH'].forEach(function (httpMethod) { 24 | it(httpMethod, function (done) { 25 | jsonreq({ 26 | method: httpMethod, 27 | uri: 'http://localhost:' + defaultPort + '/v1/test/skip', 28 | json: false, 29 | }, function (err, response, body) { 30 | should.not.exist(body) 31 | done(err) 32 | }) 33 | }) 34 | }) 35 | }) 36 | describe('should attach parseError', function () { 37 | before(function (done) { 38 | apiserver = new ApiServer() 39 | apiserver.addModule('v1', 'test', testModule) 40 | apiserver.use(/./, ApiServer.payloadParser()) 41 | apiserver.listen(defaultPort, done) 42 | }) 43 | after(function () { 44 | apiserver.close() 45 | }) 46 | it('on application/json', function (done) { 47 | jsonreq.post({ 48 | uri: 'http://localhost:' + defaultPort + '/v1/test/parse_error', 49 | headers: { 'content-type': 'application/json' }, 50 | json: false, 51 | body: '=:' 52 | }, function (err, response, body) { 53 | should.exist(body) 54 | done(err) 55 | }) 56 | }) 57 | }) 58 | describe('should handle', function () { 59 | before(function (done) { 60 | apiserver = new ApiServer() 61 | apiserver.addModule('v1', 'test', testModule) 62 | apiserver.use(/./, ApiServer.payloadParser()) 63 | apiserver.listen(defaultPort, done) 64 | }) 65 | after(function () { 66 | apiserver.close() 67 | }) 68 | it('application/x-www-form-urlencoded', function (done) { 69 | jsonreq.post({ 70 | uri: 'http://localhost:' + defaultPort + '/v1/test/form', 71 | json: false, 72 | form: { foo: 'bar', bar: 'foo' } 73 | }, function (err, response, body) { 74 | body = JSON.parse(body) 75 | body.should.be.eql({ foo: 'bar', bar: 'foo' }) 76 | done(err) 77 | }) 78 | }) 79 | it('application/json', function (done) { 80 | jsonreq.post({ 81 | uri: 'http://localhost:' + defaultPort + '/v1/test/json', 82 | json: { foo: 'bar', bar: 'foo' } 83 | }, function (err, response, body) { 84 | body.should.be.eql({ foo: 'bar', bar: 'foo' }) 85 | done(err) 86 | }) 87 | }) 88 | it('missing content-type', function (done) { 89 | request.post({ 90 | uri: 'http://localhost:' + defaultPort + '/v1/test/empty' 91 | }, function (err, response, body) { 92 | should.exist(response) 93 | done(err) 94 | }) 95 | }) 96 | }) 97 | }) -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --slow 1000 --------------------------------------------------------------------------------