├── smart.json ├── package.js ├── LICENSE.txt ├── CHANGELOG.md ├── README.md └── collectionapi.js /smart.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "collection-api", 3 | "description": "Perform CRUD operations on Collections over a RESTful API", 4 | "homepage": "https://github.com/crazytoad/meteor-collectionapi", 5 | "author": "Todd Colton", 6 | "version": "0.1.15", 7 | "git": "https://github.com/crazytoad/meteor-collectionapi.git" 8 | } -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: "CRUD operations on Collections via HTTP/HTTPS API" 3 | }); 4 | 5 | Package.on_use(function (api, where) { 6 | api.use('routepolicy', 'server'); 7 | api.use('webapp', 'server'); 8 | api.add_files("collectionapi.js", "server"); 9 | api.export("CollectionAPI", "server"); 10 | }); -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | =============================================== 2 | CollectionAPI is licensed under the MIT License 3 | =============================================== 4 | 5 | Copyright (c) 2012 - Todd Colton 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.1.15 (Requires Meteor v0.6.5+) 2 | #### released on 2013-08-17 3 | 4 | * Fixed issue #23 - Now compatible with the new package requirements in Meteor v0.6.5 5 | 6 | 7 | ## v0.1.14 (Requires Meteor v0.6.0+) 8 | #### released on 2013-05-28 9 | 10 | * Calculate the correct content length in utf8 responses (pull request #14 - Thanks, Szczyp) 11 | 12 | * Added a callback that is called right before the collection is modified/queried. (pull request #3 - Thanks, andreasgl) 13 | 14 | 15 | ## v0.1.13 (Requires Meteor v0.6.0+) 16 | #### released on 2013-04-09 17 | 18 | * Updated to run under Meteor 0.6.0+ (Thanks, DracoLi) 19 | 20 | 21 | ## v0.1.12 22 | #### released on 2012-11-09 23 | 24 | * No code changes - changing version number format for Meteorite / Atmosphere 25 | 26 | 27 | ## v0.12 28 | #### released on 2012-08-13 29 | 30 | * Fixed issue #2 - Rest method restrictions (Thanks, andreasgl) 31 | 32 | 33 | ## v0.11 34 | #### released on 2012-06-26 35 | 36 | * Added two configuration options, which changes the default behavior: 37 | 38 | * `standAlone` - run the Collection API server as a separate HTTP(S) process. Previously, this was always the case by default. `standAlone` is now set to `false` by default so it now runs within the same web server object as Meteor. This allows the API to be accessed when deployed to Meteor.com servers. If you wish to have the old behavior, set `standAlone` to `true`. 39 | 40 | * `apiPath` - access the Collection API using this prefix. Default is set to `collectionapi`, so you'd access the 'players' collection as '/collectionapi/players'. Required to be set when `standAlone` is set to `true`. 41 | 42 | 43 | ## v0.10 44 | #### released on 2012-06-12 45 | 46 | * Fixed concurrency issues with the API (fixes "Error: Can't set headers after they are sent.") 47 | 48 | * Refactored code for easier maintenance 49 | 50 | 51 | ## v0.00 (no version number) 52 | #### released on 2012-05-22 53 | 54 | * Initial release 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NOTE: This repo is no longer maintained. 2 | # If you run into any issues or want the latest version, please visit https://github.com/xcv58/meteor-collectionapi 3 | 4 | 5 | Collection API 6 | ======== 7 | 8 | Easily perform [CRUD](http://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations on Meteor Collections over HTTP/HTTPS from outside of the Meteor client or server environment. 9 | 10 | 11 | Current version: 0.1.15 ***(Requires Meteor v0.6.5+)*** 12 | 13 | 14 | Installation 15 | ------- 16 | 17 | ### With [Metorite](https://github.com/oortcloud/meteorite) 18 | 19 | $ mrt add collection-api 20 | 21 | It's that easy! Be sure to check out other cool packages over at [Atmosphere](https://atmosphere.meteor.com/). 22 | 23 | 24 | Quick Usage 25 | ------- 26 | 27 | ```javascript 28 | Players = new Meteor.Collection("players"); 29 | 30 | if (Meteor.isServer) { 31 | Meteor.startup(function () { 32 | 33 | // All values listed below are default 34 | collectionApi = new CollectionAPI({ 35 | authToken: undefined, // Require this string to be passed in on each request 36 | apiPath: 'collectionapi', // API path prefix 37 | standAlone: false, // Run as a stand-alone HTTP(S) server 38 | sslEnabled: false, // Disable/Enable SSL (stand-alone only) 39 | listenPort: 3005, // Port to listen to (stand-alone only) 40 | listenHost: undefined, // Host to bind to (stand-alone only) 41 | privateKeyFile: 'privatekey.pem', // SSL private key file (only used if SSL is enabled) 42 | certificateFile: 'certificate.pem' // SSL certificate key file (only used if SSL is enabled) 43 | }); 44 | 45 | // Add the collection Players to the API "/players" path 46 | collectionApi.addCollection(Players, 'players', { 47 | // All values listed below are default 48 | authToken: undefined, // Require this string to be passed in on each request 49 | methods: ['POST','GET','PUT','DELETE'], // Allow creating, reading, updating, and deleting 50 | before: { // This methods, if defined, will be called before the POST/GET/PUT/DELETE actions are performed on the collection. If the function returns false the action will be canceled, if you return true the action will take place. 51 | POST: undefined, // function(obj) {return true/false;}, 52 | GET: undefined, // function(collectionID, objs) {return true/false;}, 53 | PUT: undefined, //function(collectionID, obj, newValues) {return true/false;}, 54 | DELETE: undefined, //function(collectionID, obj) {return true/false;} 55 | } 56 | }); 57 | 58 | // Starts the API server 59 | collectionApi.start(); 60 | }); 61 | } 62 | ``` 63 | 64 | Using the API 65 | ------- 66 | 67 | If you specify an `authToken` it must be passed in either the `X-Auth-Token` request header or as an `auth-token` param in the query string. 68 | 69 | 70 | ### API Usage Example 71 | 72 | ```javascript 73 | Players = new Meteor.Collection("players"); 74 | 75 | if (Meteor.isServer) { 76 | Meteor.startup(function () { 77 | collectionApi = new CollectionAPI({ authToken: '97f0ad9e24ca5e0408a269748d7fe0a0' }); 78 | collectionApi.addCollection(Players, 'players'); 79 | collectionApi.start(); 80 | }); 81 | } 82 | ``` 83 | 84 | Get all of the player records: 85 | 86 | $ curl -H "X-Auth-Token: 97f0ad9e24ca5e0408a269748d7fe0a0" http://localhost:3000/collectionapi/players 87 | 88 | Get an individual record: 89 | 90 | $ curl -H "X-Auth-Token: 97f0ad9e24ca5e0408a269748d7fe0a0" http://localhost:3000/collectionapi/players/c4acddd1-a504-4212-9534-adca17af4885 91 | 92 | Create a record: 93 | 94 | $ curl -H "X-Auth-Token: 97f0ad9e24ca5e0408a269748d7fe0a0" -d "{\"name\": \"John Smith\"}" http://localhost:3000/collectionapi/players 95 | 96 | Update a record: 97 | 98 | $ curl -H "X-Auth-Token: 97f0ad9e24ca5e0408a269748d7fe0a0" -X PUT -d "{\"\$set\":{\"gender\":\"male\"}}" http://localhost:3000/collectionapi/players/c4acddd1-a504-4212-9534-adca17af4885 99 | 100 | Delete a record: 101 | 102 | $ curl -H "X-Auth-Token: 97f0ad9e24ca5e0408a269748d7fe0a0" -X DELETE http://localhost:3000/collectionapi/players/c4acddd1-a504-4212-9534-adca17af4885 103 | -------------------------------------------------------------------------------- /collectionapi.js: -------------------------------------------------------------------------------- 1 | CollectionAPI = function(options) { 2 | var self = this; 3 | 4 | self.version = '0.1.15'; 5 | self._url = Npm.require('url'); 6 | self._querystring = Npm.require('querystring'); 7 | self._fiber = Npm.require('fibers'); 8 | self._collections = {}; 9 | self.options = { 10 | apiPath: 'collectionapi', 11 | standAlone: false, 12 | sslEnabled: false, 13 | listenPort: 3005, 14 | listenHost: undefined, 15 | authToken: undefined, 16 | privateKeyFile: 'privatekey.pem', 17 | certificateFile: 'certificate.pem' 18 | }; 19 | _.extend(self.options, options || {}); 20 | }; 21 | 22 | CollectionAPI.prototype.addCollection = function(collection, path, options) { 23 | var self = this; 24 | 25 | var collectionOptions = {}; 26 | collectionOptions[path] = { 27 | collection: collection, 28 | options: options || {} 29 | }; 30 | _.extend(self._collections, collectionOptions); 31 | }; 32 | 33 | CollectionAPI.prototype.start = function() { 34 | var self = this; 35 | var httpServer, httpOptions, scheme; 36 | 37 | var startupMessage = 'Collection API v' + self.version; 38 | 39 | if (self.options.standAlone === true) { 40 | if (self.options.sslEnabled === true) { 41 | scheme = 'https://'; 42 | httpServer = Npm.require('https'); 43 | var fs = Npm.require('fs'); 44 | 45 | httpOptions = { 46 | key: fs.readFileSync(self.options.privateKeyFile), 47 | cert: fs.readFileSync(self.options.certificateFile) 48 | }; 49 | } else { 50 | scheme = 'http://'; 51 | httpServer = Npm.require('http'); 52 | } 53 | 54 | self._httpServer = httpServer.createServer(httpOptions); 55 | self._httpServer.addListener('request', function(request, response) { new CollectionAPI._requestListener(self, request, response); }); 56 | self._httpServer.listen(self.options.listenPort, self.options.listenHost); 57 | console.log(startupMessage + ' running as a stand-alone server on ' + scheme + (self.options.listenHost || 'localhost') + ':' + self.options.listenPort + '/' + (self.options.apiPath || '')); 58 | } else { 59 | 60 | RoutePolicy.declare('/' + this.options.apiPath + '/', 'network'); 61 | 62 | WebApp.connectHandlers.use(function(req, res, next) { 63 | if (req.url.split('/')[1] !== self.options.apiPath) { 64 | next(); 65 | return; 66 | } 67 | self._fiber(function () { 68 | new CollectionAPI._requestListener(self, req, res); 69 | }).run(); 70 | }); 71 | 72 | console.log(startupMessage + ' running at /' + this.options.apiPath); 73 | } 74 | }; 75 | 76 | CollectionAPI.prototype._collectionOptions = function(requestPath) { 77 | var self = this; 78 | return self._collections[requestPath.collectionPath] ? self._collections[requestPath.collectionPath].options : undefined; 79 | }; 80 | 81 | CollectionAPI._requestListener = function (server, request, response) { 82 | var self = this; 83 | 84 | self._server = server; 85 | self._request = request; 86 | self._response = response; 87 | 88 | self._requestUrl = self._server._url.parse(self._request.url); 89 | 90 | // Check for the X-Auth-Token header or auth-token in the query string 91 | self._requestAuthToken = self._request.headers['x-auth-token'] ? self._request.headers['x-auth-token'] : self._server._querystring.parse(self._requestUrl.query)['auth-token']; 92 | 93 | var requestPath; 94 | if (self._server.options.standAlone === true && ! self._server.options.apiPath) { 95 | requestPath = self._requestUrl.pathname.split('/').slice(1,3); 96 | } else { 97 | requestPath = self._requestUrl.pathname.split('/').slice(2,4); 98 | } 99 | 100 | self._requestPath = { 101 | collectionPath: requestPath[0], 102 | collectionId: requestPath[1] 103 | }; 104 | 105 | self._requestCollection = self._server._collections[self._requestPath.collectionPath] ? self._server._collections[self._requestPath.collectionPath].collection : undefined; 106 | 107 | if (!self._authenticate()) { 108 | return self._unauthorizedResponse('Invalid/Missing Auth Token'); 109 | } 110 | 111 | if (!self._requestCollection) { 112 | return self._notFoundResponse('Collection Object Not Found'); 113 | } 114 | 115 | return self._handleRequest(); 116 | }; 117 | 118 | CollectionAPI._requestListener.prototype._authenticate = function() { 119 | var self = this; 120 | var collectionOptions = self._server._collectionOptions(self._requestPath); 121 | 122 | // Check the collection's auth token 123 | if (collectionOptions && collectionOptions.authToken) { 124 | return self._requestAuthToken === collectionOptions.authToken; 125 | } 126 | 127 | // Check the global auth token 128 | if (self._server.options.authToken) { 129 | return self._requestAuthToken === self._server.options.authToken; 130 | } 131 | 132 | return true; 133 | }; 134 | 135 | CollectionAPI._requestListener.prototype._handleRequest = function() { 136 | var self = this; 137 | 138 | if (!self._requestMethodAllowed(self._request.method)) { 139 | return self._notSupportedResponse(); 140 | } 141 | 142 | switch (self._request.method) { 143 | case 'GET': 144 | return self._getRequest(); 145 | case 'POST': 146 | return self._postRequest(); 147 | case 'PUT': 148 | return self._putRequest(); 149 | case 'DELETE': 150 | return self._deleteRequest(); 151 | default: 152 | return self._notSupportedResponse(); 153 | } 154 | }; 155 | 156 | CollectionAPI._requestListener.prototype._requestMethodAllowed = function (method) { 157 | var self = this; 158 | var collectionOptions = self._server._collectionOptions(self._requestPath); 159 | 160 | if (collectionOptions && collectionOptions.methods) { 161 | return _.indexOf(collectionOptions.methods, method) >= 0; 162 | } 163 | 164 | return true; 165 | }; 166 | 167 | CollectionAPI._requestListener.prototype._beforeHandling = function (method) { 168 | var self = this; 169 | var collectionOptions = self._server._collectionOptions(self._requestPath); 170 | 171 | if (collectionOptions && collectionOptions.before && collectionOptions.before[method] && _.isFunction(collectionOptions.before[method])) { 172 | return collectionOptions.before[method].apply(self, _.rest(arguments)); 173 | } 174 | 175 | return true; 176 | } 177 | 178 | CollectionAPI._requestListener.prototype._getRequest = function(fromPutRequest) { 179 | var self = this; 180 | 181 | self._server._fiber(function() { 182 | 183 | try { 184 | // TODO: A better way to do this? 185 | var collection_result = self._requestPath.collectionId !== undefined ? self._requestCollection.find(self._requestPath.collectionId) : self._requestCollection.find(); 186 | 187 | var records = []; 188 | collection_result.forEach(function(record) { 189 | records.push(record); 190 | }); 191 | 192 | if(!self._beforeHandling('GET', self._requestPath.collectionId, records)) { 193 | if (fromPutRequest) { 194 | return records.length ? self._noContentResponse() : self._notFoundResponse('No Record(s) Found'); 195 | } 196 | return self._rejectedResponse("Could not get that collection/object."); 197 | } 198 | 199 | records = _.compact(records); 200 | 201 | if (records.length === 0) { 202 | return self._notFoundResponse('No Record(s) Found'); 203 | } 204 | 205 | return self._okResponse(JSON.stringify(records)); 206 | 207 | } catch (e) { 208 | return self._internalServerErrorResponse(e); 209 | } 210 | 211 | }).run(); 212 | 213 | }; 214 | 215 | CollectionAPI._requestListener.prototype._putRequest = function() { 216 | var self = this; 217 | 218 | if (! self._requestPath.collectionId) { 219 | return self._notFoundResponse('Missing _id'); 220 | } 221 | 222 | var requestData = ''; 223 | 224 | self._request.on('data', function(chunk) { 225 | requestData += chunk.toString(); 226 | }); 227 | 228 | self._request.on('end', function() { 229 | self._server._fiber(function() { 230 | try { 231 | var obj = JSON.parse(requestData); 232 | 233 | if(!self._beforeHandling('PUT', self._requestPath.collectionId, self._requestCollection.findOne(self._requestPath.collectionId), obj)) { 234 | return self._rejectedResponse("Could not put that object."); 235 | } 236 | self._requestCollection.update(self._requestPath.collectionId, obj); 237 | } catch (e) { 238 | return self._internalServerErrorResponse(e); 239 | } 240 | return self._getRequest('fromPutRequest'); 241 | }).run(); 242 | }); 243 | 244 | }; 245 | 246 | CollectionAPI._requestListener.prototype._deleteRequest = function() { 247 | var self = this; 248 | 249 | if (! self._requestPath.collectionId) { 250 | return self._notFoundResponse('Missing _id'); 251 | } 252 | 253 | self._server._fiber(function() { 254 | try { 255 | if(!self._beforeHandling('DELETE', self._requestPath.collectionId, self._requestCollection.findOne(self._requestPath.collectionId))) { 256 | return self._rejectedResponse("Could not delete that object."); 257 | } 258 | self._requestCollection.remove(self._requestPath.collectionId); 259 | } catch (e) { 260 | return self._internalServerErrorResponse(e); 261 | } 262 | return self._okResponse(''); 263 | }).run(); 264 | }; 265 | 266 | CollectionAPI._requestListener.prototype._postRequest = function() { 267 | var self = this; 268 | var requestData = ''; 269 | 270 | self._request.on('data', function(chunk) { 271 | requestData += chunk.toString(); 272 | }); 273 | 274 | self._request.on('end', function() { 275 | self._server._fiber(function() { 276 | try { 277 | var obj = JSON.parse(requestData); 278 | 279 | if(!self._beforeHandling('POST', obj)) { 280 | return self._rejectedResponse("Could not post that object."); 281 | } 282 | self._requestPath.collectionId = self._requestCollection.insert(obj); 283 | } catch (e) { 284 | return self._internalServerErrorResponse(e); 285 | } 286 | return self._createdResponse(JSON.stringify({_id: self._requestPath.collectionId})); 287 | }).run(); 288 | }); 289 | }; 290 | 291 | CollectionAPI._requestListener.prototype._okResponse = function(body) { 292 | var self = this; 293 | self._sendResponse(200, body); 294 | }; 295 | 296 | CollectionAPI._requestListener.prototype._createdResponse = function(body) { 297 | var self = this; 298 | self._sendResponse(201, body); 299 | }; 300 | 301 | CollectionAPI._requestListener.prototype._noContentResponse = function() { 302 | var self = this; 303 | self._sendResponse(204, ''); 304 | }; 305 | 306 | CollectionAPI._requestListener.prototype._notSupportedResponse = function() { 307 | var self = this; 308 | self._sendResponse(501, ''); 309 | }; 310 | 311 | CollectionAPI._requestListener.prototype._unauthorizedResponse = function(body) { 312 | var self = this; 313 | self._sendResponse(401, JSON.stringify({message: body.toString()})); 314 | }; 315 | 316 | CollectionAPI._requestListener.prototype._notFoundResponse = function(body) { 317 | var self = this; 318 | self._sendResponse(404, JSON.stringify({message: body.toString()})); 319 | }; 320 | 321 | CollectionAPI._requestListener.prototype._rejectedResponse= function(body) { 322 | var self = this; 323 | self._sendResponse(409, JSON.stringify({message: body.toString()})); 324 | }; 325 | 326 | CollectionAPI._requestListener.prototype._internalServerErrorResponse = function(body) { 327 | var self = this; 328 | self._sendResponse(500, JSON.stringify({error: body.toString()})); 329 | }; 330 | 331 | CollectionAPI._requestListener.prototype._sendResponse = function(statusCode, body) { 332 | var self = this; 333 | self._response.statusCode = statusCode; 334 | self._response.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')); 335 | self._response.setHeader('Content-Type', 'application/json'); 336 | self._response.write(body); 337 | self._response.end(); 338 | }; 339 | --------------------------------------------------------------------------------