├── test ├── mocha.opts ├── fixtures │ └── sampleapp │ │ ├── api │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ ├── PetController.js │ │ │ ├── UserController.js │ │ │ └── ImageController.js │ │ ├── models │ │ │ ├── useraffiliation.js │ │ │ ├── image.js │ │ │ ├── pet.js │ │ │ ├── affiliation.js │ │ │ └── user.js │ │ └── hooks │ │ │ └── sequelize │ │ │ └── index.js │ │ ├── .sailsrc │ │ ├── config │ │ ├── models.js │ │ ├── connections.js │ │ └── blueprints.js │ │ ├── app.js │ │ └── views │ │ ├── layout.ejs │ │ ├── 403.ejs │ │ ├── 404.ejs │ │ ├── homepage.ejs │ │ └── 500.ejs ├── bootstrap.test.js └── unit │ └── controllers │ └── Blueprint.test.js ├── .travis.yml ├── LICENSE ├── actions ├── findOne.js ├── destroy.js ├── create.js ├── find.js ├── populate.js ├── remove.js ├── update.js └── add.js ├── package.json ├── coercePK.js ├── jsonp.js ├── .gitignore ├── README.md ├── onRoute.js ├── .jshintrc ├── actionUtil.js └── index.js /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --timeout 12000 2 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/controllers/PetController.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | }; 3 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/.sailsrc: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "grunt": false, 4 | "orm": false, 5 | "pubsub": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/config/models.js: -------------------------------------------------------------------------------- 1 | module.exports.models = { 2 | 3 | connection: 'somePostgresqlServer', 4 | 5 | migrate: 'drop' 6 | }; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | addons: 5 | postgresql: "9.3" 6 | before_install: 7 | - psql -c 'create database sequelize;' -U postgres 8 | - 'npm install' 9 | script: 10 | - 'npm test' 11 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UserController 3 | * 4 | * @description :: Server-side logic for managing Users 5 | * @help :: See http://links.sailsjs.org/docs/controllers 6 | */ 7 | 8 | module.exports = { 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/controllers/ImageController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ImageController 3 | * 4 | * @description :: Server-side logic for managing images 5 | * @help :: See http://links.sailsjs.org/docs/controllers 6 | */ 7 | 8 | module.exports = { 9 | }; 10 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/config/connections.js: -------------------------------------------------------------------------------- 1 | module.exports.connections = { 2 | 3 | 4 | somePostgresqlServer: { 5 | user: process.env.USER || 'postgres', 6 | password: '', 7 | database: 'sequelize', 8 | dialect: 'postgres', 9 | options: { 10 | sync: 'force', 11 | dialect: 'postgres', 12 | host : 'localhost', 13 | port : 5432, 14 | logging: true 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/models/useraffiliation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UserAffiliation.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works 5 | * and what it represents here.and what it represents here. 6 | * @docs :: http://sailsjs.org/#!documentation/models 7 | */ 8 | 9 | module.exports = { 10 | 11 | attributes: { 12 | 13 | }, 14 | options: { 15 | tableName: 'useraffiliation', 16 | classMethods: {}, 17 | instanceMethods: {}, 18 | hooks: {} 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/models/image.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Image.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | attributes: { 10 | url: { 11 | type: Sequelize.STRING 12 | } 13 | }, 14 | associations: function() { 15 | image.belongsTo(user, { 16 | onDelete: 'cascade', 17 | as: 'owner', 18 | foreignKey: { 19 | name: 'userId', 20 | as: 'owner', 21 | allowNull: false 22 | } 23 | }); 24 | }, 25 | options: { 26 | classMethods: {}, 27 | instanceMethods: {}, 28 | hooks: {} 29 | } 30 | }; 31 | 32 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/models/pet.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Image.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | attributes: { 10 | name: { 11 | type: Sequelize.STRING 12 | }, 13 | breed: { 14 | type: Sequelize.STRING 15 | } 16 | }, 17 | associations: function() { 18 | pet.belongsTo(user, { 19 | as: 'owner', 20 | onDelete: 'cascade', 21 | foreignKey: { 22 | name: 'userId', 23 | as: 'owner', 24 | allowNull: false 25 | } 26 | }); 27 | }, 28 | options: { 29 | classMethods: {}, 30 | instanceMethods: {}, 31 | tableName: 'pets', 32 | hooks: {} 33 | } 34 | }; 35 | 36 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/models/affiliation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Affiliation.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | attributes: { 10 | id: { 11 | type: Sequelize.UUID, 12 | defaultValue: Sequelize.UUIDV4, 13 | primaryKey: true 14 | }, 15 | name: { 16 | type: Sequelize.STRING, 17 | allowNull: false 18 | }, 19 | state: { 20 | type: Sequelize.STRING 21 | }, 22 | city: { 23 | type: Sequelize.STRING, 24 | allowNull: false 25 | } 26 | }, 27 | associations: function() { 28 | affiliation.belongsToMany(user, { 29 | as: 'users', 30 | to: 'affiliations', // must be named as the alias in the related Model 31 | through: 'UserAffiliation', 32 | foreignKey: { 33 | name: 'affiliationId', 34 | as: 'users' 35 | } 36 | }); 37 | }, 38 | options: { 39 | tableName: 'affiliation', 40 | classMethods: {}, 41 | instanceMethods: {}, 42 | hooks: {} 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 César Augusto D. Azevedo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /actions/findOne.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var actionUtil = require('../actionUtil'), 5 | _ = require('lodash'); 6 | 7 | /** 8 | * Find One Record 9 | * 10 | * get /:modelIdentity/:id 11 | * 12 | * An API call to find and return a single model instance from the data adapter 13 | * using the specified id. 14 | * 15 | * Required: 16 | * @param {Integer|String} id - the unique id of the particular instance you'd like to look up * 17 | * 18 | * Optional: 19 | * @param {String} callback - default jsonp callback param (i.e. the name of the js function returned) 20 | */ 21 | 22 | module.exports = function findOneRecord (req, res) { 23 | var Model = actionUtil.parseModel(req); 24 | var pk = actionUtil.requirePk(req); 25 | var populate = actionUtil.populateEach(req); 26 | 27 | Model.findById(pk, {include: req._sails.config.blueprints.populate ? 28 | (_.isEmpty(populate) ? [{ all : true}] : populate) : [] 29 | }).then(function(matchingRecord) { 30 | if(!matchingRecord) return res.notFound('No record found with the specified `id`.'); 31 | 32 | if (req._sails.hooks.pubsub && req.isSocket) { 33 | Model.subscribe(req, matchingRecord); 34 | actionUtil.subscribeDeep(req, matchingRecord); 35 | } 36 | 37 | res.ok(matchingRecord); 38 | }).catch(function(err){ 39 | return res.serverError(err); 40 | }); 41 | 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User.js 3 | * 4 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 5 | * @docs :: http://sailsjs.org/#!documentation/models 6 | */ 7 | 8 | module.exports = { 9 | attributes: { 10 | name: { 11 | type: Sequelize.STRING, 12 | allowNull: false 13 | }, 14 | age: { 15 | type: Sequelize.INTEGER 16 | } 17 | }, 18 | associations: function() { 19 | user.hasMany(image, { 20 | onDelete: 'cascade', 21 | as: 'images', 22 | foreignKey: { 23 | name: 'userId', 24 | as: 'images', 25 | allowNull: false 26 | } 27 | }); 28 | user.hasMany(pet, { 29 | as: 'pets', 30 | foreignKey: { 31 | name: 'userId', 32 | as: 'pets', 33 | } 34 | }); 35 | user.belongsToMany(affiliation, { 36 | as: 'affiliations', 37 | to: 'users', // must be named as the alias in the related Model 38 | through: 'UserAffiliation', 39 | foreignKey: { 40 | name: 'userId', 41 | as: 'affiliations' 42 | } 43 | }); 44 | }, 45 | options: { 46 | tableName: 'user', 47 | classMethods: {}, 48 | instanceMethods: {}, 49 | hooks: {} 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /test/bootstrap.test.js: -------------------------------------------------------------------------------- 1 | // var Sails = require('./fixtures/sampleapp/app'); 2 | var Sails = require('./fixtures/sampleapp/app').sails; 3 | 4 | describe('Basic tests ::', function() { 5 | 6 | // Var to hold a running sails app instance 7 | var sails; 8 | 9 | // Before running any tests, attempt to lift Sails 10 | before(function(done) { 11 | 12 | // Hook will timeout in 10 seconds 13 | this.timeout(11000); 14 | 15 | // Attempt to lift sails 16 | Sails().lift({ 17 | hooks: { 18 | "sequelize": require('./fixtures/sampleapp/api/hooks/sequelize'), 19 | // Load the hook 20 | "sails-hook-sequelize-blueprint": require('../'), 21 | "blueprints": false, 22 | "orm": false, 23 | "pubsub": false, 24 | // Skip grunt (unless your hook uses it) 25 | "grunt": false, 26 | } 27 | },function (err, _sails) { 28 | if (err) return done(err); 29 | sails = _sails; 30 | return done(err, sails); 31 | }); 32 | }); 33 | 34 | // After tests are complete, lower Sails 35 | after(function (done) { 36 | 37 | // Lower Sails (if it successfully lifted) 38 | // if(sails) { 39 | // return sails.lower(done); 40 | // } 41 | // Otherwise just return 42 | return done(); 43 | }); 44 | 45 | // Test that Sails can lift with the hook in place 46 | it('sails does not crash', function() { 47 | return true; 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /actions/destroy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var actionUtil = require('../actionUtil'); 5 | 6 | /** 7 | * Destroy One Record 8 | * 9 | * delete /:modelIdentity/:id 10 | * * /:modelIdentity/destroy/:id 11 | * 12 | * Destroys the single model instance with the specified `id` from 13 | * the data adapter for the given model if it exists. 14 | * 15 | * Required: 16 | * @param {Integer|String} id - the unique id of the particular instance you'd like to delete 17 | * 18 | * Optional: 19 | * @param {String} callback - default jsonp callback param (i.e. the name of the js function returned) 20 | */ 21 | module.exports = function destroyOneRecord (req, res) { 22 | 23 | var Model = actionUtil.parseModel(req); 24 | var pk = actionUtil.requirePk(req); 25 | 26 | Model.findById(pk, { include: req._sails.config.blueprints.populate ? [{ all: true }] : []}) 27 | .then(function(record) { 28 | if(!record) return res.notFound('No record found with the specified `id`.'); 29 | 30 | Model.destroy({ where: { id: pk }}).then(function() { 31 | 32 | if (req._sails.hooks.pubsub) { 33 | Model.publishDestroy(pk, !req._sails.config.blueprints.mirror && req, {previous: record}); 34 | if (req.isSocket) { 35 | Model.unsubscribe(req, record); 36 | Model.retire(record); 37 | } 38 | } 39 | 40 | return res.ok(record); 41 | }).catch(function(err){ 42 | return res.negotiate(err); 43 | }); 44 | }).catch(function(err){ 45 | return res.serverError(err); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-hook-sequelize-blueprints", 3 | "version": "0.3.0", 4 | "description": "Sails blueprints with sequelize", 5 | "main": "index.js", 6 | "sails": { 7 | "isHook": true 8 | }, 9 | "scripts": { 10 | "test": "./node_modules/mocha/bin/mocha test/bootstrap.test.js test/unit/**/*.test.js" 11 | }, 12 | "keywords": [ 13 | "sails", 14 | "sequelize", 15 | "blueprints" 16 | ], 17 | "author": "Cesar Augusto D. Azevedo", 18 | "license": "MIT", 19 | "devDependencies": { 20 | "mocha": "^2.2.5", 21 | "pg": "^4.4.0", 22 | "pg-hstore": "^2.3.2", 23 | "root-require": "^0.3.1", 24 | "sails-disk": "^0.10.8", 25 | "should": "^7.0.2", 26 | "supertest": "^1.0.1", 27 | "sails": "^0.11.0", 28 | "sequelize": "^3.20.0" 29 | }, 30 | "dependencies": { 31 | "async": "^1.4.0", 32 | "continuation-local-storage": "^3.1.4", 33 | "lodash": "^3.10.0", 34 | "merge-defaults": "^0.2.1", 35 | "pluralize": "^1.1.2", 36 | "sails-stringfile": "^0.3.2", 37 | "sails-util": "^0.10.6" 38 | }, 39 | "peerDependencies": { 40 | "sails": "^0.x", 41 | "sequelize": "^3.x", 42 | "sails-hook-sequelize": "^1.x" 43 | }, 44 | "directories": { 45 | "test": "test" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "git@github.com:cesardeazevedo/sails-hook-sequelize-blueprints.git" 50 | }, 51 | "bugs": { 52 | "url": "https://github.com/cesardeazevedo/sails-hook-sequelize-blueprints/issues" 53 | }, 54 | "homepage": "https://github.com/cesardeazevedo/sails-hook-sequelize-blueprints" 55 | } 56 | -------------------------------------------------------------------------------- /actions/create.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var actionUtil = require('../actionUtil'); 5 | 6 | /** 7 | * Create Record 8 | * 9 | * post /:modelIdentity 10 | * 11 | * An API call to find and return a single model instance from the data adapter 12 | * using the specified criteria. If an id was specified, just the instance with 13 | * that unique id will be returned. 14 | * 15 | * Optional: 16 | * @param {String} callback - default jsonp callback param (i.e. the name of the js function returned) 17 | * @param {*} * - other params will be used as `values` in the create 18 | */ 19 | module.exports = function createRecord (req, res) { 20 | 21 | var Model = actionUtil.parseModel(req); 22 | 23 | // Create data object (monolithic combination of all parameters) 24 | // Omit the blacklisted params (like JSONP callback param, etc.) 25 | var data = actionUtil.parseValues(req); 26 | 27 | // Create new instance of model using data from params 28 | Model.create(data).then(function(newInstance) { 29 | // If we have the pubsub hook, use the model class's publish method 30 | // to notify all subscribers about the created item 31 | if (req._sails.hooks.pubsub) { 32 | if (req.isSocket) { 33 | Model.subscribe(req, newInstance); 34 | Model.introduce(newInstance); 35 | } 36 | Model.publishCreate(newInstance.toJSON(), !req.options.mirror && req); 37 | } 38 | 39 | // Send JSONP-friendly response if it's supported 40 | res.created(newInstance); 41 | }).catch(function(err){ 42 | // Differentiate between waterline-originated validation errors 43 | // and serious underlying issues. Respond with badRequest if a 44 | // validation error is encountered, w/ validation info. 45 | return res.negotiate(err); 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * app.js 3 | * 4 | * Use `app.js` to run your app without `sails lift`. 5 | * To start the server, run: `node app.js`. 6 | * 7 | * This is handy in situations where the sails CLI is not relevant or useful. 8 | * 9 | * For example: 10 | * => `node app.js` 11 | * => `forever start app.js` 12 | * => `node debug app.js` 13 | * => `modulus deploy` 14 | * => `heroku scale` 15 | * 16 | * 17 | * The same command-line arguments are supported, e.g.: 18 | * `node app.js --silent --port=80 --prod` 19 | */ 20 | 21 | // Ensure we're in the project directory, so relative paths work as expected 22 | // no matter where we actually lift from. 23 | process.chdir(__dirname); 24 | // Ensure a "sails" can be located: 25 | (function() { 26 | var sails; 27 | try { 28 | sails = require('sails'); 29 | } catch (e) { 30 | console.error('To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.'); 31 | console.error('To do that, run `npm install sails`'); 32 | console.error(''); 33 | console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); 34 | console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); 35 | console.error('but if it doesn\'t, the app will run with the global sails instead!'); 36 | return; 37 | } 38 | sails.log('Testing'); 39 | // Try to get `rc` dependency 40 | var rc; 41 | try { 42 | rc = require('rc'); 43 | } catch (e0) { 44 | try { 45 | rc = require('sails/node_modules/rc'); 46 | } catch (e1) { 47 | console.error('Could not find dependency: `rc`.'); 48 | console.error('Your `.sailsrc` file(s) will be ignored.'); 49 | console.error('To resolve this, run:'); 50 | console.error('npm install rc --save'); 51 | rc = function () { return {}; }; 52 | } 53 | } 54 | 55 | // Start server 56 | // sails.lift(rc('sails')); 57 | module.exports.sails = sails.Sails; 58 | 59 | })(); 60 | 61 | -------------------------------------------------------------------------------- /coercePK.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var _ = require('lodash'); 6 | 7 | 8 | module.exports = function (sails) { 9 | 10 | /** 11 | * @param {Natural|String} id 12 | * @param {String} controllerId 13 | * @param {String} actionId 14 | * @returns id :: 15 | * If `id` is undefined, no `id` was provided 16 | * If `id` is false, `id` is invalid, and probably unintentional 17 | * Otherwise, `id` is valid and probably intentional 18 | */ 19 | return function validId (id, controllerId, actionId) { 20 | 21 | // Interlace app-global `config.controllers` with this controller's `_config` 22 | var controllerConfig = _.merge({}, 23 | sails.config.controllers, 24 | sails.controllers[controllerId]._config || {}); 25 | 26 | 27 | // The other CRUD methods are special reserved words-- in which case we always pass 28 | // As long as the CRUD 'shortcuts' are enabled, you cannot search for models 29 | // with an id of 'find', 'update', 'create', or 'destroy' 30 | if ( controllerConfig.blueprints.shortcuts && ( 31 | id === 'find' || 32 | id === 'update' || 33 | id === 'create' || 34 | id === 'destroy' )) { 35 | return false; 36 | } 37 | 38 | 39 | // If expectIntegerId check is disabled, `id` is always ok 40 | if ( !controllerConfig.blueprints.expectIntegerId ) { 41 | return id; 42 | } 43 | 44 | // Ensure that id is numeric (unless this check is disabled) 45 | var castId = +id; 46 | if (id && _.isNaN(castId)) { 47 | 48 | // If it's not, move on to next middleware 49 | // but emit a console warning explaining the situation 50 | // (if the app is in development mode): 51 | if (sails.config.environment === 'development') { 52 | sails.log.warn('\n', 53 | 'Just then, you were prevented from being routed \n', 54 | 'to the `' + actionId + '` blueprint for controller: ' + controllerId + 55 | ' using `id='+id+'`.\n', 56 | 'This is because REST blueprint routes expect natural number ids by default, '+ 57 | 'and so the `' + actionId + '()` middleware was skipped- \n', 58 | 'If you\'d like to disable this restriction, you can do so by setting \n', 59 | '`expectIntegerId: false` in the blueprint config for this controller.'); 60 | } 61 | return false; 62 | } 63 | 64 | // Id is an integer 65 | return parseInt(id, 10); 66 | }; 67 | }; 68 | 69 | -------------------------------------------------------------------------------- /jsonp.js: -------------------------------------------------------------------------------- 1 | // from find.js: 2 | // 3 | // 4 | // 5 | // 6 | // *************************************************************** 7 | // `runtimeOverrideForJsonpCallbackParam` is disabled for now. 8 | // (will be pulled into a separate hook and configurable for 9 | // ALL requests, not just blueprints, in an upcoming release.) 10 | // *************************************************************** 11 | 12 | // * @param {String} _jsonpCallbackParam - optional override for JSONP callback param (can be overridden in req.options.requestTimeOverrideForJsonpCallbackParam) 13 | // if (isJSONPCompatibleAndEnabled){ 14 | 15 | // var jsonp = req.options.jsonp; 16 | // // Whether request-time overrides are allowed for the jsonp callback name 17 | // // (defaults to '_jsonpCallbackParam') 18 | // var requestTimeOverrideForJsonpCallbackParam = 19 | // typeof req.options.requestTimeOverrideForJsonpCallbackParam === 'undefined' ? 20 | // '_jsonpCallbackParam' : 21 | // req.options.requestTimeOverrideForJsonpCallbackParam; 22 | 23 | 24 | // // Enforce/apply request-time jsonp callback override setting 25 | // if (!requestTimeOverrideForJsonpCallbackParam && 26 | // req.param(requestTimeOverrideForJsonpCallbackParam) && 27 | // ! (typeof req.options.jsonp === 'object' && req.options.jsonp.allowOverride) ) { 28 | // return res.forbidden('JSONP callback configuration not allowed.'); 29 | // } 30 | 31 | // // The name of the parameter to use for JSONP callbacks 32 | // // Callback param can come from the params (if allowed above), `req.options`, or defaults to `callback` 33 | // var jsonpCallbackParam = req.param(requestTimeOverrideForJsonpCallbackParam) || req.options.jsonpCallbackParam || 'callback'; 34 | // var originalJsonpCallbackParam = req.app.get('jsonp callback name'); 35 | // req.app.set('jsonp callback name', jsonpCallbackParam); 36 | // } 37 | 38 | 39 | 40 | 41 | 42 | // from actionUtil.js (parseCriteria): 43 | // 44 | // 45 | // 46 | // *************************************************************** 47 | // `runtimeOverrideForJsonpCallbackParam` is disabled for now. 48 | // (will be pulled into a separate hook and configurable for 49 | // ALL requests, not just blueprints, in an upcoming release.) 50 | // *************************************************************** 51 | // 52 | // if (req.options.jsonp.runtime) { 53 | // where = _.omit(where, [runtimeOverrideForJsonpCallbackParam]); 54 | // } 55 | // } 56 | 57 | 58 | 59 | // console.log(requestTimeOverrideForJsonpCallbackParam); 60 | // console.log(isJSONPCompatibleAndEnabled, jsonpCallbackParam, 'hi'); 61 | // console.log(req.params.all(), '***\n', where); -------------------------------------------------------------------------------- /test/fixtures/sampleapp/api/hooks/sequelize/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(sails) { 2 | global['Sequelize'] = require('sequelize'); 3 | Sequelize.cls = require('continuation-local-storage').createNamespace('sails-sequelize-postgresql'); 4 | return { 5 | initialize: function(next) { 6 | this.initAdapters(); 7 | this.initModels(); 8 | 9 | var connection, migrate, sequelize; 10 | sails.log.verbose('Using connection named ' + sails.config.models.connection); 11 | connection = sails.config.connections[sails.config.models.connection]; 12 | if (connection == null) { 13 | throw new Error('Connection \'' + sails.config.models.connection + '\' not found in config/connections'); 14 | } 15 | if (connection.options == null) { 16 | connection.options = {}; 17 | } 18 | connection.options.logging = sails.log.verbose; //A function that gets executed everytime Sequelize would log something. 19 | 20 | migrate = sails.config.models.migrate; 21 | sails.log.verbose('Migration: ' + migrate); 22 | 23 | sequelize = new Sequelize(connection.database, connection.user, connection.password, connection.options); 24 | global['sequelize'] = sequelize; 25 | return sails.modules.loadModels(function(err, models) { 26 | var modelDef, modelName, ref; 27 | if (err != null) { 28 | return next(err); 29 | } 30 | for (modelName in models) { 31 | modelDef = models[modelName]; 32 | sails.log.verbose('Loading model \'' + modelDef.globalId + '\''); 33 | global[modelDef.globalId] = sequelize.define(modelDef.globalId, modelDef.attributes, modelDef.options); 34 | sails.models[modelDef.globalId.toLowerCase()] = global[modelDef.globalId]; 35 | } 36 | for (modelName in models) { 37 | modelDef = models[modelName]; 38 | if (modelDef.associations != null) { 39 | sails.log.verbose('Loading associations for \'' + modelDef.globalId + '\''); 40 | if (typeof modelDef.associations === 'function') { 41 | modelDef.associations(modelDef); 42 | } 43 | } 44 | 45 | if (modelDef.defaultScope != null) { 46 | sails.log.verbose('Loading default scope for \'' + modelDef.globalId + '\''); 47 | var model = global[modelDef.globalId]; 48 | if (typeof modelDef.defaultScope === 'function') { 49 | model.$scope = modelDef.defaultScope(); 50 | } 51 | } 52 | } 53 | 54 | if(migrate === 'safe') { 55 | return next(); 56 | } else { 57 | var forceSync = migrate === 'drop'; 58 | sequelize.sync({ force: forceSync }).then(function() { 59 | return next(); 60 | }); 61 | } 62 | }); 63 | }, 64 | 65 | initAdapters: function() { 66 | if(sails.adapters === undefined) { 67 | sails.adapters = {}; 68 | } 69 | }, 70 | 71 | initModels: function() { 72 | if(sails.models === undefined) { 73 | sails.models = {}; 74 | } 75 | } 76 | }; 77 | }; 78 | 79 | -------------------------------------------------------------------------------- /actions/find.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var actionUtil = require('../actionUtil'), 5 | _ = require('lodash'); 6 | 7 | /** 8 | * Find Records 9 | * 10 | * get /:modelIdentity 11 | * * /:modelIdentity/find 12 | * 13 | * An API call to find and return model instances from the data adapter 14 | * using the specified criteria. If an id was specified, just the instance 15 | * with that unique id will be returned. 16 | * 17 | * There are four parameters associated with pagination: limit, skip, page 18 | * and perPage. They are meant to be used only in pairs of {limit, skip} 19 | * or {page, perPage}. If the latter pair is specified, then it is used. 20 | * 21 | * Optional: 22 | * @param {Object} where - the find criteria (passed directly to the ORM) 23 | * @param {Integer} limit - the maximum number of records to send back (useful for pagination) 24 | * @param {Integer} skip - the number of records to skip (useful for pagination) 25 | * @param {Integer} page - the page number to use when limiting records to send back using perPage (useful for pagination) 26 | * @param {Integer} perPage - the maximum number of records to send back (useful for pagination) 27 | * @param {String} sort - the order of returned records, e.g. `name ASC` or `age DESC` 28 | * @param {String} callback - default jsonp callback param (i.e. the name of the js function returned) 29 | */ 30 | 31 | module.exports = function findRecords (req, res) { 32 | // Look up the model 33 | var Model = actionUtil.parseModel(req); 34 | 35 | var limit = actionUtil.parseLimit(req), 36 | offset = actionUtil.parseSkip(req), 37 | page = actionUtil.parsePage(req), 38 | perPage = actionUtil.parsePerPage(req), 39 | populate = actionUtil.populateEach(req); 40 | 41 | if(page && perPage){ 42 | limit = perPage; 43 | offset = (page - 1) * (perPage + 1); 44 | } 45 | 46 | // If an `id` param was specified, use the findOne blueprint action 47 | // to grab the particular instance with its primary key === the value 48 | // of the `id` param. (mainly here for compatibility for 0.9, where 49 | // there was no separate `findOne` action) 50 | if ( actionUtil.parsePk(req) ) { 51 | return require('./findOne')(req,res); 52 | } 53 | // Lookup for records that match the specified criteria 54 | Model.findAll({ 55 | where: actionUtil.parseCriteria(req), 56 | limit: limit, 57 | offset: offset, 58 | order: actionUtil.parseSort(req), 59 | include: req._sails.config.blueprints.populate ? 60 | (_.isEmpty(populate) ? [{ all : true}] : populate) : [] 61 | }).then(function(matchingRecords) { 62 | // Only `.watch()` for new instances of the model if 63 | // `autoWatch` is enabled. 64 | if (req._sails.hooks.pubsub && req.isSocket) { 65 | Model.subscribe(req, matchingRecords); 66 | if (req.options.autoWatch) { Model.watch(req); } 67 | // Also subscribe to instances of all associated models 68 | _.each(matchingRecords, function (record) { 69 | actionUtil.subscribeDeep(req, record); 70 | }); 71 | } 72 | 73 | res.ok(matchingRecords); 74 | }).catch(function(err){ 75 | return res.serverError(err); 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /actions/populate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var util = require('util'), 5 | actionUtil = require('../actionUtil'); 6 | 7 | 8 | /** 9 | * Populate (or "expand") an association 10 | * 11 | * get /model/:parentid/relation 12 | * get /model/:parentid/relation/:id 13 | * 14 | * @param {Integer|String} parentid - the unique id of the parent instance 15 | * @param {Integer|String} id - the unique id of the particular child instance you'd like to look up within this relation 16 | * @param {Object} where - the find criteria (passed directly to the ORM) 17 | * @param {Integer} limit - the maximum number of records to send back (useful for pagination) 18 | * @param {Integer} skip - the number of records to skip (useful for pagination) 19 | * @param {String} sort - the order of returned records, e.g. `name ASC` or `age DESC` 20 | * 21 | * @option {String} model - the identity of the model 22 | * @option {String} alias - the name of the association attribute (aka "alias") 23 | */ 24 | 25 | module.exports = function expand(req, res) { 26 | var Model = actionUtil.parseModel(req); 27 | var relation = req.options.alias; 28 | if (!relation || !Model) return res.serverError(); 29 | 30 | // Allow customizable blacklist for params. 31 | req.options.criteria = req.options.criteria || {}; 32 | req.options.criteria.blacklist = req.options.criteria.blacklist || ['limit', 'skip', 'sort', 'id', 'parentid']; 33 | 34 | var parentPk = req.param('parentid'); 35 | 36 | // Determine whether to populate using a criteria, or the 37 | // specified primary key of the child record, or with no 38 | // filter at all. 39 | var childPk = actionUtil.parsePk(req); 40 | 41 | // Coerce the child PK to an integer if necessary 42 | if (childPk) { 43 | if (Model.attributes[Model.primaryKeys.id.fieldName].type == 'integer') { 44 | childPk = +childPk || 0; 45 | } 46 | } 47 | 48 | var where = childPk ? {id: [childPk]} : actionUtil.parseCriteria(req); 49 | 50 | var populate = sails.util.objCompact({ 51 | as: relation, 52 | model: sails.models[req.options.target.toLowerCase()], 53 | order: actionUtil.parseSort(req), 54 | where: where 55 | }); 56 | 57 | // Only get limit whether association type is HasMany 58 | if(Model.associations[relation].associationType === 'HasMany') 59 | populate.limit = actionUtil.parseLimit(req); 60 | 61 | Model.findById(parentPk, { include: [populate] }) 62 | .then(function(matchingRecord) { 63 | if (!matchingRecord) { 64 | if(Model.associations[relation].associationType === 'BelongsToMany') { 65 | if (_.has(where, 'id')) return res.notFound('No record found with the specified id.'); 66 | 67 | return res.send(200, []); 68 | } else { 69 | return res.notFound('No record found with the specified id.'); 70 | } 71 | } 72 | if (!matchingRecord[relation]) return res.notFound(util.format('Specified record (%s) is missing relation `%s`', parentPk, relation)); 73 | 74 | // Subcribe to instance, if relevant 75 | // TODO: only subscribe to populated attribute- not the entire model 76 | if (sails.hooks.pubsub && req.isSocket) { 77 | Model.subscribe(req, matchingRecord); 78 | actionUtil.subscribeDeep(req, matchingRecord); 79 | } 80 | return res.ok(matchingRecord[relation]); 81 | }).catch(function(err){ 82 | return res.serverError(err); 83 | }); 84 | }; 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################ 2 | ############### .gitignore ################## 3 | ################################################ 4 | # 5 | # This file is only relevant if you are using git. 6 | # 7 | # Files which match the splat patterns below will 8 | # be ignored by git. This keeps random crap and 9 | # and sensitive credentials from being uploaded to 10 | # your repository. It allows you to configure your 11 | # app for your machine without accidentally 12 | # committing settings which will smash the local 13 | # settings of other developers on your team. 14 | # 15 | # Some reasonable defaults are included below, 16 | # but, of course, you should modify/extend/prune 17 | # to fit your needs! 18 | ################################################ 19 | 20 | 21 | 22 | 23 | ################################################ 24 | # Local Configuration 25 | # 26 | # Explicitly ignore files which contain: 27 | # 28 | # 1. Sensitive information you'd rather not push to 29 | # your git repository. 30 | # e.g., your personal API keys or passwords. 31 | # 32 | # 2. Environment-specific configuration 33 | # Basically, anything that would be annoying 34 | # to have to change every time you do a 35 | # `git pull` 36 | # e.g., your local development database, or 37 | # the S3 bucket you're using for file uploads 38 | # development. 39 | # 40 | ################################################ 41 | 42 | config/local.js 43 | .env 44 | 45 | 46 | 47 | 48 | 49 | ################################################ 50 | # Dependencies 51 | # 52 | # When releasing a production app, you may 53 | # consider including your node_modules and 54 | # bower_components directory in your git repo, 55 | # but during development, its best to exclude it, 56 | # since different developers may be working on 57 | # different kernels, where dependencies would 58 | # need to be recompiled anyway. 59 | # 60 | # More on that here about node_modules dir: 61 | # http://www.futurealoof.com/posts/nodemodules-in-git.html 62 | # (credit Mikeal Rogers, @mikeal) 63 | # 64 | # About bower_components dir, you can see this: 65 | # http://addyosmani.com/blog/checking-in-front-end-dependencies/ 66 | # (credit Addy Osmani, @addyosmani) 67 | # 68 | ################################################ 69 | 70 | node_modules 71 | assets/components 72 | assets/dest/app.min.js 73 | 74 | ################################################ 75 | # Sails.js / Waterline / Grunt 76 | # 77 | # Files generated by Sails and Grunt, or related 78 | # tasks and adapters. 79 | ################################################ 80 | .tmp 81 | dump.rdb 82 | 83 | 84 | 85 | 86 | 87 | ################################################ 88 | # Node.js / NPM 89 | # 90 | # Common files generated by Node, NPM, and the 91 | # related ecosystem. 92 | ################################################ 93 | lib-cov 94 | *.seed 95 | *.log 96 | *.out 97 | *.pid 98 | npm-debug.log 99 | 100 | 101 | 102 | 103 | 104 | ################################################ 105 | # Miscellaneous 106 | # 107 | # Common files generated by text editors, 108 | # operating systems, file systems, etc. 109 | ################################################ 110 | 111 | *.swp 112 | *~ 113 | *# 114 | .DS_STORE 115 | .netbeans 116 | nbproject 117 | .idea 118 | .node_history 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sails-hook-sequelize-blueprints 2 | Sails blueprints for sequelize ORM 3 | 4 | [![Build Status](https://travis-ci.org/cesardeazevedo/sails-hook-sequelize-blueprints.svg)](https://travis-ci.org/cesardeazevedo/sails-hook-sequelize-blueprints) 5 | [![npm version](https://badge.fury.io/js/sails-hook-sequelize-blueprints.svg)](http://badge.fury.io/js/sails-hook-sequelize-blueprints) 6 | 7 | The blueprints waterline replaced with sequelize. 8 | 9 | # Install 10 | 11 | Install [sails-hook-sequelize](https://github.com/festo/sails-hook-sequelize) first: 12 | ```sh 13 | $ npm install sails-hook-sequelize --save 14 | ``` 15 | 16 | Install this hook with: 17 | 18 | ```sh 19 | $ npm install sails-hook-sequelize-blueprints --save 20 | ``` 21 | 22 | Sequelize dependencies: 23 | 24 | ```sh 25 | $ npm install --save sequelize 26 | $ npm install --save pg pg-hstore // in case of PostgreSQL 27 | $ npm install --save continuation-local-storage 28 | ``` 29 | 30 | # Configuration 31 | 32 | `.sailsrc` 33 | 34 | ``` 35 | "hooks": { 36 | "blueprints": false, 37 | "orm": false, 38 | "pubsub": false 39 | } 40 | ``` 41 | 42 | ## Blueprints 43 | 44 | Default blueprints configurations 45 | 46 | ```javascript 47 | module.exports.blueprints = { 48 | actions: true, 49 | index: true, 50 | shortcuts: true, 51 | rest: true, 52 | prefix: '', 53 | restPrefix: '', 54 | pluralize: false, 55 | populate: true, 56 | defaultLimit: 30, 57 | populateLimit: 30, 58 | autoWatch: true, 59 | } 60 | ``` 61 | 62 | ## Connections 63 | Sequelize connection 64 | ```javascript 65 | somePostgresqlServer: { 66 | user: 'postgres', 67 | password: '', 68 | database: 'sequelize', 69 | dialect: 'postgres', 70 | options: { 71 | dialect: 'postgres', 72 | host : 'localhost', 73 | port : 5432, 74 | logging: true 75 | } 76 | } 77 | ``` 78 | 79 | ## Models 80 | Sequelize model definition 81 | `models/user.js` 82 | ```javascript 83 | module.exports = { 84 | attributes: { 85 | name: { 86 | type: Sequelize.STRING, 87 | allowNull: false 88 | }, 89 | age: { 90 | type: Sequelize.INTEGER 91 | } 92 | }, 93 | associations: function() { 94 | user.hasMany(image, { 95 | foreignKey: { 96 | name: 'owner', 97 | allowNull: false 98 | } 99 | }); 100 | user.belongsToMany(affiliation, { 101 | as: 'affiliations', 102 | to: 'users', // must be named as the alias in the related Model 103 | through: 'UserAffiliation', 104 | foreignKey: { 105 | name: 'userId', 106 | as: 'affiliations' 107 | } 108 | }); 109 | }, 110 | options: { 111 | tableName: 'user', 112 | classMethods: {}, 113 | instanceMethods: {}, 114 | hooks: {} 115 | } 116 | }; 117 | ``` 118 | 119 | # Credits 120 | A big thanks to [festo/sailsjs-sequelize-example](https://github.com/festo/sailsjs-sequelize-example) and [Manuel Darveau's answer](https://groups.google.com/forum/#!msg/sailsjs/ALMxbKfnCIo/H2RcRUnnFGE) that turn this possible with thier sequelize implementations. 121 | 122 | [Munkacsy.me](http://munkacsy.me/use-sequelize-with-sails-js/) 123 | 124 | # Contributions 125 | 126 | 1. Fork it! 127 | 2. Create your feature branch: git checkout -b my-new-feature 128 | 3. Commit your changes: git commit -m 'Add some feature' 129 | 4. Push to the branch: git push origin my-new-feature 130 | 5. Submit a pull request 131 | 132 | # License 133 | [MIT](./LICENSE) 134 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/views/layout.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | New Sails App 5 | 6 | 7 | 8 | 9 | 10 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | <%- body %> 38 | 39 | 40 | 41 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /actions/remove.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var actionUtil = require('../actionUtil'); 5 | var _ = require('lodash'); 6 | 7 | 8 | /** 9 | * Remove a member from an association 10 | * 11 | * @param {Integer|String} parentid - the unique id of the parent record 12 | * @param {Integer|String} id - the unique id of the child record to remove 13 | * 14 | * @option {String} model - the identity of the model 15 | * @option {String} alias - the name of the association attribute (aka "alias") 16 | */ 17 | 18 | module.exports = function remove(req, res) { 19 | 20 | // Ensure a model and alias can be deduced from the request. 21 | var Model = actionUtil.parseModel(req); 22 | var relation = req.options.alias; 23 | if (!relation) { 24 | return res.serverError(new Error('Missing required route option, `req.options.alias`.')); 25 | } 26 | 27 | // The primary key of the parent record 28 | var parentPk = req.param('parentid'); 29 | 30 | // Get the model class of the child in order to figure out the name of 31 | // the primary key attribute. 32 | var foreign = Model.associations[relation].options.foreignKey; 33 | var ChildModel = sails.models[req.options.target.toLowerCase()]; 34 | var childPkAttr = ChildModel.primaryKeys.id.fieldName; 35 | 36 | // The primary key of the child record to remove 37 | // from the aliased collection 38 | var childPk = actionUtil.parsePk(req); 39 | var childRemove = {}; 40 | childRemove[childPkAttr] = childPk; 41 | 42 | var isManyToManyThrough = false; 43 | // check it is a M-M through 44 | if (_.has(Model.associations[relation].options, 'through')) { 45 | isManyToManyThrough = true; 46 | var through = Model.associations[relation].options.through.model; 47 | var ThroughModel = sails.models[through.toLowerCase()]; 48 | var childRelation = Model.associations[relation].options.to; 49 | var childForeign = ChildModel.associations[childRelation].options.foreignKey; 50 | var childAttr = childForeign.name || childForeign; 51 | } 52 | 53 | if(_.isUndefined(childPk)) { 54 | return res.serverError('Missing required child PK.'); 55 | } 56 | 57 | Model.findById(parentPk, { include: [{ all: true }]}).then(function(parentRecord) { 58 | if (!parentRecord) return res.notFound(); 59 | if (!parentRecord[relation]) return res.notFound(); 60 | 61 | if (isManyToManyThrough) { 62 | var throughRemove = { }; 63 | throughRemove[childAttr] = childPk; 64 | ThroughModel.destroy({ where: throughRemove }).then(function(){ 65 | return returnParentModel(); 66 | }) 67 | .catch(function(err) { 68 | return res.negotiate(err); 69 | }); 70 | } else { // not M-M 71 | ChildModel.destroy({ where: childRemove }).then(function(){ 72 | return returnParentModel(); 73 | }).catch(function(err){ 74 | return res.negotiate(err); 75 | }); 76 | } 77 | }).catch(function(err){ 78 | return res.serverError(err); 79 | }); 80 | 81 | function returnParentModel () { 82 | Model.findById(parentPk, { include: req._sails.config.blueprints.populate ? [{ all: true }] : [] }) 83 | // .populate(relation) 84 | // TODO: use populateEach util instead 85 | .then(function(parentRecord) { 86 | if (!parentRecord) return res.serverError(); 87 | if (!parentRecord[Model.primaryKeys.id.fieldName]) return res.serverError(); 88 | 89 | // If we have the pubsub hook, use the model class's publish method 90 | // to notify all subscribers about the removed item 91 | if (sails.hooks.pubsub) { 92 | Model.publishRemove(parentRecord[Model.primaryKey], relation, childPk, !sails.config.blueprints.mirror && req); 93 | } 94 | 95 | return res.ok(parentRecord); 96 | }).catch(function(err){ 97 | return res.serverError(err); 98 | }); 99 | } 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/views/403.ejs: -------------------------------------------------------------------------------- 1 | 2 | 36 | 37 | 38 | Forbidden 39 | 40 | 44 | 45 | 46 | 47 |
48 |
49 | 50 |
51 | 52 |
53 |

54 | Forbidden 55 |

56 |

57 | <% if (typeof error !== 'undefined') { %> 58 | <%= error %> 59 | <% } else { %> 60 | You don't have permission to see the page you're trying to reach. 61 | <% } %> 62 |

63 |

64 | Why might this be happening? 65 |

66 |
67 | 68 | 73 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/views/404.ejs: -------------------------------------------------------------------------------- 1 | 2 | 36 | 37 | 38 | Page Not Found 39 | 40 | 44 | 45 | 46 | 47 |
48 |
49 | 50 |
51 | 52 |
53 |

54 | Something's fishy here. 55 |

56 |

57 | <% if (typeof error!== 'undefined') { %> 58 | <%= error %> 59 | <% } else { %> 60 | The page you were trying to reach doesn't exist. 61 | <% } %> 62 |

63 |

64 | Why might this be happening? 65 |

66 |
67 | 68 | 73 |
74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /actions/update.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var actionUtil = require('../actionUtil'); 6 | var util = require('util'); 7 | var _ = require('lodash'); 8 | 9 | 10 | /** 11 | * Update One Record 12 | * 13 | * An API call to update a model instance with the specified `id`, 14 | * treating the other unbound parameters as attributes. 15 | * 16 | * @param {Integer|String} id - the unique id of the particular record you'd like to update (Note: this param should be specified even if primary key is not `id`!!) 17 | * @param * - values to set on the record 18 | * 19 | */ 20 | module.exports = function updateOneRecord (req, res) { 21 | 22 | // Look up the model 23 | var Model = actionUtil.parseModel(req); 24 | 25 | // Locate and validate the required `id` parameter. 26 | var pk = actionUtil.requirePk(req); 27 | 28 | // Create `values` object (monolithic combination of all parameters) 29 | // But omit the blacklisted params (like JSONP callback param, etc.) 30 | var values = actionUtil.parseValues(req); 31 | 32 | // Omit the path parameter `id` from values, unless it was explicitly defined 33 | // elsewhere (body/query): 34 | var idParamExplicitlyIncluded = ((req.body && req.body.id) || req.query.id); 35 | if (!idParamExplicitlyIncluded) delete values.id; 36 | 37 | // No matter what, don't allow changing the PK via the update blueprint 38 | // (you should just drop and re-add the record if that's what you really want) 39 | if (typeof values[Model.primaryKey] !== 'undefined') { 40 | req._sails.log.warn('Cannot change primary key via update blueprint; ignoring value sent for `' + Model.primaryKey + '`'); 41 | } 42 | delete values[Model.primaryKey]; 43 | 44 | // Find and update the targeted record. 45 | // 46 | // (Note: this could be achieved in a single query, but a separate `findOne` 47 | // is used first to provide a better experience for front-end developers 48 | // integrating with the blueprint API.) 49 | Model.findById(pk).then(function(matchingRecord) { 50 | 51 | if (!matchingRecord) return res.notFound(); 52 | 53 | Model.update(values, { where: { id: pk }}).then(function(records) { 54 | // Because this should only update a single record and update 55 | // returns an array, just use the first item. If more than one 56 | // record was returned, something is amiss. 57 | if (!records || !records.length || records.length > 1) { 58 | req._sails.log.warn(util.format('Unexpected output from `%s.update`.', Model.globalId)); 59 | } 60 | 61 | var updatedRecord = pk; 62 | 63 | // If we have the pubsub hook, use the Model's publish method 64 | // to notify all subscribers about the update. 65 | if (req._sails.hooks.pubsub) { 66 | if (req.isSocket) { Model.subscribe(req, records); } 67 | Model.publishUpdate(pk, _.cloneDeep(values), !req.options.mirror && req, { 68 | previous: _.cloneDeep(matchingRecord.toJSON()) 69 | }); 70 | } 71 | 72 | // Do a final query to populate the associations of the record. 73 | // 74 | // (Note: again, this extra query could be eliminated, but it is 75 | // included by default to provide a better interface for integrating 76 | // front-end developers.) 77 | var Q = Model.findById(updatedRecord, {include: req._sails.config.blueprints.populate ? [{ all: true }] : []}) 78 | .then(function(populatedRecord) { 79 | if (!populatedRecord) return res.serverError('Could not find record after updating!'); 80 | res.ok(populatedRecord); 81 | }).catch(function(err){ 82 | return res.serverError(err); 83 | }); // 84 | }).catch(function(err){ 85 | // Differentiate between waterline-originated validation errors 86 | // and serious underlying issues. Respond with badRequest if a 87 | // validation error is encountered, w/ validation info. 88 | return res.negotiate(err); 89 | }); 90 | }).catch(function(err){ 91 | return res.serverError(err); 92 | }); 93 | }; 94 | -------------------------------------------------------------------------------- /onRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var _ = require('lodash'), 6 | util = require('sails-util'); 7 | 8 | 9 | 10 | // NOTE: 11 | // Since controllers load blueprint actions by default anyways, this route syntax handler 12 | // can be replaced with `{action: 'find'}, {action: 'create'}, ...` etc. 13 | 14 | 15 | /** 16 | * Expose route parser. 17 | * @type {Function} 18 | */ 19 | module.exports = function (sails) { 20 | 21 | 22 | return interpretRouteSyntax; 23 | 24 | 25 | 26 | /** 27 | * interpretRouteSyntax 28 | * 29 | * "Teach" router to understand direct references to blueprints 30 | * as a target to sails.router.bind() 31 | * (i.e. in the `routes.js` file) 32 | * 33 | * @param {[type]} route [description] 34 | * @return {[type]} [description] 35 | * @api private 36 | */ 37 | function interpretRouteSyntax (route) { 38 | var target = route.target, 39 | path = route.path, 40 | verb = route.verb, 41 | options = route.options; 42 | 43 | // Support referencing blueprints in explicit routes 44 | // (`{ blueprint: 'create' }` et. al.) 45 | if ( 46 | _.isObject(target) && 47 | !_.isFunction(target) && 48 | !_.isArray(target) && 49 | _.isString(target.blueprint)) { 50 | 51 | // On a match, merge leftover items in the target object into route options: 52 | options = _.merge(options, _.omit(target, 'blueprint')); 53 | return bindBlueprintAction(path, target.blueprint, verb, options); 54 | } 55 | 56 | // Ignore unknown route syntax 57 | // If it needs to be understood by another hook, the hook would have also received 58 | // the typeUnknown event, so we're done. 59 | return; 60 | } 61 | 62 | 63 | 64 | /** 65 | * Bind explicit route to a blueprint action. 66 | * 67 | * @param {[type]} path [description] 68 | * @param {[type]} blueprintActionID [description] 69 | * @param {[type]} verb [description] 70 | * @param {[type]} options [description] 71 | * @return {[type]} [description] 72 | * @api private 73 | */ 74 | function bindBlueprintAction ( path, blueprintActionID, verb, options ) { 75 | 76 | // Look up appropriate blueprint action and make sure it exists 77 | var blueprint = sails.middleware.blueprints[blueprintActionID]; 78 | 79 | // If a 'blueprint' was specified, but it doesn't exist, warn the user and ignore it. 80 | if ( ! ( blueprint && _.isFunction(blueprint) )) { 81 | sails.log.error( 82 | blueprintActionID, 83 | ':: Ignoring attempt to bind route (' + path + ') to unknown blueprint action (`'+blueprintActionID+'`).' 84 | ); 85 | return; 86 | } 87 | 88 | // If a model wasn't provided with the options, try and guess it 89 | if (!options.model) { 90 | var matches = path.match(/^\/(\w+).*$/); 91 | if (matches && matches[1] && sails.models[matches[1]]) { 92 | options.model = matches[1]; 93 | } 94 | else { 95 | sails.log.error( 96 | blueprintActionID, 97 | ':: Ignoring attempt to bind route (' + path + ') to blueprint action (`'+blueprintActionID+'`), but no valid model was specified and we couldn\'t guess one based on the path.' 98 | ); 99 | return; 100 | } 101 | } 102 | 103 | // If associations weren't provided with the options, try and get them 104 | if (!options.associations) { 105 | options = _.merge({ associations: _.cloneDeep(sails.models[options.model].associations) }, options); 106 | } 107 | // Otherwise make sure it's an array of strings of valid association aliases 108 | else { 109 | options.associations = options.associations.map(function(alias) { 110 | if (typeof(alias) != 'string') { 111 | sails.log.error( 112 | blueprintActionID, 113 | ':: Ignoring invalid association option for '+path+'.' 114 | ); 115 | return; 116 | } 117 | var association; 118 | if (!(association = _.findWhere(sails.models[options.model].associations, {alias: alias}))) { 119 | sails.log.error( 120 | blueprintActionID, 121 | ':: Ignoring invalid association option `'+alias+'` for '+path+'.' 122 | ); 123 | return; 124 | } 125 | return association; 126 | }); 127 | } 128 | 129 | // If "populate" wasn't provided in the options, use the default 130 | if (typeof (options.populate) == 'undefined') { 131 | options.populate = sails.config.blueprints.populate; 132 | } 133 | 134 | sails.router.bind(path, blueprint, verb, options); 135 | 136 | return; 137 | } 138 | 139 | }; 140 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : false, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 14 | "indent" : 4, // {int} Number of spaces to use for indentation 15 | "latedef" : false, // true: Require variables/functions to be defined before being used 16 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 17 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 18 | "noempty" : true, // true: Prohibit use of empty blocks 19 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : false, // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : true, // true: Require all defined variables be used 28 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 29 | "maxparams" : false, // {int} Max number of formal params allowed per function 30 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 31 | "maxstatements" : false, // {int} Max number statements per function 32 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 33 | "maxlen" : false, // {int} Max number of characters per line 34 | 35 | // Relaxing 36 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 37 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 38 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 39 | "eqnull" : false, // true: Tolerate use of `== null` 40 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 41 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 42 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 43 | // (ex: `for each`, multiple try/catch, function expression…) 44 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 45 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 46 | "funcscope" : false, // true: Tolerate defining variables inside control statements 47 | "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') 48 | "iterator" : false, // true: Tolerate using the `__iterator__` property 49 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 50 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 51 | "laxcomma" : true, // true: Tolerate comma-first style coding 52 | "loopfunc" : false, // true: Tolerate functions being defined in loops 53 | "multistr" : false, // true: Tolerate multi-line strings 54 | "proto" : false, // true: Tolerate using the `__proto__` property 55 | "scripturl" : false, // true: Tolerate script-targeted URLs 56 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 57 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 58 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 59 | "validthis" : false, // true: Tolerate using this in a non-constructor function 60 | 61 | // Environments 62 | "browser" : true, // Web Browser (window, document, etc) 63 | "browserify" : false, // Browserify (node.js code in the browser) 64 | "couch" : false, // CouchDB 65 | "devel" : true, // Development/debugging (alert, confirm, etc) 66 | "dojo" : false, // Dojo Toolkit 67 | "jquery" : false, // jQuery 68 | "mootools" : false, // MooTools 69 | "node" : true, // Node.js 70 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 71 | "prototypejs" : false, // Prototype and Scriptaculous 72 | "rhino" : false, // Rhino 73 | "worker" : false, // Web Workers 74 | "wsh" : false, // Windows Scripting Host 75 | "yui" : false, // Yahoo User Interface 76 | 77 | // Custom Globals 78 | // additional predefined global variables 79 | "globals" : { "angular" : true 80 | , "sails" : true 81 | , "Passport" : true 82 | , "passport" : true 83 | , "token" : true 84 | , "User" : true 85 | , "_" : true 86 | , "after" : true 87 | , "before" : true 88 | , "describe" : true 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/views/homepage.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 12 | 13 |
14 |
15 |

<%= __('A brand new app.') %>

16 |

You're looking at: <%= view.pathFromApp + '.' +view.ext %>

17 |
18 |
19 | 21 | 53 | 73 |
74 |
75 | -------------------------------------------------------------------------------- /actions/add.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | var actionUtil = require('../actionUtil'); 5 | var _ = require('lodash'); 6 | var async = require('async'); 7 | 8 | /** 9 | * Add Record To Collection 10 | * 11 | * post /:modelIdentity/:id/:collectionAttr/:childid 12 | * * /:modelIdentity/:id/:collectionAttr/add/:childid 13 | * 14 | * post /:modelIdentity/:id/:collectionAttr?:childField=Value&childAnotherField=AnotherValue 15 | * * /:modelIdentity/:id/:collectionAttr/add?:childField=Value&childAnotherField=AnotherValue 16 | * 17 | * Associate one record with the collection attribute of another. 18 | * e.g. add a Horse named "Jimmy" to a Farm's "animals". 19 | * If the record being added has a primary key value already, it will 20 | * just be linked. If it doesn't, a new record will be created, then 21 | * linked appropriately. In either case, the association is bidirectional. 22 | * 23 | * @param {Integer|String} parentid - the unique id of the parent record 24 | * @param {Integer|String} id [optional] 25 | * - the unique id of the child record to add 26 | * Alternatively, an object WITHOUT a primary key may be POSTed 27 | * to this endpoint to create a new child record, then associate 28 | * it with the parent. 29 | * 30 | * @option {String} model - the identity of the model 31 | * @option {String} alias - the name of the association attribute (aka "alias") 32 | */ 33 | 34 | module.exports = function addToCollection (req, res) { 35 | // Ensure a model and alias can be deduced from the request. 36 | var Model = actionUtil.parseModel(req); 37 | var relation = req.options.alias; 38 | if (!relation) { 39 | return res.serverError(new Error('Missing required route option, `req.options.alias`.')); 40 | } 41 | // The primary key of the parent record 42 | var parentPk = req.param('parentid'); 43 | // Get the model class of the child in order to figure out the name of 44 | // the primary key attribute. 45 | var foreign = Model.associations[relation].options.foreignKey; 46 | var associationAttr = foreign.name || foreign; 47 | var ChildModel = sails.models[req.options.target.toLowerCase()]; 48 | var isManyToManyThrough = false; 49 | // check it is a M-M through 50 | if (_.has(Model.associations[relation].options, 'through')) { 51 | isManyToManyThrough = true; 52 | var through = Model.associations[relation].options.through.model; 53 | var ThroughModel = sails.models[through.toLowerCase()]; 54 | var childRelation = Model.associations[relation].options.to; 55 | var childForeign = ChildModel.associations[childRelation].options.foreignKey; 56 | var childAttr = childForeign.name || childForeign; 57 | } 58 | var childPkAttr = ChildModel.primaryKeys.id.fieldName; 59 | // The child record to associate is defined by either... 60 | var child; 61 | 62 | // ...a primary key: 63 | var supposedChildPk = actionUtil.parsePk(req); 64 | if (supposedChildPk) { 65 | child = {}; 66 | child[childPkAttr] = supposedChildPk; 67 | } 68 | // ...or an object of values: 69 | else { 70 | req.options.values = req.options.values || {}; 71 | req.options.values.blacklist = req.options.values.blacklist || ['limit', 'skip', 'sort', 'id', 'parentid']; 72 | child = actionUtil.parseValues(req); 73 | } 74 | if (!child) { 75 | res.badRequest('You must specify the record to add (either the primary key of an existing record to link, or a new object without a primary key which will be used to create a record then link it.)'); 76 | } 77 | 78 | // add pk parent to child 79 | child[associationAttr] = parentPk; 80 | 81 | async.auto({ 82 | 83 | // Look up the parent record 84 | parent: function (cb) { 85 | Model.findById(parentPk, { include: [{ all : true }]}).then(function(parentRecord) { 86 | if (!parentRecord) return cb({status: 404}); 87 | if (!parentRecord[relation]) { return cb({status: 404}); } 88 | cb(null, parentRecord); 89 | }).catch(function(err){ 90 | return cb(err); 91 | }); 92 | }, 93 | 94 | // If a primary key was specified in the `child` object we parsed 95 | // from the request, look it up to make sure it exists. Send back its primary key value. 96 | // This is here because, although you can do this with `.save()`, you can't actually 97 | // get ahold of the created child record data, unless you create it first. 98 | actualChildPkValue: ['parent', function(cb) { 99 | // Below, we use the primary key attribute to pull out the primary key value 100 | // (which might not have existed until now, if the .add() resulted in a `create()`) 101 | 102 | // If the primary key was specified for the child record, we should try to find 103 | // it before we create it. 104 | if (isManyToManyThrough) { 105 | 106 | 107 | 108 | 109 | if (supposedChildPk) { 110 | // update just the through model with boths IDS => parentPK + supposedChildPk 111 | // needed the name of foreign keys (got parent, gets target) 112 | // throughModel update ({ associationAttr: parentPk, targetAttr: supposedChildPk }) 113 | var create = { }; 114 | create[associationAttr] = parentPk; 115 | create[childAttr] = supposedChildPk; 116 | ThroughModel.create(create).then(function(throughModelInstance) { 117 | return cb(); 118 | }) 119 | .catch(function(err) { 120 | return cb(err); 121 | }); 122 | } else { 123 | // Otherwise, it must be referring to a new thing, so create it. 124 | // and update the through model 125 | createChild(function(err, childInstanceId) { 126 | if (err) cb(err); 127 | 128 | var create = { }; 129 | create[associationAttr] = parentPk; 130 | create[childAttr] = childInstanceId; 131 | ThroughModel.create(create).then(function(throughModelInstance) { 132 | return cb(); 133 | }) 134 | .catch(function(err) { 135 | return cb(err); 136 | }); 137 | }); 138 | } 139 | } else { 140 | if (child[childPkAttr]) { 141 | ChildModel.findById(child[childPkAttr]).then(function(childRecord) { 142 | // if there is no real update, no update 143 | // if (childRecord[associationAttr] === parentPk) return cb(null, childRecord[childPkAttr]); 144 | // Didn't find it? Then try creating it. 145 | if (!childRecord) {return createChild();} 146 | // Otherwise use the one we found. 147 | // UPDATE THE CHILD WITH PARENTPK 148 | childRecord[associationAttr] = parentPk; 149 | childRecord.save().then(function() { 150 | return cb(null, childRecord[childPkAttr]); 151 | }); 152 | }).catch(function(err){ 153 | return cb(err); 154 | }); 155 | } 156 | // Otherwise, it must be referring to a new thing, so create it. 157 | else { 158 | return createChild(); 159 | } 160 | } 161 | 162 | // Create a new instance and send out any required pubsub messages. 163 | function createChild(customCb) { 164 | 165 | 166 | ChildModel.create(child).then(function(newChildRecord){ 167 | if (req._sails.hooks.pubsub) { 168 | if (req.isSocket) { 169 | ChildModel.subscribe(req, newChildRecord); 170 | ChildModel.introduce(newChildRecord); 171 | } 172 | ChildModel.publishCreate(newChildRecord, !req.options.mirror && req); 173 | } 174 | // in case we have to create a child and link it to parent(M-M through scenario) 175 | // createChild function should return the instance to be linked 176 | // in the through model => customCb 177 | return (typeof customCb === 'function') ? 178 | customCb(null, newChildRecord[childPkAttr]) : cb(null, newChildRecord[childPkAttr]); 179 | }).catch(function(err){ 180 | return cb(err); 181 | }); 182 | } 183 | }] 184 | }, function(err, results){ 185 | // if (err) return res.negotiate(err); 186 | 187 | Model.findById(parentPk, { include: req._sails.config.blueprints.populate ? [{ all: true }] : []}).then(function(matchingRecord) { 188 | if(!matchingRecord) return res.serverError(); 189 | return res.ok(matchingRecord); 190 | }).catch(function(err) { 191 | return res.serverError(err); 192 | }); 193 | }); 194 | }; 195 | -------------------------------------------------------------------------------- /test/unit/controllers/Blueprint.test.js: -------------------------------------------------------------------------------- 1 | var request = require('supertest'); 2 | var should = require('should'); 3 | 4 | describe('Sequelize Blueprint User', function(){ 5 | 6 | it('Get users', function(done){ 7 | request(sails.hooks.http.app) 8 | .get('/user') 9 | .expect(200, done); 10 | }); 11 | 12 | it('Create a user', function(done){ 13 | request(sails.hooks.http.app) 14 | .post('/user') 15 | .send({ name: 'Tester', age: 21 }) 16 | .expect(201) 17 | .end(function(err, response){ 18 | if(err) { 19 | return done(err); 20 | } 21 | 22 | response.body.should.be.type('object').and.have.property('name', 'Tester'); 23 | user.id = response.body.id; 24 | done(); 25 | }); 26 | }); 27 | 28 | it('Create a pet', function(done){ 29 | request(sails.hooks.http.app) 30 | .post('/pet') 31 | .send({ name: 'Max', breed: 'bulldog', userId: user.id }) 32 | .expect(201) 33 | .end(function(err, response){ 34 | if(err) { 35 | return done(err); 36 | } 37 | 38 | response.body.should.be.type('object').and.have.property('name', 'Max'); 39 | done(); 40 | }); 41 | }); 42 | 43 | it('Should update user name and populate all relations', function(done){ 44 | request(sails.hooks.http.app) 45 | .put('/user/'+user.id) 46 | .send({ name: 'TesterEdited' }) 47 | .expect(200) 48 | .end(function(err, response){ 49 | if(err) { 50 | return done(err); 51 | } 52 | 53 | response.body.should.be.type('object').and.have.property('name', 'TesterEdited'); 54 | response.body.should.be.type('object').and.have.property('pets'); 55 | response.body.should.be.type('object').and.have.property('images'); 56 | user.id = response.body.id; 57 | done(); 58 | }); 59 | }); 60 | 61 | it('Should update a user and not return relations', function(done){ 62 | sails.config.blueprints.populate = false; 63 | 64 | request(sails.hooks.http.app) 65 | .put('/user/'+user.id) 66 | .send({ name: 'TesterEdited 2' }) 67 | .expect(200) 68 | .end(function(err, response){ 69 | if(err) { 70 | return done(err); 71 | } 72 | 73 | response.body.should.be.type('object').and.have.property('name', 'TesterEdited 2'); 74 | response.body.should.be.type('object').and.not.have.property('pets'); 75 | response.body.should.be.type('object').and.not.have.property('images'); 76 | user.id = response.body.id; 77 | 78 | sails.config.blueprints.populate = true; 79 | done(); 80 | }); 81 | }) 82 | 83 | it('Create an image for the user', function(done){ 84 | request(sails.hooks.http.app) 85 | .post('/image') 86 | .send({ url: 'http:image.com/images.png', userId: 1 }) 87 | .expect(201, done); 88 | }); 89 | 90 | it('Get users', function(done){ 91 | request(sails.hooks.http.app) 92 | .get('/user') 93 | .expect(200, done); 94 | }); 95 | 96 | it('Get single user', function(done){ 97 | request(sails.hooks.http.app) 98 | .get('/user/1') 99 | .expect(200, done); 100 | }); 101 | 102 | it('Create an image without an owner', function(done){ 103 | request(sails.hooks.http.app) 104 | .post('/image') 105 | .send({ url: 'http:image.com/images.png' }) 106 | .expect(500, done); 107 | }); 108 | 109 | it('Add an image to a user', function(done){ 110 | request(sails.hooks.http.app) 111 | .post('/user/1/images/add') 112 | .send({ url: 'a.png' }) 113 | .expect(200) 114 | .end(function(err, response){ 115 | if(err) 116 | return done(err); 117 | 118 | response.body.images.should.be.an.instanceOf(Array); 119 | done(); 120 | }); 121 | }); 122 | 123 | it('Get images from a user', function(done){ 124 | request(sails.hooks.http.app) 125 | .get('/user/1/images') 126 | .expect(200) 127 | .end(function(err, response){ 128 | if(err) 129 | return done(err); 130 | 131 | response.body.should.be.an.instanceOf(Array); 132 | done(); 133 | }); 134 | }); 135 | 136 | it('Get an sigle image from a user', function(done){ 137 | request(sails.hooks.http.app) 138 | .get('/user/1/images/1') 139 | .expect(200) 140 | .end(function(err, response){ 141 | if(err) 142 | return done(err); 143 | 144 | response.body.should.have.length(1); 145 | response.body[0].should.have.a.property('url'); 146 | done(); 147 | }); 148 | }); 149 | 150 | it('List images sorted by url', function(done){ 151 | request(sails.hooks.http.app) 152 | .get('/user/1/images/?sort=url') 153 | .expect(200) 154 | .end(function(err, response){ 155 | if(err) 156 | return done(err); 157 | 158 | response.body[0].url.should.be.exactly('a.png'); 159 | done(); 160 | }); 161 | }); 162 | 163 | it('List images with limit', function(done){ 164 | request(sails.hooks.http.app) 165 | .get('/user/1/images/?limit=1') 166 | .expect(200) 167 | .end(function(err, response){ 168 | if(err) 169 | return done(err); 170 | 171 | response.body.should.have.length(1); 172 | done(); 173 | }); 174 | }); 175 | 176 | it('List image owner', function(done){ 177 | request(sails.hooks.http.app) 178 | .get('/image/1/owner') 179 | .expect(200, done); 180 | }); 181 | 182 | it('Populate pet', function(done){ 183 | request(sails.hooks.http.app) 184 | .get('/user/?populate=pet') 185 | .expect(200) 186 | .end(function(err, response){ 187 | if(err) 188 | return done(err); 189 | 190 | response.body[0].should.have.property('pets'); 191 | done(); 192 | }); 193 | }); 194 | 195 | it('Populate user from pet', function(done){ 196 | request(sails.hooks.http.app) 197 | .get('/pet?populate=user') 198 | .expect(200) 199 | .end(function(err, response){ 200 | if(err) 201 | return done(err); 202 | 203 | response.body[0].should.have.property('owner'); 204 | done(); 205 | }); 206 | }); 207 | 208 | it('Populate pet and image', function(done){ 209 | request(sails.hooks.http.app) 210 | .get('/user/?populate=[pet,image]') 211 | .expect(200) 212 | .end(function(err, response){ 213 | if(err) 214 | return done(err); 215 | 216 | response.body[0].should.have.property('pets'); 217 | response.body[0].should.have.property('images'); 218 | done(); 219 | }); 220 | }); 221 | 222 | it('Should get a user and not return relations', function(done){ 223 | sails.config.blueprints.populate = false; 224 | 225 | request(sails.hooks.http.app) 226 | .get('/user') 227 | .expect(200) 228 | .end(function(err, response){ 229 | if(err) 230 | return done(err); 231 | 232 | response.body[0].should.not.have.property('pets'); 233 | response.body[0].should.not.have.property('images'); 234 | 235 | sails.config.blueprints.populate = true; 236 | done(); 237 | }); 238 | }); 239 | 240 | 241 | it('Remove an image from a user without relations', function(done){ 242 | sails.config.blueprints.populate = false; 243 | 244 | request(sails.hooks.http.app) 245 | .delete('/user/1/images/remove/1') 246 | .expect(200) 247 | .end(function(err, response){ 248 | if(err) 249 | return done(err); 250 | 251 | response.body.should.not.have.property('images'); 252 | response.body.should.not.have.property('pets'); 253 | 254 | sails.config.blueprints.populate = true; 255 | done(); 256 | }); 257 | }); 258 | 259 | it('Should delete a pet without return the owner', function(done){ 260 | sails.config.blueprints.populate = false; 261 | 262 | request(sails.hooks.http.app) 263 | .delete('/pet/1') 264 | .expect(200) 265 | .end(function(err, response){ 266 | if(err) 267 | return done(err); 268 | 269 | response.body.should.not.have.property('owner'); 270 | 271 | sails.config.blueprints.populate = true; 272 | done(); 273 | }); 274 | }); 275 | 276 | it('Delete an user and return the deleted record with all relations', function(done){ 277 | sails.config.blueprints.populate = true; 278 | 279 | request(sails.hooks.http.app) 280 | .delete('/user/1') 281 | .expect(200) 282 | .end(function(err, response){ 283 | if(err) 284 | return done(err); 285 | 286 | response.body.should.have.property('pets'); 287 | response.body.should.have.property('images'); 288 | 289 | done(); 290 | }); 291 | }); 292 | }); 293 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/config/blueprints.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Blueprint API Configuration 3 | * (sails.config.blueprints) 4 | * 5 | * These settings are for the global configuration of blueprint routes and 6 | * request options (which impact the behavior of blueprint actions). 7 | * 8 | * You may also override any of these settings on a per-controller basis 9 | * by defining a '_config' key in your controller defintion, and assigning it 10 | * a configuration object with overrides for the settings in this file. 11 | * A lot of the configuration options below affect so-called "CRUD methods", 12 | * or your controllers' `find`, `create`, `update`, and `destroy` actions. 13 | * 14 | * It's important to realize that, even if you haven't defined these yourself, as long as 15 | * a model exists with the same name as the controller, Sails will respond with built-in CRUD 16 | * logic in the form of a JSON API, including support for sort, pagination, and filtering. 17 | * 18 | * For more information on the blueprint API, check out: 19 | * http://sailsjs.org/#/documentation/reference/blueprint-api 20 | * 21 | * For more information on the settings in this file, see: 22 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.blueprints.html 23 | * 24 | */ 25 | 26 | module.exports.blueprints = { 27 | 28 | /*************************************************************************** 29 | * * 30 | * Action routes speed up the backend development workflow by * 31 | * eliminating the need to manually bind routes. When enabled, GET, POST, * 32 | * PUT, and DELETE routes will be generated for every one of a controller's * 33 | * actions. * 34 | * * 35 | * If an `index` action exists, additional naked routes will be created for * 36 | * it. Finally, all `actions` blueprints support an optional path * 37 | * parameter, `id`, for convenience. * 38 | * * 39 | * `actions` are enabled by default, and can be OK for production-- * 40 | * however, if you'd like to continue to use controller/action autorouting * 41 | * in a production deployment, you must take great care not to * 42 | * inadvertently expose unsafe/unintentional controller logic to GET * 43 | * requests. * 44 | * * 45 | ***************************************************************************/ 46 | 47 | // actions: true, 48 | 49 | /*************************************************************************** 50 | * * 51 | * RESTful routes (`sails.config.blueprints.rest`) * 52 | * * 53 | * REST blueprints are the automatically generated routes Sails uses to * 54 | * expose a conventional REST API on top of a controller's `find`, * 55 | * `create`, `update`, and `destroy` actions. * 56 | * * 57 | * For example, a BoatController with `rest` enabled generates the * 58 | * following routes: * 59 | * ::::::::::::::::::::::::::::::::::::::::::::::::::::::: * 60 | * GET /boat/:id? -> BoatController.find * 61 | * POST /boat -> BoatController.create * 62 | * PUT /boat/:id -> BoatController.update * 63 | * DELETE /boat/:id -> BoatController.destroy * 64 | * * 65 | * `rest` blueprint routes are enabled by default, and are suitable for use * 66 | * in a production scenario, as long you take standard security precautions * 67 | * (combine w/ policies, etc.) * 68 | * * 69 | ***************************************************************************/ 70 | 71 | // rest: true, 72 | 73 | /*************************************************************************** 74 | * * 75 | * Shortcut routes are simple helpers to provide access to a * 76 | * controller's CRUD methods from your browser's URL bar. When enabled, * 77 | * GET, POST, PUT, and DELETE routes will be generated for the * 78 | * controller's`find`, `create`, `update`, and `destroy` actions. * 79 | * * 80 | * `shortcuts` are enabled by default, but should be disabled in * 81 | * production. * 82 | * * 83 | ***************************************************************************/ 84 | 85 | // shortcuts: true, 86 | 87 | /*************************************************************************** 88 | * * 89 | * An optional mount path for all blueprint routes on a controller, * 90 | * including `rest`, `actions`, and `shortcuts`. This allows you to take * 91 | * advantage of blueprint routing, even if you need to namespace your API * 92 | * methods. * 93 | * * 94 | * (NOTE: This only applies to blueprint autoroutes, not manual routes from * 95 | * `sails.config.routes`) * 96 | * * 97 | ***************************************************************************/ 98 | 99 | // prefix: '', 100 | 101 | /*************************************************************************** 102 | * * 103 | * Whether to pluralize controller names in blueprint routes. * 104 | * * 105 | * (NOTE: This only applies to blueprint autoroutes, not manual routes from * 106 | * `sails.config.routes`) * 107 | * * 108 | * For example, REST blueprints for `FooController` with `pluralize` * 109 | * enabled: * 110 | * GET /foos/:id? * 111 | * POST /foos * 112 | * PUT /foos/:id? * 113 | * DELETE /foos/:id? * 114 | * * 115 | ***************************************************************************/ 116 | 117 | // pluralize: false, 118 | 119 | /*************************************************************************** 120 | * * 121 | * Whether the blueprint controllers should populate model fetches with * 122 | * data from other models which are linked by associations * 123 | * * 124 | * If you have a lot of data in one-to-many associations, leaving this on * 125 | * may result in very heavy api calls * 126 | * * 127 | ***************************************************************************/ 128 | 129 | // populate: true, 130 | 131 | /**************************************************************************** 132 | * * 133 | * Whether to run Model.watch() in the find and findOne blueprint actions. * 134 | * Can be overridden on a per-model basis. * 135 | * * 136 | ****************************************************************************/ 137 | 138 | // autoWatch: true, 139 | 140 | /**************************************************************************** 141 | * * 142 | * The default number of records to show in the response from a "find" * 143 | * action. Doubles as the default size of populated arrays if populate is * 144 | * true. * 145 | * * 146 | ****************************************************************************/ 147 | 148 | // defaultLimit: 30 149 | 150 | }; 151 | -------------------------------------------------------------------------------- /actionUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('lodash'), 6 | mergeDefaults = require('merge-defaults'), 7 | util = require('util'); 8 | 9 | 10 | // Parameter used for jsonp callback is constant, as far as 11 | // blueprints are concerned (for now.) 12 | var JSONP_CALLBACK_PARAM = 'callback'; 13 | 14 | 15 | 16 | /** 17 | * Utility methods used in built-in blueprint actions. 18 | * 19 | * @type {Object} 20 | */ 21 | module.exports = { 22 | 23 | /** 24 | * Given a request, return an object with appropriate/specified 25 | * association attributes ( i.e. [{ model: Pet, as: 'pets' }] ) 26 | * 27 | * @param {Request} req 28 | * @return {Object} 29 | */ 30 | populateEach: function (req) { 31 | var DEFAULT_POPULATE_LIMIT = req._sails.config.blueprints.defaultLimit || 30; 32 | var aliasFilter = req.param('populate'); 33 | var associations = []; 34 | var parentModel = req.options.model; 35 | 36 | // Convert the string representation of the filter list to an Array. We 37 | // need this to provide flexibility in the request param. This way both 38 | // list string representations are supported: 39 | // /model?populate=alias1,alias2,alias3 40 | // /model?populate=[alias1,alias2,alias3] 41 | if (typeof aliasFilter === 'string') { 42 | aliasFilter = aliasFilter.replace(/\[|\]/g, ''); 43 | aliasFilter = (aliasFilter) ? aliasFilter.split(',') : []; 44 | } 45 | 46 | _.each(aliasFilter, function(association){ 47 | var childModel = sails.models[association.toLowerCase()]; 48 | // iterate through parent model associations 49 | _.each(sails.models[parentModel].associations, function(relation){ 50 | // check if association match childModel name 51 | if(relation.target.name === childModel.name) { 52 | var obj = { model: childModel, as: relation.options.as }; 53 | if(relation.associationType === 'HasMany') { 54 | obj.limit = req._sails.config.blueprints.populateLimit || DEFAULT_POPULATE_LIMIT; 55 | } 56 | associations.push(obj); 57 | } 58 | }); 59 | }); 60 | 61 | return associations; 62 | }, 63 | 64 | /** 65 | * Subscribe deep (associations) 66 | * 67 | * @param {[type]} associations [description] 68 | * @param {[type]} record [description] 69 | * @return {[type]} [description] 70 | */ 71 | subscribeDeep: function ( req, record ) { 72 | _.each(req.options.associations, function (assoc) { 73 | 74 | // Look up identity of associated model 75 | var ident = assoc[assoc.type]; 76 | var AssociatedModel = req._sails.models[ident]; 77 | 78 | if (req.options.autoWatch) { 79 | AssociatedModel.watch(req); 80 | } 81 | 82 | // Subscribe to each associated model instance in a collection 83 | if (assoc.type === 'collection') { 84 | _.each(record[assoc.alias], function (associatedInstance) { 85 | AssociatedModel.subscribe(req, associatedInstance); 86 | }); 87 | } 88 | // If there is an associated to-one model instance, subscribe to it 89 | else if (assoc.type === 'model' && record[assoc.alias]) { 90 | AssociatedModel.subscribe(req, record[assoc.alias]); 91 | } 92 | }); 93 | }, 94 | 95 | 96 | /** 97 | * Parse primary key value for use in a Waterline criteria 98 | * (e.g. for `find`, `update`, or `destroy`) 99 | * 100 | * @param {Request} req 101 | * @return {Integer|String} 102 | */ 103 | parsePk: function ( req ) { 104 | 105 | var pk = req.options.id || (req.options.where && req.options.where.id) || req.param('id'); 106 | 107 | // TODO: make this smarter... 108 | // (e.g. look for actual primary key of model and look for it 109 | // in the absence of `id`.) 110 | // See coercePK for reference (although be aware it is not currently in use) 111 | 112 | // exclude criteria on id field 113 | pk = _.isPlainObject(pk) ? undefined : pk; 114 | return pk; 115 | }, 116 | 117 | 118 | 119 | /** 120 | * Parse primary key value from parameters. 121 | * Throw an error if it cannot be retrieved. 122 | * 123 | * @param {Request} req 124 | * @return {Integer|String} 125 | */ 126 | requirePk: function (req) { 127 | var pk = module.exports.parsePk(req); 128 | 129 | // Validate the required `id` parameter 130 | if ( !pk ) { 131 | 132 | var err = new Error( 133 | 'No `id` parameter provided.'+ 134 | '(Note: even if the model\'s primary key is not named `id`- '+ 135 | '`id` should be used as the name of the parameter- it will be '+ 136 | 'mapped to the proper primary key name)' 137 | ); 138 | err.status = 400; 139 | throw err; 140 | } 141 | 142 | return pk; 143 | }, 144 | 145 | 146 | 147 | /** 148 | * Parse `criteria` for a Waterline `find` or `update` from all 149 | * request parameters. 150 | * 151 | * @param {Request} req 152 | * @return {Object} the WHERE criteria object 153 | */ 154 | parseCriteria: function ( req ) { 155 | 156 | // Allow customizable blacklist for params NOT to include as criteria. 157 | req.options.criteria = req.options.criteria || {}; 158 | req.options.criteria.blacklist = req.options.criteria.blacklist || ['limit', 'skip', 'page', 'perPage', 'sort', 'populate']; 159 | 160 | // Validate blacklist to provide a more helpful error msg. 161 | var blacklist = req.options.criteria && req.options.criteria.blacklist; 162 | if (blacklist && !_.isArray(blacklist)) { 163 | throw new Error('Invalid `req.options.criteria.blacklist`. Should be an array of strings (parameter names.)'); 164 | } 165 | 166 | // Look for explicitly specified `where` parameter. 167 | var where = req.params.all().where; 168 | 169 | // If `where` parameter is a string, try to interpret it as JSON 170 | if (_.isString(where)) { 171 | where = tryToParseJSON(where); 172 | } 173 | 174 | // If `where` has not been specified, but other unbound parameter variables 175 | // **ARE** specified, build the `where` option using them. 176 | if (!where) { 177 | 178 | // Prune params which aren't fit to be used as `where` criteria 179 | // to build a proper where query 180 | where = req.params.all(); 181 | 182 | // Omit built-in runtime config (like query modifiers) 183 | where = _.omit(where, blacklist || ['limit', 'skip', 'sort', 'page', 'perPage']); 184 | 185 | // Omit any params w/ undefined values 186 | where = _.omit(where, function (p){ if (_.isUndefined(p)) return true; }); 187 | 188 | // Omit jsonp callback param (but only if jsonp is enabled) 189 | var jsonpOpts = req.options.jsonp && !req.isSocket; 190 | jsonpOpts = _.isObject(jsonpOpts) ? jsonpOpts : { callback: JSONP_CALLBACK_PARAM }; 191 | if (jsonpOpts) { 192 | where = _.omit(where, [jsonpOpts.callback]); 193 | } 194 | } 195 | 196 | // Merge w/ req.options.where and return 197 | where = _.merge({}, req.options.where || {}, where) || undefined; 198 | 199 | return where; 200 | }, 201 | 202 | 203 | /** 204 | * Parse `values` for a Waterline `create` or `update` from all 205 | * request parameters. 206 | * 207 | * @param {Request} req 208 | * @return {Object} 209 | */ 210 | parseValues: function (req) { 211 | 212 | // Allow customizable blacklist for params NOT to include as values. 213 | req.options.values = req.options.values || {}; 214 | req.options.values.blacklist = req.options.values.blacklist; 215 | 216 | // Validate blacklist to provide a more helpful error msg. 217 | var blacklist = req.options.values.blacklist; 218 | if (blacklist && !_.isArray(blacklist)) { 219 | throw new Error('Invalid `req.options.values.blacklist`. Should be an array of strings (parameter names.)'); 220 | } 221 | 222 | // Merge params into req.options.values, omitting the blacklist. 223 | var values = mergeDefaults(req.params.all(), _.omit(req.options.values, 'blacklist')); 224 | 225 | // Omit values that are in the blacklist (like query modifiers) 226 | values = _.omit(values, blacklist || []); 227 | 228 | // Omit any values w/ undefined values 229 | values = _.omit(values, function (p){ if (_.isUndefined(p)) return true; }); 230 | 231 | // Omit jsonp callback param (but only if jsonp is enabled) 232 | var jsonpOpts = req.options.jsonp && !req.isSocket; 233 | jsonpOpts = _.isObject(jsonpOpts) ? jsonpOpts : { callback: JSONP_CALLBACK_PARAM }; 234 | if (jsonpOpts) { 235 | values = _.omit(values, [jsonpOpts.callback]); 236 | } 237 | 238 | return values; 239 | }, 240 | 241 | 242 | 243 | /** 244 | * Determine the model class to use w/ this blueprint action. 245 | * @param {Request} req 246 | * @return {WLCollection} 247 | */ 248 | parseModel: function (req) { 249 | 250 | // Ensure a model can be deduced from the request options. 251 | var model = req.options.model || req.options.controller; 252 | if (!model) throw new Error(util.format('No "model" specified in route options.')); 253 | 254 | var Model = req._sails.models[model]; 255 | if ( !Model ) throw new Error(util.format('Invalid route option, "model".\nI don\'t know about any models named: `%s`',model)); 256 | 257 | return Model; 258 | }, 259 | 260 | 261 | 262 | /** 263 | * @param {Request} req 264 | */ 265 | parseSort: function (req) { 266 | var sort = req.param('sort') || req.options.sort; 267 | if (typeof sort == 'undefined') {return undefined;} 268 | if (typeof sort == 'string') { 269 | try { 270 | sort = JSON.parse(sort); 271 | } catch(e) {} 272 | } 273 | return sort; 274 | }, 275 | 276 | /** 277 | * @param {Request} req 278 | */ 279 | parseLimit: function (req) { 280 | var DEFAULT_LIMIT = req._sails.config.blueprints.defaultLimit || 30; 281 | var limit = req.param('limit') || (typeof req.options.limit !== 'undefined' ? req.options.limit : DEFAULT_LIMIT); 282 | if (limit) { limit = +limit; } 283 | return limit; 284 | }, 285 | 286 | /** 287 | * @param {Request} req 288 | */ 289 | parseSkip: function (req) { 290 | var DEFAULT_SKIP = 0; 291 | var skip = req.param('skip') || (typeof req.options.skip !== 'undefined' ? req.options.skip : DEFAULT_SKIP); 292 | if (skip) { skip = +skip; } 293 | return skip; 294 | }, 295 | 296 | /** 297 | * @param {Request} req 298 | */ 299 | parsePerPage: function (req) { 300 | var DEFAULT_PER_PAGE = req._sails.config.blueprints.defaultLimit || 25; 301 | var perPage = req.param('perPage') || (typeof req.options.perPage !== 'undefined' ? req.options.perPage : DEFAULT_PER_PAGE); 302 | if (perPage) { perPage = +perPage; } 303 | return perPage; 304 | }, 305 | 306 | /** 307 | * @param {Request} req 308 | */ 309 | parsePage: function (req) { 310 | var DEFAULT_PAGE = 1; 311 | var page = req.param('page') || (typeof req.options.page !== 'undefined' ? req.options.page : DEFAULT_PAGE); 312 | if (page) { page = +page; } 313 | return page; 314 | } 315 | }; 316 | 317 | 318 | 319 | 320 | 321 | 322 | // TODO: 323 | // 324 | // Replace the following helper with the version in sails.util: 325 | 326 | // Attempt to parse JSON 327 | // If the parse fails, return the error object 328 | // If JSON is falsey, return null 329 | // (this is so that it will be ignored if not specified) 330 | function tryToParseJSON (json) { 331 | if (!_.isString(json)) return null; 332 | try { 333 | return JSON.parse(json); 334 | } 335 | catch (e) { return e; } 336 | } 337 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var _ = require('lodash') 6 | , util = require('util') 7 | , async = require('async') 8 | , pluralize = require('pluralize') 9 | , BlueprintController = { 10 | create : require('./actions/create') 11 | , find : require('./actions/find') 12 | , findone : require('./actions/findOne') 13 | , update : require('./actions/update') 14 | , destroy : require('./actions/destroy') 15 | , populate: require('./actions/populate') 16 | , add : require('./actions/add') 17 | , remove : require('./actions/remove') 18 | } 19 | , STRINGFILE = require('sails-stringfile'); 20 | 21 | 22 | /** 23 | * Blueprints (Core Hook) 24 | * 25 | * Stability: 1 - Experimental 26 | * (see http://nodejs.org/api/documentation.html#documentation_stability_index) 27 | */ 28 | 29 | module.exports = function(sails) { 30 | 31 | /** 32 | * Private dependencies. 33 | * (need access to `sails`) 34 | */ 35 | 36 | var onRoute = require('./onRoute')(sails); 37 | 38 | 39 | 40 | var hook; 41 | 42 | /** 43 | * Expose blueprint hook definition 44 | */ 45 | return { 46 | 47 | /** 48 | * Default configuration to merge w/ top-level `sails.config` 49 | * @type {Object} 50 | */ 51 | defaults: { 52 | 53 | // These config options are mixed into the route options (req.options) 54 | // and made accessible from the blueprint actions. Most of them currently 55 | // relate to the shadow (i.e. implicit) routes which are created, and are 56 | // interpreted by this hook. 57 | blueprints: { 58 | 59 | // Blueprint/Shadow-Routes Enabled 60 | // 61 | // e.g. '/frog/jump': 'FrogController.jump' 62 | actions: true, 63 | // e.g. '/frog': 'FrogController.index' 64 | index: true, 65 | // e.g. '/frog/find/:id?': 'FrogController.find' 66 | shortcuts: true, 67 | // e.g. 'get /frog/:id?': 'FrogController.find' 68 | rest: true, 69 | 70 | 71 | 72 | // Blueprint/Shadow-Route Modifiers 73 | // 74 | // e.g. 'get /api/v2/frog/:id?': 'FrogController.find' 75 | prefix: '', 76 | 77 | // Blueprint/REST-Route Modifiers 78 | // Will work only for REST and will extend `prefix` option 79 | // 80 | // e.g. 'get /api/v2/frog/:id?': 'FrogController.find' 81 | restPrefix: '', 82 | 83 | // e.g. 'get /frogs': 'FrogController.find' 84 | pluralize: false, 85 | 86 | 87 | 88 | 89 | 90 | // Configuration of the blueprint actions themselves: 91 | 92 | // Whether to populate all association attributes in the `find` 93 | // blueprint action. 94 | populate: true, 95 | 96 | // Whether to run `Model.watch()` in the `find` blueprint action. 97 | autoWatch: true, 98 | 99 | 100 | // (TODO: generated comments for jsonp configuration needs to be updated w/ new options) 101 | // (TODO: need to mention new `req.options` stuff in generated comments) 102 | 103 | // // Enable JSONP callbacks. 104 | // jsonp: false 105 | 106 | // Deprecated: 107 | // Skip blueprint if `:id?` is NOT an integer. 108 | // expectIntegerId: false, 109 | } 110 | 111 | }, 112 | 113 | 114 | 115 | /** 116 | * Initialize is fired first thing when the hook is loaded. 117 | * 118 | * @param {Function} cb 119 | */ 120 | initialize: function (cb) { 121 | 122 | // Provide hook context to closures 123 | hook = this; 124 | 125 | //////////////////////////////////////////////////////////////////////// 126 | // TODO: 127 | // Provide deprecation notice letting 0.9 users know that they need to 128 | // move their blueprint configuration to `config.blueprints` instead of 129 | // `config.controllers.blueprints`. Similarly, need a message to let 130 | // folks know to move their controller-specific blueprint config from 131 | // `SomeController._config.blueprints` to `SomeController._config`. 132 | // In both cases, we can "fix" the configuration in-memory, avoiding 133 | // allowing the app to "still work". This can be done the same way we're 134 | // doing it for adapter config. 135 | //////////////////////////////////////////////////////////////////////// 136 | 137 | // Register route syntax for binding blueprints directly. 138 | sails.on('route:typeUnknown', onRoute); 139 | 140 | // Set up listener to bind shadow routes when the time is right. 141 | // 142 | // Always wait until after router has bound static routes. 143 | // If policies hook is enabled, also wait until policies are bound. 144 | // If orm hook is enabled, also wait until models are known. 145 | // If controllers hook is enabled, also wait until controllers are known. 146 | var eventsToWaitFor = []; 147 | eventsToWaitFor.push('router:after'); 148 | if (sails.hooks.policies) { 149 | eventsToWaitFor.push('hook:policies:bound'); 150 | } 151 | if (sails.hooks.orm) { 152 | eventsToWaitFor.push('hook:orm:loaded'); 153 | } 154 | if (sails.hooks.controllers) { 155 | eventsToWaitFor.push('hook:controllers:loaded'); 156 | } 157 | sails.after(eventsToWaitFor, hook.bindShadowRoutes); 158 | 159 | // Load blueprint middleware and continue. 160 | loadMiddleware(cb); 161 | }, 162 | 163 | extendControllerMiddleware: function() { 164 | _.each(sails.middleware.controllers, function (controller) { 165 | _.defaults(controller, hook.middleware); 166 | }); 167 | }, 168 | 169 | bindShadowRoutes: function() { 170 | 171 | _.each(sails.middleware.controllers, function eachController (controller, controllerId) { 172 | if ( !_.isObject(controller) || _.isArray(controller) ) return; 173 | 174 | // Get globalId for use in errors/warnings 175 | var globalId = sails.controllers[controllerId].globalId; 176 | 177 | // Determine blueprint configuration for this controller 178 | var config = _.merge({}, 179 | sails.config.blueprints, 180 | controller._config || {}); 181 | 182 | // Validate blueprint config for this controller 183 | if ( config.prefix ) { 184 | if ( !_(config.prefix).isString() ) { 185 | sails.after('lifted', function () { 186 | sails.log.blank(); 187 | sails.log.warn(util.format('Ignoring invalid blueprint prefix configured for controller `%s`.', globalId)); 188 | sails.log.warn('`prefix` should be a string, e.g. "/api/v1".'); 189 | STRINGFILE.logMoreInfoLink(STRINGFILE.get('links.docs.config.blueprints'), sails.log.warn); 190 | }); 191 | return; 192 | } 193 | if ( !config.prefix.match(/^\//) ) { 194 | var originalPrefix = config.prefix; 195 | sails.after('lifted', function () { 196 | sails.log.blank(); 197 | sails.log.warn(util.format('Invalid blueprint prefix ("%s") configured for controller `%s` (should start with a `/`).', originalPrefix, globalId)); 198 | sails.log.warn(util.format('For now, assuming you meant: "%s".', config.prefix)); 199 | STRINGFILE.logMoreInfoLink(STRINGFILE.get('links.docs.config.blueprints'), sails.log.warn); 200 | }); 201 | 202 | config.prefix = '/' + config.prefix; 203 | } 204 | } 205 | 206 | // Validate REST route blueprint config for this controller 207 | if ( config.restPrefix ) { 208 | if ( !_(config.restPrefix).isString() ) { 209 | sails.after('lifted', function () { 210 | sails.log.blank(); 211 | sails.log.warn(util.format('Ignoring invalid blueprint rest prefix configured for controller `%s`.', globalId)); 212 | sails.log.warn('`restPrefix` should be a string, e.g. "/api/v1".'); 213 | STRINGFILE.logMoreInfoLink(STRINGFILE.get('links.docs.config.blueprints'), sails.log.warn); 214 | }); 215 | return; 216 | } 217 | if ( !config.restPrefix.match(/^\//) ) { 218 | var originalRestPrefix = config.restPrefix; 219 | sails.after('lifted', function () { 220 | sails.log.blank(); 221 | sails.log.warn(util.format('Invalid blueprint restPrefix ("%s") configured for controller `%s` (should start with a `/`).', originalRestPrefix, globalId)); 222 | sails.log.warn(util.format('For now, assuming you meant: "%s".', config.restPrefix)); 223 | STRINGFILE.logMoreInfoLink(STRINGFILE.get('links.docs.config.blueprints'), sails.log.warn); 224 | }); 225 | 226 | config.restPrefix = '/' + config.restPrefix; 227 | } 228 | } 229 | 230 | // Determine the names of the controller's user-defined actions 231 | // IMPORTANT: Use `sails.controllers` instead of `sails.middleware.controllers` 232 | // (since `sails.middleware.controllers` will have blueprints already mixed-in, 233 | // and we want the explicit actions defined in the app) 234 | var actions = Object.keys(sails.controllers[controllerId]); 235 | 236 | 237 | 238 | // Determine base route 239 | var baseRoute = config.prefix + '/' + controllerId; 240 | // Determine base route for RESTful service 241 | // Note that restPrefix will always start with / 242 | var baseRestRoute = config.prefix + config.restPrefix + '/' + controllerId; 243 | 244 | if (config.pluralize) { 245 | baseRoute = pluralize(baseRoute); 246 | baseRestRoute = pluralize(baseRestRoute); 247 | } 248 | 249 | // Build route options for blueprint 250 | var routeOpts = config; 251 | 252 | // Bind "actions" and "index" shadow routes for each action 253 | _.each(actions, function eachActionID (actionId) { 254 | 255 | var opts = _.merge({ 256 | action: actionId, 257 | controller: controllerId 258 | }, routeOpts); 259 | 260 | // Bind a route based on the action name, if `actions` shadows enabled 261 | if (config.actions) { 262 | var actionRoute = baseRoute + '/' + actionId.toLowerCase() + '/:id?'; 263 | sails.log.silly('Binding action ('+actionId.toLowerCase()+') blueprint/shadow route for controller:',controllerId); 264 | sails.router.bind(actionRoute, controller[actionId.toLowerCase()], null, opts); 265 | } 266 | 267 | // Bind base route to index action, if `index` shadows are not disabled 268 | if (config.index !== false && actionId.match(/^index$/i)) { 269 | sails.log.silly('Binding index blueprint/shadow route for controller:',controllerId); 270 | sails.router.bind(baseRoute, controller.index, null, opts); 271 | } 272 | }); 273 | 274 | // Determine the model connected to this controller either by: 275 | // -> explicit configuration 276 | // -> on the controller 277 | // -> on the routes config 278 | // -> or implicitly by globalId 279 | // -> or implicitly by controller id 280 | var routeConfig = sails.router.explicitRoutes[controllerId] || {}; 281 | var modelFromGlobalId = sails.util.findWhere(sails.models, {globalId: globalId}); 282 | var modelId = config.model || routeConfig.model || (modelFromGlobalId && modelFromGlobalId.identity) || controllerId; 283 | 284 | // If the orm hook is enabled, it has already been loaded by this time, 285 | // so just double-check to see if the attached model exists in `sails.models` 286 | // before trying to attach any CRUD blueprint actions to the controller. 287 | if (!sails.hooks.orm && sails.hooks.sequelize && sails.models && sails.models[modelId]) { 288 | // If a model with matching identity exists, 289 | // extend route options with the id of the model. 290 | routeOpts.model = modelId; 291 | 292 | var Model = sails.models[modelId]; 293 | 294 | // Bind convenience functions for readability below: 295 | 296 | // Given an action id like "find" or "create", returns the appropriate 297 | // blueprint action (or explicit controller action if the controller 298 | // overrode the blueprint CRUD action.) 299 | var _getAction = _.partial(_getMiddlewareForShadowRoute, controllerId); 300 | 301 | // Returns a customized version of the route template as a string. 302 | var _getRoute = _.partialRight(util.format, baseRoute); 303 | 304 | var _getRestRoute = _getRoute; 305 | if (config.restPrefix) { 306 | // Returns a customized version of the route template as a string for REST 307 | _getRestRoute = _.partialRight(util.format, baseRestRoute); 308 | } 309 | 310 | 311 | // Mix in the known associations for this model to the route options. 312 | routeOpts = _.merge({ associations: _.cloneDeep(Model.options.associations) }, routeOpts); 313 | 314 | // Binds a route to the specifed action using _getAction, and sets the action and controller 315 | // options for req.options 316 | var _bindRoute = function (path, action, options) { 317 | options = options || routeOpts; 318 | options = _.extend({}, options, {action: action, controller: controllerId}); 319 | sails.router.bind ( path, _getAction(action), null, options); 320 | 321 | }; 322 | 323 | // Bind URL-bar "shortcuts" 324 | // (NOTE: in a future release, these may be superceded by embedding actions in generated controllers 325 | // and relying on action blueprints instead.) 326 | if ( config.shortcuts ) { 327 | sails.log.silly('Binding shortcut blueprint/shadow routes for model ', modelId, ' on controller:', controllerId); 328 | 329 | _bindRoute(_getRoute('%s/find'), 'find'); 330 | _bindRoute(_getRoute('%s/find/:id'), 'findOne'); 331 | _bindRoute(_getRoute('%s/create'), 'create'); 332 | _bindRoute(_getRoute('%s/update/:id'), 'update'); 333 | _bindRoute(_getRoute('%s/destroy/:id?'), 'destroy'); 334 | 335 | // Bind add/remove "shortcuts" for each `collection` associations 336 | _.mapKeys(Model.associations, function(value){ 337 | var foreign = value.options.foreignKey; 338 | var alias = foreign.as || foreign.name || foreign; 339 | var _getAssocRoute = _.partialRight(util.format, baseRoute, alias); 340 | var opts = _.merge({ alias: alias, target: value.target.name }, routeOpts); 341 | sails.log.verbose('Binding "shortcuts" to association blueprint `'+alias+'` for',controllerId); 342 | _bindRoute(_getAssocRoute('%s/:parentid/%s/add/:id?'), 'add', opts); 343 | _bindRoute(_getAssocRoute('%s/:parentid/%s/remove/:id?'), 'remove', opts); 344 | }); 345 | } 346 | 347 | // Bind "rest" blueprint/shadow routes 348 | if ( config.rest ) { 349 | sails.log.silly('Binding RESTful blueprint/shadow routes for model+controller:',controllerId); 350 | 351 | _bindRoute(_getRestRoute('get %s'), 'find'); 352 | _bindRoute(_getRestRoute('get %s/:id'), 'findOne'); 353 | _bindRoute(_getRestRoute('post %s'), 'create'); 354 | _bindRoute(_getRestRoute('put %s/:id'), 'update'); 355 | _bindRoute(_getRestRoute('post %s/:id'), 'update'); 356 | _bindRoute(_getRestRoute('delete %s/:id?'), 'destroy'); 357 | 358 | // Bind "rest" blueprint/shadow routes based on known associations in our model's schema 359 | // Bind add/remove for each `collection` associations 360 | _.mapKeys(Model.associations, function(value, key){ 361 | var foreign = value.options.foreignKey; 362 | var alias = foreign.as || foreign.name || foreign; 363 | var _getAssocRoute = _.partialRight(util.format, baseRestRoute, alias); 364 | var opts = _.merge({ alias: alias, target: value.target.name }, routeOpts); 365 | sails.log.silly('Binding RESTful association blueprint `'+alias+'` for',controllerId); 366 | 367 | _bindRoute(_getAssocRoute('post %s/:parentid/%s/:id?'), 'add', opts); 368 | _bindRoute(_getAssocRoute('delete %s/:parentid/%s/:id?'), 'remove', opts); 369 | }); 370 | 371 | // and populate for both `collection` and `model` associations 372 | _.mapKeys(Model.associations, function(value){ 373 | var foreign = value.options.foreignKey; 374 | var alias = foreign.as || foreign.name || foreign; 375 | var _getAssocRoute = _.partialRight(util.format, baseRestRoute, alias); 376 | var opts = _.merge({ alias: alias, target: value.target.name }, routeOpts); 377 | sails.log.silly('Binding RESTful association blueprint `'+alias+'` for',controllerId); 378 | 379 | _bindRoute( _getAssocRoute('get %s/:parentid/%s/:id?'), 'populate', opts ); 380 | }); 381 | } 382 | } 383 | }); 384 | 385 | 386 | /** 387 | * Return the middleware function that should be bound for a shadow route 388 | * pointing to the specified blueprintId. Will use the explicit controller 389 | * action if it exists, otherwise the blueprint action. 390 | * 391 | * @param {String} controllerId 392 | * @param {String} blueprintId [find, create, etc.] 393 | * @return {Function} [middleware] 394 | */ 395 | function _getMiddlewareForShadowRoute (controllerId, blueprintId) { 396 | 397 | // Allow custom actions defined in controller to override blueprint actions. 398 | return sails.middleware.controllers[controllerId][blueprintId.toLowerCase()] || hook.middleware[blueprintId.toLowerCase()]; 399 | } 400 | } 401 | 402 | }; 403 | 404 | 405 | 406 | 407 | /** 408 | * Bind blueprint/shadow routes for each controller. 409 | */ 410 | function bindShadowRoutes () {} 411 | 412 | 413 | 414 | /** 415 | * (Re)load middleware. 416 | * 417 | * First, built-in blueprint actions in core Sails will be loaded. 418 | * Then, we'll attempt to load any custom blueprint definitions from 419 | * the user app using moduleloader. 420 | * 421 | * @api private 422 | */ 423 | 424 | function loadMiddleware (cb) { 425 | sails.log.verbose('Loading blueprint middleware...'); 426 | 427 | // Start off w/ the built-in blueprint actions (generic CRUD logic) 428 | BlueprintController; 429 | 430 | // Get custom blueprint definitions 431 | sails.modules.loadBlueprints(function modulesLoaded (err, modules) { 432 | if (err) return cb(err); 433 | 434 | // Merge custom overrides from our app into the BlueprintController 435 | // in Sails core. 436 | _.extend(BlueprintController, modules); 437 | 438 | // Add _middlewareType keys to the functions, for debugging 439 | _.each(BlueprintController, function(fn, key) { 440 | fn._middlewareType = 'BLUEPRINT: '+fn.name || key; 441 | }); 442 | 443 | // Save reference to blueprints middleware in hook. 444 | hook.middleware = BlueprintController; 445 | 446 | // When our app's controllers are finished loading, 447 | // merge the blueprint actions into each of them as defaults. 448 | sails.once('middleware:registered', hook.extendControllerMiddleware); 449 | 450 | return cb(err); 451 | }); 452 | } 453 | 454 | }; 455 | 456 | -------------------------------------------------------------------------------- /test/fixtures/sampleapp/views/500.ejs: -------------------------------------------------------------------------------- 1 | 2 | 36 | 37 | 38 | Server Error 39 | 40 | 47 | 48 | 49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 |
57 |

58 | Internal Server Error 59 |

60 |

61 | Something isn't right here. 62 |

63 | <% if (typeof error !== 'undefined') { %> 64 |

65 |         	<%- error %>
66 |         
67 | <% } else { %> 68 |

69 | A team of highly trained sea bass is working on this as we speak.
70 | If the problem persists, please contact the system administrator and inform them of the time that the error occured, and anything you might have done that may have caused the error. 71 |

72 | <% } %> 73 | 74 |
75 | 76 | 79 |
80 | 81 | 82 | --------------------------------------------------------------------------------