├── 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 | [](https://travis-ci.org/cesardeazevedo/sails-hook-sequelize-blueprints)
5 | [](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 |
18 |
19 |
21 |
22 |
23 |
24 |
25 |
Generate a REST API.
26 |
27 | Run sails generate api user. This will create two files: a model api/models/User.js and a controller api/controllers/UserController.js.
28 |
29 |
30 |
31 |
32 |
41 |
42 |
43 |
44 |
45 |
46 | Dive in.
47 |
48 |
Blueprints are just the beginning. You'll probably also want to learn how to customize your app's routes , set up security policies , configure your data sources , and build custom controller actions . For more help getting started, check out the links on this page.
49 |
50 |
51 |
52 |
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 |
--------------------------------------------------------------------------------