├── .gitignore ├── .jslintrc ├── README.md ├── gulpfile.js ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules -------------------------------------------------------------------------------- /.jslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent": 2, 3 | "vars": true, 4 | "nomen": true 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keystone Rest API 2 | 3 | This extension for Keystone is intended to create a REST API very easy. Also is prepared to output the Documentation for the created API. The Documentation is based on API Blueprint ( Format 1A8 ). 4 | 5 | ## Features 6 | - Automatic REST API 7 | - API Documentataion 8 | 9 | ## Documentation 10 | 11 | [http://sarriaroman.github.io/Keystone-Rest-API](http://sarriaroman.github.io/Keystone-Rest-API) 12 | 13 | ## Options 14 | 15 | - Model 16 | + rest {Boolean} 17 | 18 | + restOptions {String} 'list show create update delete' 19 | 20 | + restDescription {String} 21 | 22 | - List Object 23 | + restHooks {Object} 24 | 25 | ``` 26 | { 27 | list: [listMiddleware], 28 | show: [showMiddleware], 29 | create: [createMiddleware], 30 | update: [updateMiddleware], 31 | delete: [deleteMiddleware] 32 | } 33 | ``` 34 | 35 | - Fields 36 | + restSelected {Boolean} 37 | 38 | + restEditable {Boolean} 39 | 40 | ## Usage 41 | 42 | ``` 43 | var keystone = require('keystone'), 44 | fs = require('fs'), 45 | Types = keystone.Field.Types, 46 | keystoneRestApi = require('keystone-rest-api'); 47 | 48 | var User = new keystone.List('User', { 49 | rest: true, 50 | restOptions: 'list show create update delete' 51 | }); 52 | 53 | User.add({ 54 | name: { type: Types.Name, required: true, index: true }, 55 | password: { type: Types.Password, initial: true, required: false, restSelected: false }, 56 | token: { type: String, restEditable: false } 57 | }); 58 | 59 | User.restHooks = { 60 | list: [listMiddleware], 61 | show: [showMiddleware], 62 | create: [createMiddleware], 63 | update: [updateMiddleware], 64 | delete: [deleteMiddleware] 65 | }; 66 | 67 | User.register(); 68 | 69 | // Make sure keystone is initialized and started before 70 | // calling createRest 71 | keystone.init(config); 72 | keystone.start(); 73 | 74 | // Add routes with Keystone 75 | keystoneRestApi.createRest(keystone, { 76 | apiRoot: '/api/v1/' 77 | }); 78 | 79 | // Create Documentation and write it to a file 80 | fs.writeFileSync('api.md', keystoneRestApi.apiDocs(), 'UTF-8'); 81 | ``` 82 | 83 | ### Changelog 84 | 85 | ___0.9.7.1___ 86 | - Added ignoreNoEdit to Create to avoid awful errors for now 87 | 88 | ___0.9.7___ 89 | - restDescription field to specify the Description of the REST Endpoint 90 | - Use of keystone Name to create the Header of the Blueprint API Document 91 | 92 | ___0.9.6___ 93 | - Added attributes to Model Definition in the Documentation 94 | - Added Support for Select Field on API Generation 95 | - Added support for Required in Documentation 96 | 97 | ___0.9.5___ 98 | - Added support for UpdateHandler 99 | 100 | ### TODO 101 | - The "update" and "create" method must use the Keystone UpdateHandler (Done) 102 | - Implement a way to set Options for UpdateHandler 103 | 104 | ```javascript 105 | restOptions: { 106 | ignoreNoedit: true 107 | } 108 | ``` 109 | 110 | - New Tests based on the changes. 111 | 112 | ## Authors 113 | 114 | * Román A. Sarria 115 | 116 | * Based on Keystone Rest from Dan Quinn [https://github.com/danielpquinn/keystone-rest](Original Repository) 117 | 118 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | jsdoc = require('gulp-jsdoc'); 3 | 4 | gulp.task('jsdoc', function () { 5 | gulp.src('./index.js') 6 | .pipe(jsdoc.parser()) 7 | .pipe(jsdoc.generator('./documentation/', { 8 | path: 'ink-docstrap', 9 | theme: 'flatly' 10 | })); 11 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | 5 | /** 6 | * Options 7 | * 8 | * - Model 9 | * + rest {Boolean} 10 | * + restOptions {String} 'list show create update delete' 11 | * 12 | * - Methods 13 | * + restHooks {Object} 14 | * 15 | * { 16 | list: [listMiddleware], 17 | show: [showMiddleware], 18 | create: [createMiddleware], 19 | update: [updateMiddleware], 20 | delete: [deleteMiddleware] 21 | } 22 | * 23 | * - Fields 24 | * + restSelected {Boolean} 25 | * + restEditable {Boolean} 26 | */ 27 | 28 | /** 29 | * API Blueprint Documentation templates 30 | * Variables 31 | * - name 32 | * - root 33 | * - endpoint 34 | * - attributes 35 | * 36 | * + Ignored 37 | * - id 38 | */ 39 | 40 | 41 | var api_blueprint = { 42 | new_line: '\n', 43 | tab: ' ', 44 | 45 | api_doc_templates: { 46 | model: '# Endpoint for {name} [{root}{endpoint}]\nThis endpoint will provide all the required methods available for {name}\n\n+ Attributes\n{attributes}\n\n', 47 | list: '## List all {name} [GET {root}{endpoint}]\nRetrieves the list of {name}\n\n+ Response 200 (application/json)', 48 | show: '## Retrieve {name} [GET {root}{endpoint}/{id}]\nRetrieves item with the id\n\n+ Response 200 (application/json)', 49 | create: '## Create a {name} [POST {root}{endpoint}]\n\n+ Attributes\n{attributes}\n\n+ Response 200 (application/json)', 50 | update: '## Updates a {name} [PUT {root}{endpoint}]\n\n+ Attributes\n{attributes}\n\n+ Response 200 (application/json)', 51 | delete: '## Deletes an item from {name} [DELETE {root}{endpoint}/{id}]\nDelete a {name}. **Warning:** This action **permanently** removes the {name} from the database.\n\n+ Response 200 (application/json)', 52 | }, 53 | 54 | /** 55 | * Gets a method type and the vars and created the documentation Text 56 | * 57 | * @param {String} type Method type 58 | * @param {Object} vars Variables to be merged 59 | * @returns {String} Template output 60 | */ 61 | _docTemplate: function (type, vars) { 62 | var tmp = api_blueprint.api_doc_templates[type]; 63 | 64 | _.forEach(vars, function (val, key) { 65 | tmp = tmp.replace(new RegExp('{' + key + '}', 'g'), val); 66 | }); 67 | 68 | return tmp; 69 | }, 70 | 71 | convertType: function (type) { 72 | type = type.toLowerCase(); 73 | 74 | if (type == 'objectid') { 75 | type = 'object'; 76 | } 77 | 78 | return type; 79 | }, 80 | 81 | convertDefault: function (value) { 82 | if (typeof (value) == 'string' && value == '') { 83 | return "''"; 84 | } 85 | 86 | if (!value) { 87 | return "''"; 88 | } 89 | 90 | return value; 91 | } 92 | }; 93 | 94 | /** 95 | * @constructor 96 | */ 97 | function KeystoneRest() { 98 | var self = this; 99 | 100 | /** 101 | * Root of the API 102 | */ 103 | var apiRoot = '/api/'; 104 | 105 | var api_doc = {}; 106 | 107 | // Mongoose instance attached to keystone object. 108 | // Assigned in addRoutes 109 | var mongoose, 110 | keystone; 111 | 112 | /** 113 | * Array containing routes and handlers 114 | * @type {Array} 115 | */ 116 | 117 | self.routes = []; 118 | 119 | 120 | /** 121 | * Send a 404 response 122 | * @param {Object} res Express response 123 | * @param {String} message Message 124 | */ 125 | var _send404 = function (res, message) { 126 | res.status(404); 127 | res.json({ 128 | status: 'missing', 129 | message: message 130 | }); 131 | }; 132 | 133 | 134 | /** 135 | * Send an error response 136 | * @param {Object} err Error response object 137 | * @param {Object} res Express response 138 | */ 139 | var _sendError = function (err, req, res, next) { 140 | /*jslint unparam: true */ 141 | next(err); 142 | }; 143 | 144 | 145 | /** 146 | * Convert fields that are relationships to _ids 147 | * @param {Object} model instance of mongoose model 148 | */ 149 | var _flattenRelationships = function (model, body) { 150 | _.each(body, function (field, key) { 151 | var schemaField = model.schema.paths[key]; 152 | 153 | // return if value is a string 154 | if (typeof field === 'string' || !schemaField || _.isEmpty(schemaField)) { 155 | return; 156 | } 157 | 158 | if (schemaField.options.ref) { 159 | body[key] = field._id; 160 | } 161 | 162 | if (_.isArray(schemaField.options.type)) { 163 | if (schemaField.options.type[0].ref) { 164 | _.each(field, function (value, i) { 165 | if (typeof value === 'string' || !value) { 166 | return; 167 | } 168 | body[key][i] = value._id; 169 | }); 170 | } 171 | } 172 | }); 173 | }; 174 | 175 | 176 | /** 177 | * Get list of selected fields based on options in schema 178 | * 179 | * @param {Schema} schema Mongoose schema 180 | */ 181 | var _getSelectedFieldsArray = function (schema) { 182 | var selected = []; 183 | 184 | _.each(schema.paths, function (path) { 185 | if (path.options.restSelected !== false) { 186 | selected.push(path); 187 | } 188 | }); 189 | 190 | return selected; 191 | }; 192 | 193 | /** 194 | * Get list of selected fields based on options in schema 195 | * 196 | * @param {Schema} schema Mongoose schema 197 | */ 198 | var _getSelectedArray = function (schema) { 199 | /*var selected = []; 200 | 201 | _.each(schema.paths, function (path) { 202 | if (path.options.restSelected !== false) { 203 | selected.push(path.path); 204 | } 205 | });*/ 206 | 207 | return _.pluck(_getSelectedFieldsArray(schema), 'path'); 208 | }; 209 | 210 | 211 | /** 212 | * Get list of selected fields based on options in schema 213 | * @param {Schema} schema Mongoose schema 214 | */ 215 | var _getSelected = function (schema) { 216 | return _getSelectedArray(schema).join(' '); 217 | }; 218 | 219 | 220 | /** 221 | * Get Uneditable 222 | * @param {Schema} schema Mongoose schema 223 | */ 224 | var _getUneditable = function (schema) { 225 | var uneditable = []; 226 | 227 | _.each(schema.paths, function (path) { 228 | if (path.options.restEditable === false) { 229 | uneditable.push(path.path); 230 | return; 231 | } 232 | if (path.options.type.constructor.name === 'Array') { 233 | if (path.options.type[0].restEditable === false) { 234 | uneditable.push(path.path); 235 | } 236 | } 237 | }); 238 | 239 | return uneditable; 240 | }; 241 | 242 | 243 | /** 244 | * Get name of reference model 245 | * @param {Model} Model Mongoose model 246 | * @param {String} path Ref path to get name from 247 | */ 248 | var _getRefName = function (Model, path) { 249 | var options = Model.schema.paths[path].options; 250 | 251 | // One to one relationship 252 | if (options.ref) { 253 | return options.ref; 254 | } 255 | 256 | // One to many relationsihp 257 | return options.type[0].ref; 258 | }; 259 | 260 | var _registerList = function (md) { 261 | // Check if Rest must be enabled 262 | try { 263 | if (md && md.options.rest) { 264 | // Register the model 265 | addRoutes(md, md.options.restOptions, md.restHooks, md.model.collection.name); 266 | } else { 267 | console.info('Rest is not enabled for ' + md.model.collection.name); 268 | } 269 | } catch (e) { 270 | console.info('Error registering List. Please verify'); 271 | console.error(e); 272 | } 273 | }; 274 | 275 | /** 276 | * Register the models that has the Rest Option enabled 277 | * 278 | * @param {Object} app Keystone App 279 | */ 280 | var _registerRestModels = function (app) { 281 | // Get the models 282 | var keys = Object.keys(app.mongoose.models); 283 | 284 | for (var i = 0; i < keys.length; i++) { 285 | // Get the Keyston List 286 | var md = app.list(keys[i]); 287 | 288 | _registerList(md); 289 | } 290 | }; 291 | 292 | /** 293 | * Creates the documentation for an endpoint 294 | * 295 | * @param {String} method The method to be created 296 | * @param {Object} Model The Model 297 | */ 298 | var _createDocumentation = function (method, Model) { 299 | var collectionName = Model.collection.name; 300 | 301 | // Defaults 302 | var vars = { 303 | name: collectionName, 304 | root: apiRoot, 305 | endpoint: collectionName.toLowerCase() 306 | }; 307 | 308 | // create / update 309 | if (method == 'create' || method == 'update' || method == 'model') { 310 | var selecteds = _getSelectedFieldsArray(Model.schema); 311 | 312 | var attributes = []; 313 | _.forEach(selecteds, function (selected) { 314 | var tmp = api_blueprint.tab + '+ ' + selected.path; 315 | 316 | if (selected.instance != undefined) { 317 | tmp += ' (' + api_blueprint.convertType(selected.instance) + ( selected.isRequired ? ', required' : '' ) + ')'; 318 | } 319 | 320 | if( _.has(selected, 'enumValues') && _.isArray(selected.enumValues) && selected.enumValues.length > 0 ) { 321 | tmp += api_blueprint.new_line + api_blueprint.tab + api_blueprint.tab + '+ Options: ' + selected.enumValues.join(', '); 322 | } 323 | 324 | if (_.has(selected.options, 'default') && typeof (selected.options.default) != 'Function') { 325 | tmp += api_blueprint.new_line + api_blueprint.tab + api_blueprint.tab + '+ Default: ' + api_blueprint.convertDefault(selected.options.default); 326 | } 327 | 328 | if (_.has(selected.options, 'ref')) { 329 | tmp += api_blueprint.new_line + api_blueprint.tab + api_blueprint.tab + '+ Reference: ' + selected.options.ref; 330 | } 331 | 332 | attributes.push(tmp); 333 | }); 334 | 335 | vars['attributes'] = attributes.join(api_blueprint.new_line); 336 | } 337 | 338 | api_doc[collectionName.toLowerCase()][method] = api_blueprint._docTemplate(method, vars); 339 | }; 340 | 341 | /** 342 | * Add get route 343 | * @param {Model} model Mongoose Model 344 | * @param {Mixed} middleware Express middleware to execute before route handler 345 | * @param {String} selected String passed to mongoose "select" method 346 | */ 347 | var _addList = function (Model, middleware, selected, relationships) { 348 | // Create Docs 349 | _createDocumentation('list', Model); 350 | 351 | // Get a list of items 352 | self.routes.push({ 353 | method: 'get', 354 | middleware: middleware, 355 | route: apiRoot + Model.collection.name.toLowerCase(), 356 | handler: function (req, res, next) { 357 | var populated = req.query.populate ? req.query.populate.split(',') : [], 358 | criteria = _.omit(req.query, ['populate', '_', 'limit', 'skip', 'sort', 'select']), 359 | querySelect; 360 | 361 | if (req.query.select) { 362 | querySelect = req.query.select.split(','); 363 | querySelect = querySelect.filter(function (field) { 364 | return (selected.indexOf(field) > -1); 365 | }).join(' '); 366 | } 367 | 368 | Model.find().count(function (err, count) { 369 | if (err) { 370 | return _sendError(err, req, res, next); 371 | } 372 | 373 | var query = Model.find(criteria).skip(req.query.skip) 374 | .limit(req.query.limit) 375 | .sort(req.query.sort) 376 | .select(querySelect || selected); 377 | 378 | populated.forEach(function (path) { 379 | query.populate({ 380 | path: path, 381 | select: _getSelected(mongoose.model(_getRefName(Model, path)).schema) 382 | }); 383 | }); 384 | 385 | query.exec(function (err, response) { 386 | if (err) { 387 | return _sendError(err, req, res, next); 388 | } 389 | 390 | // Make total total accessible via response headers 391 | res.setHeader('total', count); 392 | res.json(response); 393 | }); 394 | }); 395 | } 396 | }); 397 | 398 | 399 | // Get a list of relationships 400 | if (relationships) { 401 | 402 | _.each(relationships, function (relationship) { 403 | self.routes.push({ 404 | method: 'get', 405 | middleware: [], 406 | route: apiRoot + Model.collection.name.toLowerCase() + '/:id/' + relationship, 407 | handler: function (req, res, next) { 408 | Model.findById(req.params.id).exec(function (err, result) { 409 | var total, 410 | criteria = _.omit(req.query, ['populate', '_', 'limit', 'skip', 'sort', 'select']), 411 | ref, 412 | query, 413 | querySelect, 414 | refSelected, 415 | sortedResults = []; 416 | 417 | if (err && err.type !== 'ObjectId') { 418 | return _sendError(err, req, res, next); 419 | } 420 | if (!result) { 421 | return _send404(res, 'Could not find ' + Model.collection.name.toLowerCase() + ' with id ' + req.params.id); 422 | } 423 | 424 | total = result[relationship].length; 425 | ref = Model.schema.paths[relationship].caster.options.ref; 426 | 427 | refSelected = _getSelected(mongoose.model(ref).schema); 428 | 429 | query = mongoose.model(ref) 430 | .find(criteria) 431 | .in('_id', result[relationship]) 432 | .limit(req.query.limit) 433 | .skip(req.query.skip) 434 | .sort(req.query.sort); 435 | 436 | if (req.query.select) { 437 | querySelect = req.query.select.split(','); 438 | querySelect = querySelect.filter(function (field) { 439 | return (refSelected.indexOf(field) > -1); 440 | }).join(' '); 441 | query.select(querySelect); 442 | } 443 | 444 | if (req.query.populate && typeof req.query.populate === 'string') { 445 | query.populate(req.query.populate); 446 | } 447 | 448 | query.exec(function (err, response) { 449 | if (err) { 450 | return _sendError(err, req, res, next); 451 | } 452 | 453 | // Put relationship results into same order 454 | // that they appear in document 455 | if (!req.query.sort) { 456 | result[relationship].forEach(function (_id, i) { 457 | sortedResults[i] = _.findWhere(response, { 458 | _id: _id 459 | }); 460 | }); 461 | response = sortedResults; 462 | } 463 | 464 | // Make total total accessible via response headers 465 | res.setHeader('total', total); 466 | res.json(response); 467 | }); 468 | }); 469 | } 470 | }); 471 | }); 472 | } 473 | }; 474 | 475 | 476 | /** 477 | * Add list route 478 | * @param {Model} model Mongoose Model 479 | * @param {Mixed} middleware Express middleware to execute before route handler 480 | * @param {String} selected String passed to mongoose "select" method 481 | */ 482 | 483 | var _addShow = function (Model, middleware, selected, findBy) { 484 | // Create Docs 485 | _createDocumentation('show', Model); 486 | 487 | var collectionName = Model.collection.name.toLowerCase(); 488 | var paramName = Model.modelName.toLowerCase(); 489 | 490 | // Get one item 491 | self.routes.push({ 492 | method: 'get', 493 | middleware: middleware, 494 | route: apiRoot + collectionName + '/:' + paramName, 495 | handler: function (req, res, next) { 496 | var populated = req.query.populate ? req.query.populate.split(',') : []; 497 | var criteria = {}; 498 | var querySelect; 499 | 500 | if (req.query.select) { 501 | querySelect = req.query.select.split(','); 502 | querySelect = querySelect.filter(function (field) { 503 | return (selected.indexOf(field) > -1); 504 | }).join(' '); 505 | } 506 | 507 | criteria[findBy] = req.params[paramName]; 508 | 509 | var query = Model.findOne(criteria) 510 | .select(querySelect || selected); 511 | 512 | populated.forEach(function (path) { 513 | query.populate({ 514 | path: path, 515 | select: _getSelected(mongoose.model(_getRefName(Model, path)).schema) 516 | }); 517 | }); 518 | 519 | query.exec(function (err, result) { 520 | if (err && err.type !== 'ObjectId') { 521 | return _sendError(err, req, res, next); 522 | } 523 | if (!result) { 524 | return _send404(res, 'Could not find ' + Model.collection.name.toLowerCase() + ' with id ' + req.params.id); 525 | } 526 | res.json(result); 527 | }); 528 | } 529 | }); 530 | }; 531 | 532 | 533 | /** 534 | * Add post route 535 | * @param {Model} Model Mongoose Model 536 | * @param {Mixed} middleware Express middleware to execute before route handler 537 | * @param {String} selected String passed to mongoose "select" method 538 | */ 539 | 540 | var _addCreate = function (Model, middleware, selected) { 541 | // Create Docs 542 | _createDocumentation('create', Model); 543 | 544 | // Create a new item 545 | self.routes.push({ 546 | method: 'post', 547 | middleware: middleware, 548 | route: apiRoot + Model.collection.name.toLowerCase(), 549 | handler: function (req, res, next) { 550 | var item; 551 | 552 | _flattenRelationships(Model, req.body); 553 | 554 | var md = new Model(); 555 | var options = { 556 | flashErrors: false, 557 | ignoreNoedit: true 558 | }; 559 | 560 | // Get the UpdateHandler from Keystone and process the Request 561 | md.getUpdateHandler(req).process(req.body, options, function (err, item) { 562 | console.error(err); 563 | console.info(item); 564 | if (err) { 565 | return _sendError(err, req, res, next); 566 | } 567 | res.json(item); 568 | }); 569 | } 570 | }); 571 | }; 572 | 573 | 574 | /** 575 | * Add put route 576 | * @param {Model} Model Mongoose Model 577 | * @param {Mixed} middleware Express middleware to execute before route handler 578 | * @param {String} selected String passed to mongoose "select" method 579 | * @param {Array} uneditable Array of fields to remove from post 580 | */ 581 | 582 | var _addUpdate = function (Model, middleware, uneditable, selected, findBy) { 583 | // Create Docs 584 | _createDocumentation('update', Model); 585 | 586 | var collectionName = Model.collection.name.toLowerCase(); 587 | var paramName = Model.modelName.toLowerCase(); 588 | var versionKey = Model.schema.options.versionKey; 589 | 590 | var handler = function (req, res, next) { 591 | var populated = req.query.populate ? req.query.populate.split(',') : ''; 592 | var criteria = {}; 593 | var querySelect; 594 | 595 | if (req.query.select) { 596 | querySelect = req.query.select.split(','); 597 | querySelect = querySelect.filter(function (field) { 598 | return (selected.indexOf(field) > -1); 599 | }).join(' '); 600 | } 601 | 602 | criteria[findBy] = req.params[paramName]; 603 | 604 | _flattenRelationships(Model, req.body); 605 | req.body = _.omit(req.body, uneditable); 606 | 607 | Model.findOne(criteria).exec(function (err, item) { 608 | 609 | /*jslint unparam: true */ 610 | if (err && err.type !== 'ObjectId') { 611 | return _sendError(err, req, res, next); 612 | } 613 | if (!item) { 614 | return _send404(res, 'Could not find ' + Model.collection.name.toLowerCase() + ' with id ' + req.params.id); 615 | } 616 | 617 | if (req.body[versionKey] < item[versionKey]) { 618 | return _sendError(new mongoose.Error.VersionError(), req, res, next); 619 | } 620 | 621 | //_.extend(item, req.body); // Not sure about extending with UpdateHandler 622 | 623 | // Get the UpdateHandler and update 624 | item.getUpdateHandler(req).process(req.body, { 625 | flashErrors: false 626 | }, function (err, item) { 627 | if (err) { 628 | return _sendError(err, req, res, next); 629 | } 630 | 631 | Model.findOne(criteria).select(querySelect || selected).populate(populated).exec(function (err, item) { 632 | if (err) { 633 | return _sendError(err, req, res, next); 634 | } 635 | res.json(item); 636 | }); 637 | }); 638 | }); 639 | }; 640 | 641 | // Update an item having a given key 642 | self.routes.push({ 643 | method: 'put', 644 | middleware: middleware, 645 | route: apiRoot + collectionName + '/:' + paramName, 646 | handler: handler 647 | }); 648 | 649 | self.routes.push({ 650 | method: 'patch', 651 | middleware: middleware, 652 | route: apiRoot + collectionName + '/:' + paramName, 653 | handler: handler 654 | }); 655 | }; 656 | 657 | 658 | /** 659 | * Add delete route 660 | * @param {Model} model Mongoose Model 661 | * @param {Mixed} middleware Express middleware to execute before route handler 662 | */ 663 | 664 | var _addDelete = function (Model, middleware, findBy) { 665 | // Create Docs 666 | _createDocumentation('delete', Model); 667 | 668 | var collectionName = Model.collection.name.toLowerCase(); 669 | var paramName = Model.modelName.toLowerCase(); 670 | 671 | // Delete an item having a given id 672 | self.routes.push({ 673 | method: 'delete', 674 | middleware: middleware, 675 | route: apiRoot + collectionName + '/:' + paramName, 676 | handler: function (req, res, next) { 677 | var criteria = {}; 678 | 679 | criteria[findBy] = req.params[paramName]; 680 | 681 | // First find so middleware hooks (pre,post) will execute 682 | Model.findOne(criteria, function (err, item) { 683 | if (err && err.type !== 'ObjectId') { 684 | return _sendError(err, req, res, next); 685 | } 686 | if (!item) { 687 | return _send404(res, 'Could not find ' + Model.collection.name.toLowerCase() + ' with id ' + req.params.id); 688 | } 689 | 690 | item.remove(function (err) { 691 | if (err) { 692 | return _sendError(err, req, res, next); 693 | } 694 | res.json({ 695 | message: 'Successfully deleted ' + collectionName 696 | }); 697 | }); 698 | }); 699 | } 700 | }); 701 | }; 702 | 703 | 704 | /** 705 | * Add routes 706 | * 707 | * @param {Object} keystoneList Instance of KeystoneList 708 | * @param {String} methods Methods to expose('list show create update delete') 709 | * @param {Object} middleware Map containing middleware to execute for each action ({ list: [middleware] }) 710 | * @param {String} relationships Space separated list of relationships to build routes for 711 | */ 712 | 713 | var addRoutes = function (keystoneList, methods, middleware, relationships) { 714 | // Get reference to mongoose for internal use 715 | mongoose = keystone.mongoose; 716 | 717 | var findBy; 718 | var Model = keystoneList.model; 719 | 720 | if (!Model instanceof mongoose.model) { 721 | throw new Error('keystoneList is required'); 722 | } 723 | if (!methods) { 724 | throw new Error('Methods are required'); 725 | } 726 | if (!mongoose) { 727 | throw new Error('Keystone must be initialized before attempting to add routes'); 728 | } 729 | 730 | var collectionName = Model.collection.name; 731 | if (!_.has(api_doc, collectionName.toLowerCase())) { 732 | api_doc[collectionName.toLowerCase()] = {}; 733 | 734 | _createDocumentation('model', Model); 735 | } 736 | 737 | var selected = _getSelected(Model.schema), 738 | uneditable = _getUneditable(Model.schema), 739 | listMiddleware, 740 | showMiddleware, 741 | createMiddleware, 742 | updateMiddleware, 743 | deleteMiddleware; 744 | 745 | methods = methods.split(' '); 746 | 747 | // Use autoKey to find doc if it exists 748 | if (keystoneList.options.autokey) { 749 | findBy = keystoneList.options.autokey.path; 750 | } else { 751 | findBy = '_id'; 752 | } 753 | 754 | // Set up default middleware 755 | middleware = middleware || {}; 756 | listMiddleware = middleware.list || []; 757 | showMiddleware = middleware.show || []; 758 | createMiddleware = middleware.create || []; 759 | updateMiddleware = middleware.update || []; 760 | deleteMiddleware = middleware.delete || []; 761 | 762 | relationships = relationships ? relationships.split(' ') : []; 763 | 764 | if (methods.indexOf('list') !== -1) { 765 | _addList(Model, listMiddleware, selected, relationships); 766 | } 767 | if (methods.indexOf('show') !== -1) { 768 | _addShow(Model, showMiddleware, selected, findBy); 769 | } 770 | if (methods.indexOf('create') !== -1) { 771 | _addCreate(Model, createMiddleware, selected); 772 | } 773 | if (methods.indexOf('update') !== -1) { 774 | _addUpdate(Model, updateMiddleware, uneditable, selected, findBy); 775 | } 776 | if (methods.indexOf('delete') !== -1) { 777 | _addDelete(Model, deleteMiddleware, findBy); 778 | } 779 | }; 780 | 781 | /** 782 | * Register a Keystone List manually. 783 | * 784 | * @param {Object} list Object of Type Keystone List 785 | */ 786 | this.registerList = function (list) { 787 | _registerList(list); 788 | }; 789 | 790 | 791 | /** 792 | * Creates Rest 793 | * 794 | * @param {Object} app Keystone instance 795 | */ 796 | 797 | this.createRest = function (kref, options) { 798 | keystone = kref; // Get the app reference of Keystone 799 | 800 | if (options == undefined) { 801 | options = {}; 802 | }; 803 | 804 | if (_.has(options, 'apiRoot') && options.apiRoot != '') { 805 | apiRoot = options.apiRoot; 806 | } 807 | 808 | // Get and register the models 809 | _registerRestModels(keystone); 810 | 811 | _.each(self.routes, function (route) { 812 | keystone.app[route.method](route.route, route.middleware, route.handler); 813 | }); 814 | }; 815 | 816 | /** 817 | * Returns the API Docs for the API Created 818 | * 819 | * @returns {String} The Blueprint formatted API 820 | */ 821 | this.apiDocs = function () { 822 | var md_doc = []; 823 | 824 | _.forEach(api_doc, function (model_doc, key) { 825 | var documentation = []; 826 | 827 | _.forEach(model_doc, function (doc, k) { 828 | documentation.push(doc); 829 | }); 830 | 831 | md_doc.push(documentation.join((api_blueprint.new_line + api_blueprint.new_line))); 832 | }); 833 | 834 | return md_doc.join((api_blueprint.new_line + api_blueprint.new_line)); 835 | }; 836 | } 837 | 838 | /* 839 | ** Exports 840 | */ 841 | 842 | module.exports = new KeystoneRest(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keystone-rest-api", 3 | "version": "0.9.8", 4 | "description": "Creates a powerful Rest API based on Keystone Lists", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/sarriaroman/Keystone-Rest-API.git" 9 | }, 10 | "keywords": [ 11 | "keystone", 12 | "keystonejs", 13 | "rest", 14 | "restful" 15 | ], 16 | "author": "Román Sarria", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/sarriaroman/Keystone-Rest-API/issues" 20 | }, 21 | "dependencies": { 22 | "lodash": "~3.9.0" 23 | }, 24 | "devDependencies": { 25 | "ink-docstrap": "~0.3.0-0", 26 | "jsdoc": "~3.3.0-alpha5", 27 | "gulp-jsdoc": "~0.1.4", 28 | "gulp": "~3.6.2" 29 | } 30 | } --------------------------------------------------------------------------------