├── api ├── models │ ├── .gitkeep │ └── User.js ├── services │ └── .gitkeep ├── controllers │ ├── .gitkeep │ ├── AuthController.js │ └── UserController.js ├── policies │ └── sessionAuth.js └── responses │ ├── ok.js │ ├── badRequest.js │ ├── forbidden.js │ ├── serverError.js │ └── notFound.js ├── assets ├── images │ └── .gitkeep ├── templates │ └── .gitkeep ├── favicon.ico ├── robots.txt └── styles │ └── importer.less ├── .sailsrc ├── config ├── locales │ ├── de.json │ ├── en.json │ ├── fr.json │ ├── es.json │ └── _README.md ├── bootstrap.js ├── env │ ├── development.js │ └── production.js ├── log.js ├── models.js ├── policies.js ├── routes.js ├── i18n.js ├── csrf.js ├── globals.js ├── cors.js ├── http.js ├── session.js ├── views.js ├── connections.js ├── blueprints.js └── sockets.js ├── tasks ├── register │ ├── default.js │ ├── syncAssets.js │ ├── build.js │ ├── compileAssets.js │ ├── buildProd.js │ ├── linkAssets.js │ ├── linkAssetsBuild.js │ ├── linkAssetsBuildProd.js │ └── prod.js ├── config │ ├── clean.js │ ├── uglify.js │ ├── cssmin.js │ ├── sync.js │ ├── less.js │ ├── concat.js │ ├── watch.js │ ├── coffee.js │ ├── copy.js │ ├── jst.js │ └── sails-linker.js ├── pipeline.js └── README.md ├── .editorconfig ├── lib ├── stringUtil.js ├── JoiParamError.js ├── SymbolicError.js ├── Params.js ├── TestSetup.js ├── MethodInterceptor.js └── SailsControllerContext.js ├── tests └── node_modules │ └── Local │ ├── stringutil.js │ ├── SymbolicError.js │ └── Params.js ├── package.json ├── app.js ├── Gruntfile.js ├── .gitignore ├── views ├── layout.ejs ├── 403.ejs ├── 404.ejs └── homepage.ejs ├── README.md └── TUTORIALS └── TUTORIAL01.md /api/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/services/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /api/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/templates/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.sailsrc: -------------------------------------------------------------------------------- 1 | { 2 | "generators": { 3 | "modules": {} 4 | } 5 | } -------------------------------------------------------------------------------- /assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grokible/sails-tutorial-1/HEAD/assets/favicon.ico -------------------------------------------------------------------------------- /config/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Willkommen", 3 | "A brand new app.": "Eine neue App." 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Welcome", 3 | "A brand new app.": "A brand new app." 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenue", 3 | "A brand new app.": "Une toute nouvelle application." 4 | } 5 | -------------------------------------------------------------------------------- /config/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "Welcome": "Bienvenido", 3 | "A brand new app.": "Una aplicación de la nueva marca." 4 | } 5 | -------------------------------------------------------------------------------- /tasks/register/default.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('default', ['compileAssets', 'linkAssets', 'watch']); 3 | }; 4 | -------------------------------------------------------------------------------- /tasks/register/syncAssets.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('syncAssets', [ 3 | 'jst:dev', 4 | 'less:dev', 5 | 'sync:dev', 6 | 'coffee:dev' 7 | ]); 8 | }; 9 | -------------------------------------------------------------------------------- /tasks/register/build.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('build', [ 3 | 'compileAssets', 4 | 'linkAssetsBuild', 5 | 'clean:build', 6 | 'copy:build' 7 | ]); 8 | }; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /tasks/register/compileAssets.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('compileAssets', [ 3 | 'clean:dev', 4 | 'jst:dev', 5 | 'less:dev', 6 | 'copy:dev', 7 | 'coffee:dev' 8 | ]); 9 | }; 10 | -------------------------------------------------------------------------------- /tasks/register/buildProd.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('buildProd', [ 3 | 'compileAssets', 4 | 'concat', 5 | 'uglify', 6 | 'cssmin', 7 | 'linkAssetsBuildProd', 8 | 'clean:build', 9 | 'copy:build' 10 | ]); 11 | }; 12 | -------------------------------------------------------------------------------- /assets/robots.txt: -------------------------------------------------------------------------------- 1 | # The robots.txt file is used to control how search engines index your live URLs. 2 | # See http://www.robotstxt.org/wc/norobots.html for more information. 3 | 4 | 5 | 6 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 7 | # User-Agent: * 8 | # Disallow: / 9 | -------------------------------------------------------------------------------- /tasks/register/linkAssets.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('linkAssets', [ 3 | 'sails-linker:devJs', 4 | 'sails-linker:devStyles', 5 | 'sails-linker:devTpl', 6 | 'sails-linker:devJsJade', 7 | 'sails-linker:devStylesJade', 8 | 'sails-linker:devTplJade' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /tasks/register/linkAssetsBuild.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('linkAssetsBuild', [ 3 | 'sails-linker:devJsRelative', 4 | 'sails-linker:devStylesRelative', 5 | 'sails-linker:devTpl', 6 | 'sails-linker:devJsRelativeJade', 7 | 'sails-linker:devStylesRelativeJade', 8 | 'sails-linker:devTplJade' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /tasks/register/linkAssetsBuildProd.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('linkAssetsBuildProd', [ 3 | 'sails-linker:prodJsRelative', 4 | 'sails-linker:prodStylesRelative', 5 | 'sails-linker:devTpl', 6 | 'sails-linker:prodJsRelativeJade', 7 | 'sails-linker:prodStylesRelativeJade', 8 | 'sails-linker:devTplJade' 9 | ]); 10 | }; 11 | -------------------------------------------------------------------------------- /tasks/register/prod.js: -------------------------------------------------------------------------------- 1 | module.exports = function (grunt) { 2 | grunt.registerTask('prod', [ 3 | 'compileAssets', 4 | 'concat', 5 | 'uglify', 6 | 'cssmin', 7 | 'sails-linker:prodJs', 8 | 'sails-linker:prodStyles', 9 | 'sails-linker:devTpl', 10 | 'sails-linker:prodJsJade', 11 | 'sails-linker:prodStylesJade', 12 | 'sails-linker:devTplJade' 13 | ]); 14 | }; 15 | -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module models/User 3 | * @desc User account with first/last name, login, and encrypted password. 4 | * @see module:controllers/UserController 5 | */ 6 | 7 | module.exports = { 8 | 9 | attributes: { 10 | firstName : { type: 'string', required: true }, 11 | lastName : { type: 'string', required: true }, 12 | email : { type: 'string', required: true, unique: true, email: true }, 13 | password : { type: 'string', required: true } 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /lib/stringUtil.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Clean proper name - removes whitespace and upcases first character 3 | 4 | cleanProperName: function (s) { 5 | if (s == null) 6 | throw { message: 'null argument passed (requires string)', code: 'call.badArgument' } 7 | 8 | var s2 = s.replace (/\s/g, ""); // remove all whitespace 9 | if (s2.length == 0) 10 | return s2; 11 | return s2.substring (0, 1).toUpperCase () + s2.substring (1); 12 | } 13 | } -------------------------------------------------------------------------------- /api/controllers/AuthController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * AuthController 3 | * 4 | * @description :: Server-side logic for managing auths 5 | * @help :: See http://links.sailsjs.org/docs/controllers 6 | */ 7 | 8 | module.exports = { 9 | // Disable Sail's default routing to controller 10 | _config: { actions: false, shortcuts: false, rest: false }, 11 | 12 | login: function (req, res) { 13 | return res.json ({ 14 | todo: 'login() is not implemented yet!' 15 | }); 16 | } 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /tasks/config/clean.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Clean files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * This grunt task is configured to clean out the contents in the .tmp/public of your 7 | * sails project. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-clean 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('clean', { 15 | dev: ['.tmp/public/**'], 16 | build: ['www'] 17 | }); 18 | 19 | grunt.loadNpmTasks('grunt-contrib-clean'); 20 | }; 21 | -------------------------------------------------------------------------------- /tasks/config/uglify.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Minify files with UglifyJS. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minifies client-side javascript `assets`. 7 | * 8 | * For usage docs see: 9 | * https://github.com/gruntjs/grunt-contrib-uglify 10 | * 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('uglify', { 15 | dist: { 16 | src: ['.tmp/public/concat/production.js'], 17 | dest: '.tmp/public/min/production.min.js' 18 | } 19 | }); 20 | 21 | grunt.loadNpmTasks('grunt-contrib-uglify'); 22 | }; 23 | -------------------------------------------------------------------------------- /tasks/config/cssmin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compress CSS files. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Minifies css files and places them into .tmp/public/min directory. 7 | * 8 | * For usage docs see: 9 | * https://github.com/gruntjs/grunt-contrib-cssmin 10 | */ 11 | module.exports = function(grunt) { 12 | 13 | grunt.config.set('cssmin', { 14 | dist: { 15 | src: ['.tmp/public/concat/production.css'], 16 | dest: '.tmp/public/min/production.min.css' 17 | } 18 | }); 19 | 20 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 21 | }; 22 | -------------------------------------------------------------------------------- /config/bootstrap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap 3 | * (sails.config.bootstrap) 4 | * 5 | * An asynchronous bootstrap function that runs before your Sails app gets lifted. 6 | * This gives you an opportunity to set up your data model, run jobs, or perform some special logic. 7 | * 8 | * For more information on bootstrapping your app, check out: 9 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.bootstrap.html 10 | */ 11 | 12 | module.exports.bootstrap = function(cb) { 13 | 14 | // It's very important to trigger this callback method when you are finished 15 | // with the bootstrap! (otherwise your server will never lift, since it's waiting on the bootstrap) 16 | cb(); 17 | }; 18 | -------------------------------------------------------------------------------- /tasks/config/sync.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A grunt task to keep directories in sync. It is very similar to grunt-contrib-copy 3 | * but tries to copy only those files that has actually changed. 4 | * 5 | * --------------------------------------------------------------- 6 | * 7 | * Synchronize files from the `assets` folder to `.tmp/public`, 8 | * smashing anything that's already there. 9 | * 10 | * For usage docs see: 11 | * https://github.com/tomusdrw/grunt-sync 12 | * 13 | */ 14 | module.exports = function(grunt) { 15 | 16 | grunt.config.set('sync', { 17 | dev: { 18 | files: [{ 19 | cwd: './assets', 20 | src: ['**/*.!(coffee)'], 21 | dest: '.tmp/public' 22 | }] 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-sync'); 27 | }; 28 | -------------------------------------------------------------------------------- /tests/node_modules/Local/stringutil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unit test for stringutil 3 | */ 4 | 5 | var TestSetup = require ('Local/TestSetup') 6 | var test = new TestSetup (__filename) 7 | var Unit = test.require () 8 | 9 | var assert = require ('assert') 10 | 11 | describe ('stringutil (Local Module)', function () { 12 | describe ('#cleanProperName', function () { 13 | it ('should make first letter uppercase and trim leading/trailing whitespace', function () { 14 | assert.equal ('Roger', Unit.cleanProperName (' Roger ')) // whitespace 15 | assert.equal ('Roger', Unit.cleanProperName ('roger')) // caps 16 | assert.equal ("Mc'Donald", Unit.cleanProperName (" Mc'Donald ")) // whitespace and case 17 | }) 18 | }) 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /api/policies/sessionAuth.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sessionAuth 3 | * 4 | * @module :: Policy 5 | * @description :: Simple policy to allow any authenticated user 6 | * Assumes that your login action in one of your controllers sets `req.session.authenticated = true;` 7 | * @docs :: http://sailsjs.org/#!documentation/policies 8 | * 9 | */ 10 | module.exports = function(req, res, next) { 11 | 12 | // User is allowed, proceed to the next policy, 13 | // or if this is the last policy, the controller 14 | if (req.session.authenticated) { 15 | return next(); 16 | } 17 | 18 | // User is not allowed 19 | // (default res.forbidden() behavior can be overridden in `config/403.js`) 20 | return res.forbidden('You are not permitted to perform this action.'); 21 | }; 22 | -------------------------------------------------------------------------------- /tasks/config/less.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compiles LESS files into CSS. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Only the `assets/styles/importer.less` is compiled. 7 | * This allows you to control the ordering yourself, i.e. import your 8 | * dependencies, mixins, variables, resets, etc. before other stylesheets) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-less 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('less', { 16 | dev: { 17 | files: [{ 18 | expand: true, 19 | cwd: 'assets/styles/', 20 | src: ['importer.less'], 21 | dest: '.tmp/public/styles/', 22 | ext: '.css' 23 | }] 24 | } 25 | }); 26 | 27 | grunt.loadNpmTasks('grunt-contrib-less'); 28 | }; 29 | -------------------------------------------------------------------------------- /tasks/config/concat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Concatenate files. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Concatenates files javascript and css from a defined array. Creates concatenated files in 7 | * .tmp/public/contact directory 8 | * [concat](https://github.com/gruntjs/grunt-contrib-concat) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-concat 12 | */ 13 | module.exports = function(grunt) { 14 | 15 | grunt.config.set('concat', { 16 | js: { 17 | src: require('../pipeline').jsFilesToInject, 18 | dest: '.tmp/public/concat/production.js' 19 | }, 20 | css: { 21 | src: require('../pipeline').cssFilesToInject, 22 | dest: '.tmp/public/concat/production.css' 23 | } 24 | }); 25 | 26 | grunt.loadNpmTasks('grunt-contrib-concat'); 27 | }; 28 | -------------------------------------------------------------------------------- /tasks/config/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Run predefined tasks whenever watched file patterns are added, changed or deleted. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Watch for changes on 7 | * - files in the `assets` folder 8 | * - the `tasks/pipeline.js` file 9 | * and re-run the appropriate tasks. 10 | * 11 | * For usage docs see: 12 | * https://github.com/gruntjs/grunt-contrib-watch 13 | * 14 | */ 15 | module.exports = function(grunt) { 16 | 17 | grunt.config.set('watch', { 18 | api: { 19 | 20 | // API files to watch: 21 | files: ['api/**/*'] 22 | }, 23 | assets: { 24 | 25 | // Assets to watch: 26 | files: ['assets/**/*', 'tasks/pipeline.js'], 27 | 28 | // When assets are changed: 29 | tasks: ['syncAssets' , 'linkAssets'] 30 | } 31 | }); 32 | 33 | grunt.loadNpmTasks('grunt-contrib-watch'); 34 | }; 35 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Development environment settings 3 | * 4 | * This file can include shared settings for a development team, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | /*************************************************************************** 16 | * Set the default database connection for models in the development * 17 | * environment (see config/connections.js and config/models.js ) * 18 | ***************************************************************************/ 19 | 20 | // models: { 21 | // connection: 'someMongodbServer' 22 | // } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /lib/JoiParamError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Local/JoiParamError 3 | * @desc Error object describing a single invalid parameter 4 | * @example 5 | * 6 | * throw new JoiParamError (e) <= Where E should be a ValidationError from Joi library 7 | * 8 | * The exception will take the first validation error and hoist the details 9 | */ 10 | 11 | module.exports = JoiParamError 12 | 13 | var inherits = require ('util').inherits 14 | var SymbolicError = require ('Local/SymbolicError') 15 | 16 | function JoiParamError (validationError) { 17 | // TODO - maybe some error checking? 18 | var detail = validationError.details [0]; 19 | var message = detail.message; 20 | var param = detail.path; 21 | 22 | SymbolicError.call (this, 'api.paramInvalid', message, validationError) 23 | this.param = param; 24 | } 25 | 26 | inherits (JoiParamError, SymbolicError) 27 | 28 | var Class = JoiParamError.prototype; 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tasks/config/coffee.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compile CoffeeScript files to JavaScript. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Compiles coffeeScript files from `assest/js` into Javascript and places them into 7 | * `.tmp/public/js` directory. 8 | * 9 | * For usage docs see: 10 | * https://github.com/gruntjs/grunt-contrib-coffee 11 | */ 12 | module.exports = function(grunt) { 13 | 14 | grunt.config.set('coffee', { 15 | dev: { 16 | options: { 17 | bare: true, 18 | sourceMap: true, 19 | sourceRoot: './' 20 | }, 21 | files: [{ 22 | expand: true, 23 | cwd: 'assets/js/', 24 | src: ['**/*.coffee'], 25 | dest: '.tmp/public/js/', 26 | ext: '.js' 27 | }, { 28 | expand: true, 29 | cwd: 'assets/js/', 30 | src: ['**/*.coffee'], 31 | dest: '.tmp/public/js/', 32 | ext: '.js' 33 | }] 34 | } 35 | }); 36 | 37 | grunt.loadNpmTasks('grunt-contrib-coffee'); 38 | }; 39 | -------------------------------------------------------------------------------- /tasks/config/copy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copy files and folders. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * # dev task config 7 | * Copies all directories and files, exept coffescript and less fiels, from the sails 8 | * assets folder into the .tmp/public directory. 9 | * 10 | * # build task config 11 | * Copies all directories nd files from the .tmp/public directory into a www directory. 12 | * 13 | * For usage docs see: 14 | * https://github.com/gruntjs/grunt-contrib-copy 15 | */ 16 | module.exports = function(grunt) { 17 | 18 | grunt.config.set('copy', { 19 | dev: { 20 | files: [{ 21 | expand: true, 22 | cwd: './assets', 23 | src: ['**/*.!(coffee|less)'], 24 | dest: '.tmp/public' 25 | }] 26 | }, 27 | build: { 28 | files: [{ 29 | expand: true, 30 | cwd: '.tmp/public', 31 | src: ['**/*'], 32 | dest: 'www' 33 | }] 34 | } 35 | }); 36 | 37 | grunt.loadNpmTasks('grunt-contrib-copy'); 38 | }; 39 | -------------------------------------------------------------------------------- /assets/styles/importer.less: -------------------------------------------------------------------------------- 1 | /** 2 | * importer.less 3 | * 4 | * By default, new Sails projects are configured to compile this file 5 | * from LESS to CSS. Unlike CSS files, LESS files are not compiled and 6 | * included automatically unless they are imported below. 7 | * 8 | * The LESS files imported below are compiled and included in the order 9 | * they are listed. Mixins, variables, etc. should be imported first 10 | * so that they can be accessed by subsequent LESS stylesheets. 11 | * 12 | * (Just like the rest of the asset pipeline bundled in Sails, you can 13 | * always omit, customize, or replace this behavior with SASS, SCSS, 14 | * or any other Grunt tasks you like.) 15 | */ 16 | 17 | 18 | 19 | // For example: 20 | // 21 | // @import 'variables/colors.less'; 22 | // @import 'mixins/foo.less'; 23 | // @import 'mixins/bar.less'; 24 | // @import 'mixins/baz.less'; 25 | // 26 | // @import 'styleguide.less'; 27 | // @import 'pages/login.less'; 28 | // @import 'pages/signup.less'; 29 | // 30 | // etc. 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-tutorial-1", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "a Sails application", 6 | "keywords": [], 7 | "dependencies": { 8 | "sails": "~0.10.5", 9 | "sails-disk": "~0.10.0", 10 | "rc": "~0.5.0", 11 | "include-all": "~0.1.3", 12 | "ejs": "~0.8.4", 13 | "grunt": "0.4.2", 14 | "grunt-sync": "~0.0.4", 15 | "grunt-contrib-copy": "~0.5.0", 16 | "grunt-contrib-clean": "~0.5.0", 17 | "grunt-contrib-concat": "~0.3.0", 18 | "grunt-sails-linker": "~0.9.5", 19 | "grunt-contrib-jst": "~0.6.0", 20 | "grunt-contrib-watch": "~0.5.3", 21 | "grunt-contrib-uglify": "~0.4.0", 22 | "grunt-contrib-cssmin": "~0.9.0", 23 | "grunt-contrib-less": "0.11.1", 24 | "grunt-contrib-coffee": "~0.10.1", 25 | "joi": "~5.1.0" 26 | }, 27 | "scripts": { 28 | "start": "node app.js", 29 | "debug": "node debug app.js", 30 | "test": "mocha --recursive tests", 31 | "script-gen-html-docs": "jsdoc -d doc/html $(find api/controllers api/models api/services node_modules/Local -name '*.js')" 32 | }, 33 | "main": "app.js", 34 | "repository": { 35 | "type": "git", 36 | "url": "git://github.com/rogerbush8/sails-tutorial-1.git" 37 | }, 38 | "author": "rogerbush8", 39 | "license": "" 40 | } -------------------------------------------------------------------------------- /config/locales/_README.md: -------------------------------------------------------------------------------- 1 | # Internationalization / Localization Settings 2 | 3 | > Also see the official docs on internationalization/localization: 4 | > http://links.sailsjs.org/docs/config/locales 5 | 6 | ## Locales 7 | All locale files live under `config/locales`. Here is where you can add translations 8 | as JSON key-value pairs. The name of the file should match the language that you are supporting, which allows for automatic language detection based on request headers. 9 | 10 | Here is an example locale stringfile for the Spanish language (`config/locales/es.json`): 11 | ```json 12 | { 13 | "Hello!": "Hola!", 14 | "Hello %s, how are you today?": "¿Hola %s, como estas?", 15 | } 16 | ``` 17 | ## Usage 18 | Locales can be accessed in controllers/policies through `res.i18n()`, or in views through the `__(key)` or `i18n(key)` functions. 19 | Remember that the keys are case sensitive and require exact key matches, e.g. 20 | 21 | ```ejs 22 |

<%= __('Welcome to PencilPals!') %>

23 |

<%= i18n('Hello %s, how are you today?', 'Pencil Maven') %>

24 |

<%= i18n('That\'s right-- you can use either i18n() or __()') %>

25 | ``` 26 | 27 | ## Configuration 28 | Localization/internationalization config can be found in `config/i18n.js`, from where you can set your supported locales. 29 | -------------------------------------------------------------------------------- /config/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Built-in Log Configuration 3 | * (sails.config.log) 4 | * 5 | * Configure the log level for your app, as well as the transport 6 | * (Underneath the covers, Sails uses Winston for logging, which 7 | * allows for some pretty neat custom transports/adapters for log messages) 8 | * 9 | * For more information on the Sails logger, check out: 10 | * http://sailsjs.org/#/documentation/concepts/Logging 11 | */ 12 | 13 | module.exports.log = { 14 | 15 | /*************************************************************************** 16 | * * 17 | * Valid `level` configs: i.e. the minimum log level to capture with * 18 | * sails.log.*() * 19 | * * 20 | * The order of precedence for log levels from lowest to highest is: * 21 | * silly, verbose, info, debug, warn, error * 22 | * * 23 | * You may also set the level to "silent" to suppress all logs. * 24 | * * 25 | ***************************************************************************/ 26 | 27 | // level: 'info' 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /api/responses/ok.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 200 (OK) Response 3 | * 4 | * Usage: 5 | * return res.ok(); 6 | * return res.ok(data); 7 | * return res.ok(data, 'auth/login'); 8 | * 9 | * @param {Object} data 10 | * @param {String|Object} options 11 | * - pass string to render specified view 12 | */ 13 | 14 | module.exports = function sendOK (data, options) { 15 | 16 | // Get access to `req`, `res`, & `sails` 17 | var req = this.req; 18 | var res = this.res; 19 | var sails = req._sails; 20 | 21 | sails.log.silly('res.ok() :: Sending 200 ("OK") response'); 22 | 23 | // Set status code 24 | res.status(200); 25 | 26 | // If appropriate, serve data as JSON(P) 27 | if (req.wantsJSON) { 28 | return res.jsonx(data); 29 | } 30 | 31 | // If second argument is a string, we take that to mean it refers to a view. 32 | // If it was omitted, use an empty object (`{}`) 33 | options = (typeof options === 'string') ? { view: options } : options || {}; 34 | 35 | // If a view was provided in options, serve it. 36 | // Otherwise try to guess an appropriate view, or if that doesn't 37 | // work, just send JSON. 38 | if (options.view) { 39 | return res.view(options.view, { data: data }); 40 | } 41 | 42 | // If no second argument provided, try to serve the implied view, 43 | // but fall back to sending JSON(P) if no view can be inferred. 44 | else return res.guessView({ data: data }, function couldNotGuessView () { 45 | return res.jsonx(data); 46 | }); 47 | 48 | }; 49 | -------------------------------------------------------------------------------- /tasks/config/jst.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Precompiles Underscore templates to a `.jst` file. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * (i.e. basically it takes HTML files and turns them into tiny little 7 | * javascript functions that you pass data to and return HTML. This can 8 | * speed up template rendering on the client, and reduce bandwidth usage.) 9 | * 10 | * For usage docs see: 11 | * https://github.com/gruntjs/grunt-contrib-jst 12 | * 13 | */ 14 | 15 | module.exports = function(grunt) { 16 | 17 | var templateFilesToInject = [ 18 | 'templates/**/*.html' 19 | ]; 20 | 21 | grunt.config.set('jst', { 22 | dev: { 23 | 24 | // To use other sorts of templates, specify a regexp like the example below: 25 | // options: { 26 | // templateSettings: { 27 | // interpolate: /\{\{(.+?)\}\}/g 28 | // } 29 | // }, 30 | 31 | // Note that the interpolate setting above is simply an example of overwriting lodash's 32 | // default interpolation. If you want to parse templates with the default _.template behavior 33 | // (i.e. using
), there's no need to overwrite `templateSettings.interpolate`. 34 | 35 | 36 | files: { 37 | // e.g. 38 | // 'relative/path/from/gruntfile/to/compiled/template/destination' : ['relative/path/to/sourcefiles/**/*.html'] 39 | '.tmp/public/jst.js': require('../pipeline').templateFilesToInject 40 | } 41 | } 42 | }); 43 | 44 | grunt.loadNpmTasks('grunt-contrib-jst'); 45 | }; 46 | -------------------------------------------------------------------------------- /config/env/production.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Production environment settings 3 | * 4 | * This file can include shared settings for a production environment, 5 | * such as API keys or remote database passwords. If you're using 6 | * a version control solution for your Sails app, this file will 7 | * be committed to your repository unless you add it to your .gitignore 8 | * file. If your repository will be publicly viewable, don't add 9 | * any private information to this file! 10 | * 11 | */ 12 | 13 | module.exports = { 14 | 15 | /*************************************************************************** 16 | * Set the default database connection for models in the production * 17 | * environment (see config/connections.js and config/models.js ) * 18 | ***************************************************************************/ 19 | 20 | // models: { 21 | // connection: 'someMysqlServer' 22 | // }, 23 | 24 | /*************************************************************************** 25 | * Set the port in the production environment to 80 * 26 | ***************************************************************************/ 27 | 28 | // port: 80, 29 | 30 | /*************************************************************************** 31 | * Set the log level in production environment to "silent" * 32 | ***************************************************************************/ 33 | 34 | // log: { 35 | // level: "silent" 36 | // } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /config/models.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default model configuration 3 | * (sails.config.models) 4 | * 5 | * Unless you override them, the following properties will be included 6 | * in each of your models. 7 | * 8 | * For more info on Sails models, see: 9 | * http://sailsjs.org/#/documentation/concepts/ORM 10 | */ 11 | 12 | module.exports.models = { 13 | 14 | /*************************************************************************** 15 | * * 16 | * Your app's default connection. i.e. the name of one of your app's * 17 | * connections (see `config/connections.js`) * 18 | * * 19 | ***************************************************************************/ 20 | // connection: 'localDiskDb', 21 | 22 | connection: 'mysql1', 23 | 24 | 25 | /*************************************************************************** 26 | * * 27 | * How and whether Sails will attempt to automatically rebuild the * 28 | * tables/collections/etc. in your schema. * 29 | * * 30 | * See http://sailsjs.org/#/documentation/concepts/ORM/model-settings.html * 31 | * * 32 | ***************************************************************************/ 33 | // migrate: 'alter' 34 | migrate: 'alter' 35 | 36 | }; 37 | -------------------------------------------------------------------------------- /tests/node_modules/Local/SymbolicError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unit test for SymbolicError 3 | */ 4 | 5 | var TestSetup = require ('Local/TestSetup') 6 | var test = new TestSetup (__filename) 7 | var SymbolicError = test.require () 8 | 9 | var assert = require ('assert') 10 | 11 | describe ('SymbolicError, constructor forms)', function () { 12 | describe ('#constructor all args', function () { 13 | it ('has its properties set after the constructor', function () { 14 | // To generate stack trace 15 | SymbolicError.setDebug (true) 16 | 17 | var s = 'login and password invalid'; 18 | var e2 = new Error ("blah blah") 19 | var e = new SymbolicError ('auth.badCredentials', s, e2) 20 | assert (e.code === 'auth.badCredentials') 21 | assert (e.chained === e2) 22 | assert (e.message === s) 23 | 24 | // Look for stack trace in test 25 | if (e2.stack) 26 | console.log (("" + e).indexOf ('Test.Runnable') > 0) 27 | }) 28 | }), 29 | 30 | describe ('#constructor 2 args, first null (symbol)', function () { 31 | it ('has default symbol unknown and message that was passed', function () { 32 | var s = 'login and password invalid' 33 | var e = new SymbolicError (null, s) 34 | assert (e.code === 'unknown') 35 | assert (e.message === s) 36 | }) 37 | }), 38 | 39 | describe ('#constructor instanceof Error', function () { 40 | it ('is an instanceof Error class', function () { 41 | var e = new SymbolicError () 42 | assert (e instanceof Error); 43 | }) 44 | }) 45 | }) 46 | 47 | -------------------------------------------------------------------------------- /lib/SymbolicError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Local/SymbolicError 3 | * @desc Error object with symbolic code (symbol) and error chaining 4 | * @example 5 | * 6 | * if (someError) 7 | * throw new SymbolicError ('auth.badCredentials', "Login/Password pair not valid.") 8 | * 9 | * // example 2: chaining exceptions ... 10 | * 11 | * } catch (e) { 12 | * throw new SymbolicError ('auth.badCredentials', 'bad credentials', e) 13 | * } 14 | * 15 | * // Turn on verbose debugging (stack dump) in stringify output 16 | * 17 | * SymbolicError.setDebug (true) 18 | */ 19 | 20 | module.exports = SymbolicError 21 | 22 | var inherits = require ('util').inherits 23 | 24 | var _debug = false 25 | 26 | function SymbolicError (code, message, chainedException) { 27 | Error.call (this, message) 28 | this.code = code ? code : 'unknown' 29 | this.message = message ? message : "" 30 | 31 | if (_debug) 32 | this.chained = chainedException ? chainedException : null 33 | } 34 | 35 | inherits (SymbolicError, Error) 36 | 37 | var Class = SymbolicError.prototype 38 | 39 | /** 40 | * Turns on debugging, which prints stack trace of e on trace. 41 | */ 42 | Class.setDebug = SymbolicError.setDebug = function (val) { _debug = val ? true : false } 43 | Class.getDebug = SymbolicError.getDebug = function () { return _debug } 44 | 45 | Class.toString = function () { 46 | var s = "SymbolicError(" + this.code + "): " + this.message 47 | 48 | if ( ! _debug) 49 | return s 50 | 51 | if (this.stack) 52 | return s + "\n: " + this.stack 53 | 54 | if ( ! this.chained) 55 | return s 56 | 57 | if (this.chained.stack) 58 | return s + "\n: " + this.chained.stack 59 | 60 | s += "\nChained: " + this.chained 61 | return s 62 | } 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /lib/Params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Local/Params.js 3 | * @desc Utility object for handling Sails query parameters 4 | */ 5 | 6 | var _ = require ('lodash') 7 | var SymbolicError = require ('Local/SymbolicError') 8 | var JoiParamError = require ('Local/JoiParamError') 9 | 10 | module.exports = Params 11 | 12 | var _copyMap = function (map) { 13 | var obj = {}; 14 | Object.getOwnPropertyNames (map).forEach (function (key) { obj [key] = map [key] }) 15 | return obj; 16 | } 17 | 18 | function Params (httpRequest, optSchema) { 19 | var map = httpRequest.params.all (); 20 | this.items = _copyMap (map) 21 | this.schema = optSchema ? optSchema : null 22 | 23 | // Not sure where this comes from, but it's not a query param so delete it 24 | this.delete ("id") 25 | } 26 | 27 | var Class = Params.prototype; 28 | 29 | Class.delete = function (name) { 30 | delete this.items [name]; 31 | return this 32 | } 33 | 34 | Class.has = function (name) { return this.items [name] !== undefined } 35 | Class.get = function (name) { return this.items [name] } 36 | 37 | /** 38 | * criteria is a scalar or an array 39 | */ 40 | Class.apply = function (collection, fnEach) { 41 | var that = this; 42 | _.each (collection, function (v) { 43 | if (that.items.hasOwnProperty (v)) 44 | that.items [v] = fnEach (that.items [v]); 45 | }) 46 | 47 | return this 48 | } 49 | 50 | Class.toString = function () { return JSON.stringify (this.items) } 51 | 52 | Class.validate = function () { 53 | if (this.schema === null) 54 | throw new SymbolicError ('bug.schemaIsNull', 'call to validate() with null schema in ctor'); 55 | 56 | this.schema.validate (this.items, function (e, value) { 57 | if (e) 58 | throw new JoiParamError(e) 59 | }); 60 | 61 | return this 62 | } 63 | 64 | Class.getAll = function () { return _copyMap (this.items) } 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /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 | 25 | // Ensure a "sails" can be located: 26 | (function() { 27 | var sails; 28 | try { 29 | sails = require('sails'); 30 | } catch (e) { 31 | 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.'); 32 | console.error('To do that, run `npm install sails`'); 33 | console.error(''); 34 | console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); 35 | console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); 36 | console.error('but if it doesn\'t, the app will run with the global sails instead!'); 37 | return; 38 | } 39 | 40 | // Try to get `rc` dependency 41 | var rc; 42 | try { 43 | rc = require('rc'); 44 | } catch (e0) { 45 | try { 46 | rc = require('sails/node_modules/rc'); 47 | } catch (e1) { 48 | console.error('Could not find dependency: `rc`.'); 49 | console.error('Your `.sailsrc` file(s) will be ignored.'); 50 | console.error('To resolve this, run:'); 51 | console.error('npm install rc --save'); 52 | rc = function () { return {}; }; 53 | } 54 | } 55 | 56 | 57 | // Start server 58 | sails.lift(rc('sails')); 59 | })(); 60 | -------------------------------------------------------------------------------- /api/responses/badRequest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 400 (Bad Request) Handler 3 | * 4 | * Usage: 5 | * return res.badRequest(); 6 | * return res.badRequest(data); 7 | * return res.badRequest(data, 'some/specific/badRequest/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.badRequest( 12 | * 'Please choose a valid `password` (6-12 characters)', 13 | * 'trial/signup' 14 | * ); 15 | * ``` 16 | */ 17 | 18 | module.exports = function badRequest(data, options) { 19 | 20 | // Get access to `req`, `res`, & `sails` 21 | var req = this.req; 22 | var res = this.res; 23 | var sails = req._sails; 24 | 25 | // Set status code 26 | res.status(400); 27 | 28 | // Log error to console 29 | if (data !== undefined) { 30 | sails.log.verbose('Sending 400 ("Bad Request") response: \n',data); 31 | } 32 | else sails.log.verbose('Sending 400 ("Bad Request") response'); 33 | 34 | // Only include errors in response if application environment 35 | // is not set to 'production'. In production, we shouldn't 36 | // send back any identifying information about errors. 37 | if (sails.config.environment === 'production') { 38 | data = undefined; 39 | } 40 | 41 | // If the user-agent wants JSON, always respond with JSON 42 | if (req.wantsJSON) { 43 | return res.jsonx(data); 44 | } 45 | 46 | // If second argument is a string, we take that to mean it refers to a view. 47 | // If it was omitted, use an empty object (`{}`) 48 | options = (typeof options === 'string') ? { view: options } : options || {}; 49 | 50 | // If a view was provided in options, serve it. 51 | // Otherwise try to guess an appropriate view, or if that doesn't 52 | // work, just send JSON. 53 | if (options.view) { 54 | return res.view(options.view, { data: data }); 55 | } 56 | 57 | // If no second argument provided, try to serve the implied view, 58 | // but fall back to sending JSON(P) if no view can be inferred. 59 | else return res.guessView({ data: data }, function couldNotGuessView () { 60 | return res.jsonx(data); 61 | }); 62 | 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /api/controllers/UserController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * User controller logic for REST service. 3 | * @module controllers/UserController 4 | * @see module:models/User 5 | */ 6 | 7 | var Promise = require ('bluebird'); 8 | var BCrypt = Promise.promisifyAll (require ('bcrypt')); 9 | var Joi = require ('joi'); 10 | 11 | var StringUtil = require ('Local/stringutil.js'); 12 | var Params = require ('Local/Params'); 13 | 14 | var SailsControllerContext = require ('Local/SailsControllerContext'); 15 | 16 | var createSchema = Joi.object ().keys ({ 17 | firstName: Joi.string ().alphanum ().max (30).required (), 18 | lastName: Joi.string ().alphanum ().max (30).required (), 19 | email: Joi.string ().email ().required (), 20 | password: Joi.string ().regex (/\w{6,128}/).required () 21 | }) 22 | 23 | var ci = SailsControllerContext (); 24 | 25 | module.exports = ci.intercept ({ 26 | /** 27 | * Create User. Must have unique email. Password is hashed prior to storage. 28 | * @function create 29 | * @arg {string} firstName will have lead/trail ws trimmed and upcase first 30 | * @arg {string} lastName will have lead/trail ws trimmed and upcase first 31 | * @arg {string} password Plaintext password for user account. 32 | * @arg {string} email Email must be unique (is used as login for account). 33 | * @returns {string} json User or Standard Error 34 | * @see createSchema for parameter validation 35 | * // req - http.IncomingMessage, res - http.ServerResponse 36 | */ 37 | create: function (req, res) { 38 | var pm = new Params (req, createSchema) 39 | pm.apply (['firstName', 'lastName'], StringUtil.cleanProperName) 40 | 41 | var p = pm.validate ().getAll () 42 | 43 | BCrypt.genSaltAsync (10) 44 | .then (function (salt) { return BCrypt.hashAsync (p.password, salt) }) 45 | .then (function (hash) { p ['password'] = hash; return { data: p } /*User.create (p)*/ }) 46 | .then (function (user) { res.json (user) }) 47 | .catch (function (e) { ci.error (res, e) }) 48 | } 49 | }); 50 | 51 | -------------------------------------------------------------------------------- /lib/TestSetup.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TestSetup class is a setup for a unit test. Initialization of the constructor, 3 | * by default, assumes a parallel test directory structure to that of the actual 4 | * files under test (this can be overridden in the constructor). This makes locating 5 | * the files to be tested mostly automatic, and less brittle than specifying string paths. 6 | * 7 | * @module Local/TestSetup 8 | * @example 9 | * var TestSetup = require ('Local/TestSetup') 10 | * var test = new TestSetup (__filename) 11 | * var Unit = test.require () 12 | * // snip ... 13 | * // Unit is the loaded unit under test, e.g.: 14 | * assert.equal ('Roger', Unit.cleanProperName (' Roger ')) 15 | */ 16 | 17 | module.exports = TestSetup 18 | 19 | function TestSetup (testFilePath, unitFilePath) { 20 | this.testFilePath = testFilePath 21 | if (unitFilePath === undefined) 22 | this.unitFilePath = this.getAssociatedUnitFilePath () 23 | } 24 | 25 | var TOP_LEVEL_TEST_DIR = "tests" 26 | 27 | TestSetup.prototype.getAssociatedUnitFilePath = function () { 28 | 29 | // Given an absolute path to a test file, in a parallel directory structure, under 30 | // "/tests", find the unit file that we should be testing (unit file and test file 31 | // must have exact same name) 32 | 33 | // Assume testing has been launched from top-level directory, and that 34 | // our tests are defined under a parallel stucture in /tests directory in top-level 35 | 36 | var cwd = process.cwd () 37 | 38 | // Make sure current working directory + /tests at beginning testFilepath, and remove. 39 | // (has the effect of removing /tests from the filepath, as well as making sure we 40 | // are in a parallel directory structure, as a sanity check). 41 | 42 | var dir = cwd + "/" + TOP_LEVEL_TEST_DIR 43 | 44 | var i = this.testFilePath.indexOf (dir) 45 | if (i != 0) 46 | throw { code: '', message: "Unit Test filepath = " + this.testFilePath + 47 | ". File is not in directory = " + dir }; 48 | 49 | var unitFilePath = cwd + this.testFilePath.substr (dir.length) 50 | return unitFilePath 51 | } 52 | 53 | TestSetup.prototype.require = function () { 54 | return require (this.unitFilePath) 55 | } 56 | -------------------------------------------------------------------------------- /tasks/pipeline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * grunt/pipeline.js 3 | * 4 | * The order in which your css, javascript, and template files should be 5 | * compiled and linked from your views and static HTML files. 6 | * 7 | * (Note that you can take advantage of Grunt-style wildcard/glob/splat expressions 8 | * for matching multiple files.) 9 | */ 10 | 11 | 12 | 13 | // CSS files to inject in order 14 | // 15 | // (if you're using LESS with the built-in default config, you'll want 16 | // to change `assets/styles/importer.less` instead.) 17 | var cssFilesToInject = [ 18 | 'styles/**/*.css' 19 | ]; 20 | 21 | 22 | // Client-side javascript files to inject in order 23 | // (uses Grunt-style wildcard/glob/splat expressions) 24 | var jsFilesToInject = [ 25 | 26 | // Load sails.io before everything else 27 | 'js/dependencies/sails.io.js', 28 | 29 | // Dependencies like jQuery, or Angular are brought in here 30 | 'js/dependencies/**/*.js', 31 | 32 | // All of the rest of your client-side js files 33 | // will be injected here in no particular order. 34 | 'js/**/*.js' 35 | ]; 36 | 37 | 38 | // Client-side HTML templates are injected using the sources below 39 | // The ordering of these templates shouldn't matter. 40 | // (uses Grunt-style wildcard/glob/splat expressions) 41 | // 42 | // By default, Sails uses JST templates and precompiles them into 43 | // functions for you. If you want to use jade, handlebars, dust, etc., 44 | // with the linker, no problem-- you'll just want to make sure the precompiled 45 | // templates get spit out to the same file. Be sure and check out `tasks/README.md` 46 | // for information on customizing and installing new tasks. 47 | var templateFilesToInject = [ 48 | 'templates/**/*.html' 49 | ]; 50 | 51 | 52 | 53 | // Prefix relative paths to source files so they point to the proper locations 54 | // (i.e. where the other Grunt tasks spit them out, or in some cases, where 55 | // they reside in the first place) 56 | module.exports.cssFilesToInject = cssFilesToInject.map(function(path) { 57 | return '.tmp/public/' + path; 58 | }); 59 | module.exports.jsFilesToInject = jsFilesToInject.map(function(path) { 60 | return '.tmp/public/' + path; 61 | }); 62 | module.exports.templateFilesToInject = templateFilesToInject.map(function(path) { 63 | return 'assets/' + path; 64 | }); 65 | -------------------------------------------------------------------------------- /lib/MethodInterceptor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Local/MethodInterceptor.js 3 | * @desc Utility object for intercepting/wrapping methods 4 | * @example 5 | * 6 | * // Here's an OOP way to do things: 7 | * 8 | * function SomeConstructor () { 9 | * this.interceptor = new MethodInterceptor 10 | * (SomeConstructor.prototype.wrapper, this); 11 | * } 12 | * 13 | * // This function is the "interceptor" (wrapper) function. It 14 | * // doesn't automatically call the wrapped function (we must do it ourselves). 15 | * 16 | * SomeConstructor.prototype.wrapper = function (originalFn, originalThis, arguments) { 17 | * // N.B. 'this' will be set to the second arg of the MethodInterceptor 18 | * // constructor. We passed in "this" so this will behave like an OOP method. 19 | * // But the second arg can be anything, including null (optional context). 20 | * 21 | * // You should call the original function like this: 22 | * 23 | * originalFn.apply (originalThis, arguments) 24 | * } 25 | */ 26 | 27 | module.exports = MethodInterceptor 28 | 29 | /** 30 | * Constructor. 31 | * @arg {function} interceptorFn is the function that wraps the original function. 32 | * @arg {object} optContextObject is optional object passed into the wrapper function. 33 | */ 34 | function MethodInterceptor (interceptorFn, optContextObject) { 35 | this.interceptorFn = interceptorFn 36 | this.contextObject = optContextObject ? optContextObject : null 37 | } 38 | 39 | var Class = MethodInterceptor.prototype; 40 | 41 | Class.intercept = function (obj) { 42 | for (var name in obj) { 43 | 44 | // Only wrap local functions 45 | 46 | if ( ! obj.hasOwnProperty (name) || typeof obj [name] !== 'function') 47 | continue; 48 | 49 | // fn is original function on the object, to wrap 50 | 51 | var originalFn = obj [name]; 52 | var ifn = this.interceptorFn; 53 | 54 | // set method on object to be our function wrapper, which gets 55 | // the original function (fn), optContextObject as 'this', and arguments array 56 | 57 | obj [name] = function () { 58 | return ifn.call (this.optContextObject, originalFn, this, arguments) 59 | } 60 | } 61 | 62 | return obj; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /lib/SailsControllerContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module Local/SailsControllerContext.js 3 | * @desc Singleton for all controllers. Has a wrapper (interceptor) 4 | * function that can be easily hooked around all methods for 5 | * a Sails controller. 6 | * @example 7 | * 8 | * // get singleton ('new' optional) 9 | * var ci = SailsControllerContext (); 10 | * 11 | * // setup intercept on all methods of the object, 12 | * // and return the object 13 | * module.exports = ci.intercept ({ 14 | * create: function (req, res) { 15 | */ 16 | 17 | var MethodInterceptor = require ('Local/MethodInterceptor') 18 | var SymbolicError = require ('Local/SymbolicError') 19 | 20 | module.exports = _SailsControllerContext; 21 | 22 | var interceptor = null 23 | 24 | function SailsControllerContext () { 25 | interceptor = new MethodInterceptor (Class.controllerAction, this); 26 | } 27 | 28 | var Class = SailsControllerContext.prototype; 29 | 30 | /** 31 | * Call this function to finish a controller method, e.g. from an error callback 32 | * @example 33 | * 34 | * .then (function (user) { res.json (user) }) 35 | * .catch (function (e) { ci.error (res, e) }) // <= call here with exception 36 | */ 37 | Class.error = function (res, e) { 38 | // SymbolicErrors are designed to serialize correctly 39 | 40 | if (e instanceof SymbolicError) 41 | res.json ({ error: e }); // Google JSON standard => { error: } or { data: } 42 | else 43 | throw e; // Unhandled/unexpected exception 44 | } 45 | 46 | /** 47 | * wrapper (interceptor) function 48 | */ 49 | Class.controllerAction = function (originalFn, originalThis, arguments) { 50 | try { 51 | var req = arguments [0]; 52 | var res = arguments [1]; 53 | 54 | // Call original intercepted (wrapped) function 55 | 56 | originalFn.apply (originalThis, arguments) 57 | } catch (e) { 58 | this.error (res, e); 59 | } 60 | } 61 | 62 | // Expose the interceptor function 63 | 64 | Class.intercept = function (obj) { return interceptor.intercept (obj) } 65 | 66 | // ** Singleton pattern 67 | 68 | var _singleton = null 69 | 70 | function _SailsControllerContext () { 71 | if (_singleton === null) 72 | _singleton = new SailsControllerContext (); 73 | 74 | return _singleton; 75 | } 76 | 77 | -------------------------------------------------------------------------------- /config/policies.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Policy Mappings 3 | * (sails.config.policies) 4 | * 5 | * Policies are simple functions which run **before** your controllers. 6 | * You can apply one or more policies to a given controller, or protect 7 | * its actions individually. 8 | * 9 | * Any policy file (e.g. `api/policies/authenticated.js`) can be accessed 10 | * below by its filename, minus the extension, (e.g. "authenticated") 11 | * 12 | * For more information on how policies work, see: 13 | * http://sailsjs.org/#/documentation/concepts/Policies 14 | * 15 | * For more information on configuring policies, check out: 16 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.policies.html 17 | */ 18 | 19 | 20 | module.exports.policies = { 21 | 22 | /*************************************************************************** 23 | * * 24 | * Default policy for all controllers and actions (`true` allows public * 25 | * access) * 26 | * * 27 | ***************************************************************************/ 28 | 29 | // '*': true, 30 | 31 | /*************************************************************************** 32 | * * 33 | * Here's an example of mapping some policies to run before a controller * 34 | * and its actions * 35 | * * 36 | ***************************************************************************/ 37 | // RabbitController: { 38 | 39 | // Apply the `false` policy as the default for all of RabbitController's actions 40 | // (`false` prevents all access, which ensures that nothing bad happens to our rabbits) 41 | // '*': false, 42 | 43 | // For the action `nurture`, apply the 'isRabbitMother' policy 44 | // (this overrides `false` above) 45 | // nurture : 'isRabbitMother', 46 | 47 | // Apply the `isNiceToAnimals` AND `hasRabbitFood` policies 48 | // before letting any users feed our rabbits 49 | // feed : ['isNiceToAnimals', 'hasRabbitFood'] 50 | // } 51 | }; 52 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Gruntfile 3 | * 4 | * This Node script is executed when you run `grunt` or `sails lift`. 5 | * It's purpose is to load the Grunt tasks in your project's `tasks` 6 | * folder, and allow you to add and remove tasks as you see fit. 7 | * For more information on how this works, check out the `README.md` 8 | * file that was generated in your `tasks` folder. 9 | * 10 | * WARNING: 11 | * Unless you know what you're doing, you shouldn't change this file. 12 | * Check out the `tasks` directory instead. 13 | */ 14 | 15 | module.exports = function(grunt) { 16 | 17 | 18 | // Load the include-all library in order to require all of our grunt 19 | // configurations and task registrations dynamically. 20 | var includeAll; 21 | try { 22 | includeAll = require('include-all'); 23 | } catch (e0) { 24 | try { 25 | includeAll = require('sails/node_modules/include-all'); 26 | } 27 | catch(e1) { 28 | console.error('Could not find `include-all` module.'); 29 | console.error('Skipping grunt tasks...'); 30 | console.error('To fix this, please run:'); 31 | console.error('npm install include-all --save`'); 32 | console.error(); 33 | 34 | grunt.registerTask('default', []); 35 | return; 36 | } 37 | } 38 | 39 | 40 | /** 41 | * Loads Grunt configuration modules from the specified 42 | * relative path. These modules should export a function 43 | * that, when run, should either load/configure or register 44 | * a Grunt task. 45 | */ 46 | function loadTasks(relPath) { 47 | return includeAll({ 48 | dirname: require('path').resolve(__dirname, relPath), 49 | filter: /(.+)\.js$/ 50 | }) || {}; 51 | } 52 | 53 | /** 54 | * Invokes the function from a Grunt configuration module with 55 | * a single argument - the `grunt` object. 56 | */ 57 | function invokeConfigFn(tasks) { 58 | for (var taskName in tasks) { 59 | if (tasks.hasOwnProperty(taskName)) { 60 | tasks[taskName](grunt); 61 | } 62 | } 63 | } 64 | 65 | 66 | 67 | 68 | // Load task functions 69 | var taskConfigurations = loadTasks('./tasks/config'), 70 | registerDefinitions = loadTasks('./tasks/register'); 71 | 72 | // (ensure that a default task exists) 73 | if (!registerDefinitions.default) { 74 | registerDefinitions.default = function (grunt) { grunt.registerTask('default', []); }; 75 | } 76 | 77 | // Run task functions to configure Grunt. 78 | invokeConfigFn(taskConfigurations); 79 | invokeConfigFn(registerDefinitions); 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Route Mappings 3 | * (sails.config.routes) 4 | * 5 | * Your routes map URLs to views and controllers. 6 | * 7 | * If Sails receives a URL that doesn't match any of the routes below, 8 | * it will check for matching files (images, scripts, stylesheets, etc.) 9 | * in your assets directory. e.g. `http://localhost:1337/images/foo.jpg` 10 | * might match an image file: `/assets/images/foo.jpg` 11 | * 12 | * Finally, if those don't match either, the default 404 handler is triggered. 13 | * See `api/responses/notFound.js` to adjust your app's 404 logic. 14 | * 15 | * Note: Sails doesn't ACTUALLY serve stuff from `assets`-- the default Gruntfile in Sails copies 16 | * flat files from `assets` to `.tmp/public`. This allows you to do things like compile LESS or 17 | * CoffeeScript for the front-end. 18 | * 19 | * For more information on configuring custom routes, check out: 20 | * http://sailsjs.org/#/documentation/concepts/Routes/RouteTargetSyntax.html 21 | */ 22 | 23 | module.exports.routes = { 24 | 25 | /*************************************************************************** 26 | * * 27 | * Make the view located at `views/homepage.ejs` (or `views/homepage.jade`, * 28 | * etc. depending on your default view engine) your home page. * 29 | * * 30 | * (Alternatively, remove this and add an `index.html` file in your * 31 | * `assets` directory) * 32 | * * 33 | ***************************************************************************/ 34 | 35 | '/': { 36 | view: 'homepage' 37 | }, 38 | 39 | /*************************************************************************** 40 | * * 41 | * Custom routes here... * 42 | * * 43 | * If a request to a URL doesn't match any of the custom routes above, it * 44 | * is matched against Sails route blueprints. See `config/blueprints.js` * 45 | * for configuration options and examples. * 46 | * * 47 | ***************************************************************************/ 48 | 49 | 'post /login' : 'AuthController.login' 50 | 51 | }; 52 | -------------------------------------------------------------------------------- /api/responses/forbidden.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 403 (Forbidden) Handler 3 | * 4 | * Usage: 5 | * return res.forbidden(); 6 | * return res.forbidden(err); 7 | * return res.forbidden(err, 'some/specific/forbidden/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.forbidden('Access denied.'); 12 | * ``` 13 | */ 14 | 15 | module.exports = function forbidden (data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(403); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.verbose('Sending 403 ("Forbidden") response: \n',data); 28 | } 29 | else sails.log.verbose('Sending 403 ("Forbidden") response'); 30 | 31 | // Only include errors in response if application environment 32 | // is not set to 'production'. In production, we shouldn't 33 | // send back any identifying information about errors. 34 | if (sails.config.environment === 'production') { 35 | data = undefined; 36 | } 37 | 38 | // If the user-agent wants JSON, always respond with JSON 39 | if (req.wantsJSON) { 40 | return res.jsonx(data); 41 | } 42 | 43 | // If second argument is a string, we take that to mean it refers to a view. 44 | // If it was omitted, use an empty object (`{}`) 45 | options = (typeof options === 'string') ? { view: options } : options || {}; 46 | 47 | // If a view was provided in options, serve it. 48 | // Otherwise try to guess an appropriate view, or if that doesn't 49 | // work, just send JSON. 50 | if (options.view) { 51 | return res.view(options.view, { data: data }); 52 | } 53 | 54 | // If no second argument provided, try to serve the default view, 55 | // but fall back to sending JSON(P) if any errors occur. 56 | else return res.view('403', { data: data }, function (err, html) { 57 | 58 | // If a view error occured, fall back to JSON(P). 59 | if (err) { 60 | // 61 | // Additionally: 62 | // • If the view was missing, ignore the error but provide a verbose log. 63 | if (err.code === 'E_VIEW_FAILED') { 64 | sails.log.verbose('res.forbidden() :: Could not locate view for error page (sending JSON instead). Details: ',err); 65 | } 66 | // Otherwise, if this was a more serious error, log to the console with the details. 67 | else { 68 | sails.log.warn('res.forbidden() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 69 | } 70 | return res.jsonx(data); 71 | } 72 | 73 | return res.send(html); 74 | }); 75 | 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /api/responses/serverError.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 500 (Server Error) Response 3 | * 4 | * Usage: 5 | * return res.serverError(); 6 | * return res.serverError(err); 7 | * return res.serverError(err, 'some/specific/error/view'); 8 | * 9 | * NOTE: 10 | * If something throws in a policy or controller, or an internal 11 | * error is encountered, Sails will call `res.serverError()` 12 | * automatically. 13 | */ 14 | 15 | module.exports = function serverError (data, options) { 16 | 17 | // Get access to `req`, `res`, & `sails` 18 | var req = this.req; 19 | var res = this.res; 20 | var sails = req._sails; 21 | 22 | // Set status code 23 | res.status(500); 24 | 25 | // Log error to console 26 | if (data !== undefined) { 27 | sails.log.error('Sending 500 ("Server Error") response: \n',data); 28 | } 29 | else sails.log.error('Sending empty 500 ("Server Error") response'); 30 | 31 | // Only include errors in response if application environment 32 | // is not set to 'production'. In production, we shouldn't 33 | // send back any identifying information about errors. 34 | if (sails.config.environment === 'production') { 35 | data = undefined; 36 | } 37 | 38 | // If the user-agent wants JSON, always respond with JSON 39 | if (req.wantsJSON) { 40 | return res.jsonx(data); 41 | } 42 | 43 | // If second argument is a string, we take that to mean it refers to a view. 44 | // If it was omitted, use an empty object (`{}`) 45 | options = (typeof options === 'string') ? { view: options } : options || {}; 46 | 47 | // If a view was provided in options, serve it. 48 | // Otherwise try to guess an appropriate view, or if that doesn't 49 | // work, just send JSON. 50 | if (options.view) { 51 | return res.view(options.view, { data: data }); 52 | } 53 | 54 | // If no second argument provided, try to serve the default view, 55 | // but fall back to sending JSON(P) if any errors occur. 56 | else return res.view('500', { data: data }, function (err, html) { 57 | 58 | // If a view error occured, fall back to JSON(P). 59 | if (err) { 60 | // 61 | // Additionally: 62 | // • If the view was missing, ignore the error but provide a verbose log. 63 | if (err.code === 'E_VIEW_FAILED') { 64 | sails.log.verbose('res.serverError() :: Could not locate view for error page (sending JSON instead). Details: ',err); 65 | } 66 | // Otherwise, if this was a more serious error, log to the console with the details. 67 | else { 68 | sails.log.warn('res.serverError() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 69 | } 70 | return res.jsonx(data); 71 | } 72 | 73 | return res.send(html); 74 | }); 75 | 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /api/responses/notFound.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 404 (Not Found) Handler 3 | * 4 | * Usage: 5 | * return res.notFound(); 6 | * return res.notFound(err); 7 | * return res.notFound(err, 'some/specific/notfound/view'); 8 | * 9 | * e.g.: 10 | * ``` 11 | * return res.notFound(); 12 | * ``` 13 | * 14 | * NOTE: 15 | * If a request doesn't match any explicit routes (i.e. `config/routes.js`) 16 | * or route blueprints (i.e. "shadow routes", Sails will call `res.notFound()` 17 | * automatically. 18 | */ 19 | 20 | module.exports = function notFound (data, options) { 21 | 22 | // Get access to `req`, `res`, & `sails` 23 | var req = this.req; 24 | var res = this.res; 25 | var sails = req._sails; 26 | 27 | // Set status code 28 | res.status(404); 29 | 30 | // Log error to console 31 | if (data !== undefined) { 32 | sails.log.verbose('Sending 404 ("Not Found") response: \n',data); 33 | } 34 | else sails.log.verbose('Sending 404 ("Not Found") response'); 35 | 36 | // Only include errors in response if application environment 37 | // is not set to 'production'. In production, we shouldn't 38 | // send back any identifying information about errors. 39 | if (sails.config.environment === 'production') { 40 | data = undefined; 41 | } 42 | 43 | // If the user-agent wants JSON, always respond with JSON 44 | if (req.wantsJSON) { 45 | return res.jsonx(data); 46 | } 47 | 48 | // If second argument is a string, we take that to mean it refers to a view. 49 | // If it was omitted, use an empty object (`{}`) 50 | options = (typeof options === 'string') ? { view: options } : options || {}; 51 | 52 | // If a view was provided in options, serve it. 53 | // Otherwise try to guess an appropriate view, or if that doesn't 54 | // work, just send JSON. 55 | if (options.view) { 56 | return res.view(options.view, { data: data }); 57 | } 58 | 59 | // If no second argument provided, try to serve the default view, 60 | // but fall back to sending JSON(P) if any errors occur. 61 | else return res.view('404', { data: data }, function (err, html) { 62 | 63 | // If a view error occured, fall back to JSON(P). 64 | if (err) { 65 | // 66 | // Additionally: 67 | // • If the view was missing, ignore the error but provide a verbose log. 68 | if (err.code === 'E_VIEW_FAILED') { 69 | sails.log.verbose('res.notFound() :: Could not locate view for error page (sending JSON instead). Details: ',err); 70 | } 71 | // Otherwise, if this was a more serious error, log to the console with the details. 72 | else { 73 | sails.log.warn('res.notFound() :: When attempting to render error page view, an error occured (sending JSON instead). Details: ', err); 74 | } 75 | return res.jsonx(data); 76 | } 77 | 78 | return res.send(html); 79 | }); 80 | 81 | }; 82 | 83 | -------------------------------------------------------------------------------- /tests/node_modules/Local/Params.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unit test for Params 3 | */ 4 | 5 | var TestSetup = require ('Local/TestSetup') 6 | var test = new TestSetup (__filename) 7 | var Params = test.require () 8 | 9 | var assert = require ('assert') 10 | 11 | function makeMockRequest (obj) { 12 | var o = obj 13 | o.id = 1 14 | return { 15 | params: { 16 | all: function () { 17 | return o 18 | } 19 | } 20 | } 21 | } 22 | 23 | describe ('Params (Local Module)', function () { 24 | describe ('#constructor', function () { 25 | it ('should copy all passed parameters.', function () { 26 | 27 | var req = makeMockRequest ({ 'hello': 1, 'world': 5, 'howdy': 'there' }); 28 | var pm = new Params (req); 29 | 30 | assert.equal (true, pm.has ('hello'), "1st property is there"); 31 | assert.equal (1, pm.get ('hello'), "first property value equal"); 32 | 33 | assert.equal (true, pm.has ('world'), "2nd property is there"); 34 | assert.equal (5, pm.get ('world'), "2nd property value equal"); 35 | 36 | assert.equal (true, pm.has ('howdy'), "3rd property is there"); 37 | assert.equal ('there', pm.get ('howdy'), "3rd property value equal"); 38 | }) // it 39 | }), 40 | 41 | describe ('#delete', function () { 42 | it ('should delete item', function () { 43 | 44 | var req = makeMockRequest ({ 'hello': 1, 'world': 5, 'howdy': 'there' }); 45 | var pm = new Params (req); 46 | 47 | pm.delete ('hello'); 48 | 49 | assert.equal (false, pm.has ('hello'), "1st property is there"); 50 | 51 | assert.equal (true, pm.has ('world'), "2nd property is there"); 52 | assert.equal (5, pm.get ('world'), "2nd property value equal"); 53 | 54 | assert.equal (true, pm.has ('howdy'), "3rd property is there"); 55 | assert.equal ('there', pm.get ('howdy'), "3rd property value equal"); 56 | }) 57 | }), 58 | 59 | describe ('#apply', function () { 60 | it ('should apply functions to array items', function () { 61 | 62 | var req = makeMockRequest ({ 'hello': 1, 'world': 5 }); 63 | var pm = new Params (req); 64 | 65 | var criteria = ['world']; 66 | var blahFunction = function (x) { return x + "blah" }; 67 | pm.apply (criteria, blahFunction); 68 | 69 | assert.equal (true, pm.has ('hello'), "1st property is there"); 70 | assert.equal (1, pm.get ('hello'), "first property value equal"); 71 | 72 | assert.equal (true, pm.has ('world'), "2nd property is there"); 73 | assert.equal ("5blah", pm.get ('world'), "applied value - 2nd property value got applied"); 74 | }) 75 | }) 76 | 77 | 78 | }) 79 | 80 | -------------------------------------------------------------------------------- /config/i18n.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Internationalization / Localization Settings 3 | * (sails.config.i18n) 4 | * 5 | * If your app will touch people from all over the world, i18n (or internationalization) 6 | * may be an important part of your international strategy. 7 | * 8 | * 9 | * For more informationom i18n in Sails, check out: 10 | * http://sailsjs.org/#/documentation/concepts/Internationalization 11 | * 12 | * For a complete list of i18n options, see: 13 | * https://github.com/mashpie/i18n-node#list-of-configuration-options 14 | * 15 | * 16 | */ 17 | 18 | module.exports.i18n = { 19 | 20 | /*************************************************************************** 21 | * * 22 | * Which locales are supported? * 23 | * * 24 | ***************************************************************************/ 25 | 26 | // locales: ['en', 'es', 'fr', 'de'] 27 | 28 | /**************************************************************************** 29 | * * 30 | * What is the default locale for the site? Note that this setting will be * 31 | * overridden for any request that sends an "Accept-Language" header (i.e. * 32 | * most browsers), but it's still useful if you need to localize the * 33 | * response for requests made by non-browser clients (e.g. cURL). * 34 | * * 35 | ****************************************************************************/ 36 | 37 | // defaultLocale: 'en', 38 | 39 | /**************************************************************************** 40 | * * 41 | * Automatically add new keys to locale (translation) files when they are * 42 | * encountered during a request? * 43 | * * 44 | ****************************************************************************/ 45 | 46 | // updateFiles: false, 47 | 48 | /**************************************************************************** 49 | * * 50 | * Path (relative to app root) of directory to store locale (translation) * 51 | * files in. * 52 | * * 53 | ****************************************************************************/ 54 | 55 | // localesDirectory: '/config/locales' 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /config/csrf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cross-Site Request Forgery Protection Settings 3 | * (sails.config.csrf) 4 | * 5 | * CSRF tokens are like a tracking chip. While a session tells the server that a user 6 | * "is who they say they are", a csrf token tells the server "you are where you say you are". 7 | * 8 | * When enabled, all non-GET requests to the Sails server must be accompanied by 9 | * a special token, identified as the '_csrf' parameter. 10 | * 11 | * This option protects your Sails app against cross-site request forgery (or CSRF) attacks. 12 | * A would-be attacker needs not only a user's session cookie, but also this timestamped, 13 | * secret CSRF token, which is refreshed/granted when the user visits a URL on your app's domain. 14 | * 15 | * This allows us to have certainty that our users' requests haven't been hijacked, 16 | * and that the requests they're making are intentional and legitimate. 17 | * 18 | * This token has a short-lived expiration timeline, and must be acquired by either: 19 | * 20 | * (a) For traditional view-driven web apps: 21 | * Fetching it from one of your views, where it may be accessed as 22 | * a local variable, e.g.: 23 | *
24 | * 25 | *
26 | * 27 | * or (b) For AJAX/Socket-heavy and/or single-page apps: 28 | * Sending a GET request to the `/csrfToken` route, where it will be returned 29 | * as JSON, e.g.: 30 | * { _csrf: 'ajg4JD(JGdajhLJALHDa' } 31 | * 32 | * 33 | * Enabling this option requires managing the token in your front-end app. 34 | * For traditional web apps, it's as easy as passing the data from a view into a form action. 35 | * In AJAX/Socket-heavy apps, just send a GET request to the /csrfToken route to get a valid token. 36 | * 37 | * For more information on CSRF, check out: 38 | * http://en.wikipedia.org/wiki/Cross-site_request_forgery 39 | * 40 | * For more information on this configuration file, including info on CSRF + CORS, see: 41 | * http://beta.sailsjs.org/#/documentation/reference/sails.config/sails.config.csrf.html 42 | * 43 | */ 44 | 45 | /**************************************************************************** 46 | * * 47 | * Enabled CSRF protection for your site? * 48 | * * 49 | ****************************************************************************/ 50 | 51 | // module.exports.csrf = false; 52 | 53 | /**************************************************************************** 54 | * * 55 | * You may also specify more fine-grained settings for CSRF, including the * 56 | * domains which are allowed to request the CSRF token via AJAX. These * 57 | * settings override the general CORS settings in your config/cors.js file. * 58 | * * 59 | ****************************************************************************/ 60 | 61 | // module.exports.csrf = { 62 | // grantTokenViaAjax: true, 63 | // origin: '' 64 | // } 65 | -------------------------------------------------------------------------------- /tasks/README.md: -------------------------------------------------------------------------------- 1 | # About the `tasks` folder 2 | 3 | The `tasks` directory is a suite of Grunt tasks and their configurations, bundled for your convenience. The Grunt integration is mainly useful for bundling front-end assets, (like stylesheets, scripts, & markup templates) but it can also be used to run all kinds of development tasks, from browserify compilation to database migrations. 4 | 5 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, read on! 6 | 7 | 8 | ### How does this work? 9 | 10 | The asset pipeline bundled in Sails is a set of Grunt tasks configured with conventional defaults designed to make your project more consistent and productive. 11 | 12 | The entire front-end asset workflow in Sails is completely customizable-- while it provides some suggestions out of the box, Sails makes no pretense that it can anticipate all of the needs you'll encounter building the browser-based/front-end portion of your application. Who's to say you're even building an app for a browser? 13 | 14 | 15 | 16 | ### What tasks does Sails run automatically? 17 | 18 | Sails runs some of these tasks (the ones in the `tasks/register` folder) automatically when you run certain commands. 19 | 20 | ###### `sails lift` 21 | 22 | Runs the `default` task (`tasks/register/default.js`). 23 | 24 | ###### `sails lift --prod` 25 | 26 | Runs the `prod` task (`tasks/register/prod.js`). 27 | 28 | ###### `sails www` 29 | 30 | Runs the `build` task (`tasks/register/build.js`). 31 | 32 | ###### `sails www --prod` (production) 33 | 34 | Runs the `buildProd` task (`tasks/register/buildProd.js`). 35 | 36 | 37 | ### Can I customize this for SASS, Angular, client-side Jade templates, etc? 38 | 39 | You can modify, omit, or replace any of these Grunt tasks to fit your requirements. You can also add your own Grunt tasks- just add a `someTask.js` file in the `grunt/config` directory to configure the new task, then register it with the appropriate parent task(s) (see files in `grunt/register/*.js`). 40 | 41 | 42 | ### Do I have to use Grunt? 43 | 44 | Nope! To disable Grunt integration in Sails, just delete your Gruntfile or disable the Grunt hook. 45 | 46 | 47 | ### What if I'm not building a web frontend? 48 | 49 | That's ok! A core tenant of Sails is client-agnosticism-- it's especially designed for building APIs used by all sorts of clients; native Android/iOS/Cordova, serverside SDKs, etc. 50 | 51 | You can completely disable Grunt by following the instructions above. 52 | 53 | If you still want to use Grunt for other purposes, but don't want any of the default web front-end stuff, just delete your project's `assets` folder and remove the front-end oriented tasks from the `grunt/register` and `grunt/config` folders. You can also run `sails new myCoolApi --no-frontend` to omit the `assets` folder and front-end-oriented Grunt tasks for future projects. You can also replace your `sails-generate-frontend` module with alternative community generators, or create your own. This allows `sails new` to create the boilerplate for native iOS apps, Android apps, Cordova apps, SteroidsJS apps, etc. 54 | 55 | -------------------------------------------------------------------------------- /config/globals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Global Variable Configuration 3 | * (sails.config.globals) 4 | * 5 | * Configure which global variables which will be exposed 6 | * automatically by Sails. 7 | * 8 | * For more information on configuration, check out: 9 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.globals.html 10 | */ 11 | module.exports.globals = { 12 | 13 | /**************************************************************************** 14 | * * 15 | * Expose the lodash installed in Sails core as a global variable. If this * 16 | * is disabled, like any other node module you can always run npm install * 17 | * lodash --save, then var _ = require('lodash') at the top of any file. * 18 | * * 19 | ****************************************************************************/ 20 | 21 | // _: true, 22 | 23 | /**************************************************************************** 24 | * * 25 | * Expose the async installed in Sails core as a global variable. If this is * 26 | * disabled, like any other node module you can always run npm install async * 27 | * --save, then var async = require('async') at the top of any file. * 28 | * * 29 | ****************************************************************************/ 30 | 31 | // async: true, 32 | 33 | /**************************************************************************** 34 | * * 35 | * Expose the sails instance representing your app. If this is disabled, you * 36 | * can still get access via req._sails. * 37 | * * 38 | ****************************************************************************/ 39 | 40 | // sails: true, 41 | 42 | /**************************************************************************** 43 | * * 44 | * Expose each of your app's services as global variables (using their * 45 | * "globalId"). E.g. a service defined in api/models/NaturalLanguage.js * 46 | * would have a globalId of NaturalLanguage by default. If this is disabled, * 47 | * you can still access your services via sails.services.* * 48 | * * 49 | ****************************************************************************/ 50 | 51 | // services: true, 52 | 53 | /**************************************************************************** 54 | * * 55 | * Expose each of your app's models as global variables (using their * 56 | * "globalId"). E.g. a model defined in api/models/User.js would have a * 57 | * globalId of User by default. If this is disabled, you can still access * 58 | * your models via sails.models.*. * 59 | * * 60 | ****************************************************************************/ 61 | 62 | // models: true 63 | }; 64 | -------------------------------------------------------------------------------- /.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 | 44 | 45 | 46 | 47 | 48 | ################################################ 49 | # Dependencies 50 | # 51 | # When releasing a production app, you may 52 | # consider including your node_modules and 53 | # bower_components directory in your git repo, 54 | # but during development, its best to exclude it, 55 | # since different developers may be working on 56 | # different kernels, where dependencies would 57 | # need to be recompiled anyway. 58 | # 59 | # More on that here about node_modules dir: 60 | # http://www.futurealoof.com/posts/nodemodules-in-git.html 61 | # (credit Mikeal Rogers, @mikeal) 62 | # 63 | # About bower_components dir, you can see this: 64 | # http://addyosmani.com/blog/checking-in-front-end-dependencies/ 65 | # (credit Addy Osmani, @addyosmani) 66 | # 67 | ################################################ 68 | 69 | node_modules/* 70 | !node_modules/Local 71 | 72 | bower_components 73 | 74 | 75 | 76 | 77 | ################################################ 78 | # Sails.js / Waterline / Grunt 79 | # 80 | # Files generated by Sails and Grunt, or related 81 | # tasks and adapters. 82 | ################################################ 83 | .tmp 84 | dump.rdb 85 | 86 | 87 | 88 | 89 | 90 | ################################################ 91 | # Node.js / NPM 92 | # 93 | # Common files generated by Node, NPM, and the 94 | # related ecosystem. 95 | ################################################ 96 | lib-cov 97 | *.seed 98 | *.log 99 | *.out 100 | *.pid 101 | npm-debug.log 102 | 103 | 104 | 105 | 106 | 107 | ################################################ 108 | # Miscellaneous 109 | # 110 | # Common files generated by text editors, 111 | # operating systems, file systems, etc. 112 | ################################################ 113 | 114 | *~ 115 | *# 116 | .DS_STORE 117 | .netbeans 118 | nbproject 119 | .idea 120 | .node_history 121 | 122 | ## Our local scratch directory 123 | scratch 124 | 125 | ################################################ 126 | # Documentation 127 | # 128 | # Allow checkin of markdown (for GitHub), but 129 | # ignore all other docs 130 | ################################################ 131 | 132 | doc/* 133 | !doc/markdown 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/cors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Cross-Origin Resource Sharing (CORS) Settings 3 | * (sails.config.cors) 4 | * 5 | * CORS is like a more modern version of JSONP-- it allows your server/API 6 | * to successfully respond to requests from client-side JavaScript code 7 | * running on some other domain (e.g. google.com) 8 | * Unlike JSONP, it works with POST, PUT, and DELETE requests 9 | * 10 | * For more information on CORS, check out: 11 | * http://en.wikipedia.org/wiki/Cross-origin_resource_sharing 12 | * 13 | * Note that any of these settings (besides 'allRoutes') can be changed on a per-route basis 14 | * by adding a "cors" object to the route configuration: 15 | * 16 | * '/get foo': { 17 | * controller: 'foo', 18 | * action: 'bar', 19 | * cors: { 20 | * origin: 'http://foobar.com,https://owlhoot.com' 21 | * } 22 | * } 23 | * 24 | * For more information on this configuration file, see: 25 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.cors.html 26 | * 27 | */ 28 | 29 | module.exports.cors = { 30 | 31 | /*************************************************************************** 32 | * * 33 | * Allow CORS on all routes by default? If not, you must enable CORS on a * 34 | * per-route basis by either adding a "cors" configuration object to the * 35 | * route config, or setting "cors:true" in the route config to use the * 36 | * default settings below. * 37 | * * 38 | ***************************************************************************/ 39 | 40 | // allRoutes: false, 41 | 42 | /*************************************************************************** 43 | * * 44 | * Which domains which are allowed CORS access? This can be a * 45 | * comma-delimited list of hosts (beginning with http:// or https://) or * 46 | * "*" to allow all domains CORS access. * 47 | * * 48 | ***************************************************************************/ 49 | 50 | // origin: '*', 51 | 52 | /*************************************************************************** 53 | * * 54 | * Allow cookies to be shared for CORS requests? * 55 | * * 56 | ***************************************************************************/ 57 | 58 | // credentials: true, 59 | 60 | /*************************************************************************** 61 | * * 62 | * Which methods should be allowed for CORS requests? This is only used in * 63 | * response to preflight requests (see article linked above for more info) * 64 | * * 65 | ***************************************************************************/ 66 | 67 | // methods: 'GET, POST, PUT, DELETE, OPTIONS, HEAD', 68 | 69 | /*************************************************************************** 70 | * * 71 | * Which headers should be allowed for CORS requests? This is only used in * 72 | * response to preflight requests. * 73 | * * 74 | ***************************************************************************/ 75 | 76 | // headers: 'content-type' 77 | 78 | }; 79 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/http.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTTP Server Settings 3 | * (sails.config.http) 4 | * 5 | * Configuration for the underlying HTTP server in Sails. 6 | * Only applies to HTTP requests (not WebSockets) 7 | * 8 | * For more information on configuration, check out: 9 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.http.html 10 | */ 11 | 12 | module.exports.http = { 13 | 14 | /**************************************************************************** 15 | * * 16 | * Express middleware to use for every Sails request. To add custom * 17 | * middleware to the mix, add a function to the middleware config object and * 18 | * add its key to the "order" array. The $custom key is reserved for * 19 | * backwards-compatibility with Sails v0.9.x apps that use the * 20 | * `customMiddleware` config option. * 21 | * * 22 | ****************************************************************************/ 23 | 24 | // middleware: { 25 | 26 | /*************************************************************************** 27 | * * 28 | * The order in which middleware should be run for HTTP request. (the Sails * 29 | * router is invoked by the "router" middleware below.) * 30 | * * 31 | ***************************************************************************/ 32 | 33 | // order: [ 34 | // 'startRequestTimer', 35 | // 'cookieParser', 36 | // 'session', 37 | // 'myRequestLogger', 38 | // 'bodyParser', 39 | // 'handleBodyParserError', 40 | // 'compress', 41 | // 'methodOverride', 42 | // 'poweredBy', 43 | // '$custom', 44 | // 'router', 45 | // 'www', 46 | // 'favicon', 47 | // '404', 48 | // '500' 49 | // ], 50 | 51 | /**************************************************************************** 52 | * * 53 | * Example custom middleware; logs each request to the console. * 54 | * * 55 | ****************************************************************************/ 56 | 57 | // myRequestLogger: function (req, res, next) { 58 | // console.log("Requested :: ", req.method, req.url); 59 | // return next(); 60 | // } 61 | 62 | 63 | /*************************************************************************** 64 | * * 65 | * The body parser that will handle incoming multipart HTTP requests. By * 66 | * default as of v0.10, Sails uses * 67 | * [skipper](http://github.com/balderdashy/skipper). See * 68 | * http://www.senchalabs.org/connect/multipart.html for other options. * 69 | * * 70 | ***************************************************************************/ 71 | 72 | // bodyParser: require('skipper') 73 | 74 | // }, 75 | 76 | /*************************************************************************** 77 | * * 78 | * The number of seconds to cache flat files on disk being served by * 79 | * Express static middleware (by default, these files are in `.tmp/public`) * 80 | * * 81 | * The HTTP static cache is only active in a 'production' environment, * 82 | * since that's the only time Express will cache flat-files. * 83 | * * 84 | ***************************************************************************/ 85 | 86 | // cache: 31557600000 87 | }; 88 | -------------------------------------------------------------------------------- /config/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Session Configuration 3 | * (sails.config.session) 4 | * 5 | * Sails session integration leans heavily on the great work already done by 6 | * Express, but also unifies Socket.io with the Connect session store. It uses 7 | * Connect's cookie parser to normalize configuration differences between Express 8 | * and Socket.io and hooks into Sails' middleware interpreter to allow you to access 9 | * and auto-save to `req.session` with Socket.io the same way you would with Express. 10 | * 11 | * For more information on configuring the session, check out: 12 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.session.html 13 | */ 14 | 15 | module.exports.session = { 16 | 17 | /*************************************************************************** 18 | * * 19 | * Session secret is automatically generated when your new app is created * 20 | * Replace at your own risk in production-- you will invalidate the cookies * 21 | * of your users, forcing them to log in again. * 22 | * * 23 | ***************************************************************************/ 24 | secret: '93f593a12fcd4ba7d5b2b918e67ab66a', 25 | 26 | 27 | /*************************************************************************** 28 | * * 29 | * Set the session cookie expire time The maxAge is set by milliseconds, * 30 | * the example below is for 24 hours * 31 | * * 32 | ***************************************************************************/ 33 | 34 | // cookie: { 35 | // maxAge: 24 * 60 * 60 * 1000 36 | // } 37 | 38 | /*************************************************************************** 39 | * * 40 | * In production, uncomment the following lines to set up a shared redis * 41 | * session store that can be shared across multiple Sails.js servers * 42 | ***************************************************************************/ 43 | 44 | // adapter: 'redis', 45 | 46 | /*************************************************************************** 47 | * * 48 | * The following values are optional, if no options are set a redis * 49 | * instance running on localhost is expected. Read more about options at: * 50 | * https://github.com/visionmedia/connect-redis * 51 | * * 52 | * * 53 | ***************************************************************************/ 54 | 55 | // host: 'localhost', 56 | // port: 6379, 57 | // ttl: , 58 | // db: 0, 59 | // pass: 60 | // prefix: 'sess:' 61 | 62 | 63 | /*************************************************************************** 64 | * * 65 | * Uncomment the following lines to use your Mongo adapter as a session * 66 | * store * 67 | * * 68 | ***************************************************************************/ 69 | 70 | // adapter: 'mongo', 71 | // host: 'localhost', 72 | // port: 27017, 73 | // db: 'sails', 74 | // collection: 'sessions', 75 | 76 | /*************************************************************************** 77 | * * 78 | * Optional Values: * 79 | * * 80 | * # Note: url will override other connection settings url: * 81 | * 'mongodb://user:pass@host:port/database/collection', * 82 | * * 83 | ***************************************************************************/ 84 | 85 | // username: '', 86 | // password: '', 87 | // auto_reconnect: false, 88 | // ssl: false, 89 | // stringify: true 90 | 91 | }; 92 | -------------------------------------------------------------------------------- /config/views.js: -------------------------------------------------------------------------------- 1 | /** 2 | * View Engine Configuration 3 | * (sails.config.views) 4 | * 5 | * Server-sent views are a classic and effective way to get your app up 6 | * and running. Views are normally served from controllers. Below, you can 7 | * configure your templating language/framework of choice and configure 8 | * Sails' layout support. 9 | * 10 | * For more information on views and layouts, check out: 11 | * http://sailsjs.org/#/documentation/concepts/Views 12 | */ 13 | 14 | module.exports.views = { 15 | 16 | /**************************************************************************** 17 | * * 18 | * View engine (aka template language) to use for your app's *server-side* * 19 | * views * 20 | * * 21 | * Sails+Express supports all view engines which implement TJ Holowaychuk's * 22 | * `consolidate.js`, including, but not limited to: * 23 | * * 24 | * ejs, jade, handlebars, mustache underscore, hogan, haml, haml-coffee, * 25 | * dust atpl, eco, ect, jazz, jqtpl, JUST, liquor, QEJS, swig, templayed, * 26 | * toffee, walrus, & whiskers * 27 | * * 28 | * For more options, check out the docs: * 29 | * https://github.com/balderdashy/sails-wiki/blob/0.9/config.views.md#engine * 30 | * * 31 | ****************************************************************************/ 32 | 33 | engine: 'ejs', 34 | 35 | 36 | /**************************************************************************** 37 | * * 38 | * Layouts are simply top-level HTML templates you can use as wrappers for * 39 | * your server-side views. If you're using ejs or jade, you can take * 40 | * advantage of Sails' built-in `layout` support. * 41 | * * 42 | * When using a layout, when one of your views is served, it is injected * 43 | * into the `body` partial defined in the layout. This lets you reuse header * 44 | * and footer logic between views. * 45 | * * 46 | * NOTE: Layout support is only implemented for the `ejs` view engine! * 47 | * For most other engines, it is not necessary, since they implement * 48 | * partials/layouts themselves. In those cases, this config will be * 49 | * silently ignored. * 50 | * * 51 | * The `layout` setting may be set to one of the following: * 52 | * * 53 | * If `false`, layouts will be disabled. Otherwise, if a string is * 54 | * specified, it will be interpreted as the relative path to your layout * 55 | * file from `views/` folder. (the file extension, ".ejs", should be * 56 | * omitted) * 57 | * * 58 | ****************************************************************************/ 59 | 60 | layout: 'layout' 61 | 62 | /**************************************************************************** 63 | * * 64 | * Using Multiple Layouts with EJS * 65 | * * 66 | * If you're using the default engine, `ejs`, Sails supports the use of * 67 | * multiple `layout` files. To take advantage of this, before rendering a * 68 | * view, override the `layout` local in your controller by setting * 69 | * `res.locals.layout`. (this is handy if you parts of your app's UI look * 70 | * completely different from each other) * 71 | * * 72 | * e.g. your default might be * 73 | * layout: 'layouts/public' * 74 | * * 75 | * But you might override that in some of your controllers with: * 76 | * layout: 'layouts/internal' * 77 | * * 78 | ****************************************************************************/ 79 | 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /config/connections.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Connections 3 | * (sails.config.connections) 4 | * 5 | * `Connections` are like "saved settings" for your adapters. What's the difference between 6 | * a connection and an adapter, you might ask? An adapter (e.g. `sails-mysql`) is generic-- 7 | * it needs some additional information to work (e.g. your database host, password, user, etc.) 8 | * A `connection` is that additional information. 9 | * 10 | * Each model must have a `connection` property (a string) which is references the name of one 11 | * of these connections. If it doesn't, the default `connection` configured in `config/models.js` 12 | * will be applied. Of course, a connection can (and usually is) shared by multiple models. 13 | * . 14 | * Note: If you're using version control, you should put your passwords/api keys 15 | * in `config/local.js`, environment variables, or use another strategy. 16 | * (this is to prevent you inadvertently sensitive credentials up to your repository.) 17 | * 18 | * For more information on configuration, check out: 19 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.connections.html 20 | */ 21 | 22 | module.exports.connections = { 23 | 24 | /*************************************************************************** 25 | * * 26 | * Local disk storage for DEVELOPMENT ONLY * 27 | * * 28 | * Installed by default. * 29 | * * 30 | ***************************************************************************/ 31 | localDiskDb: { 32 | adapter: 'sails-disk' 33 | }, 34 | 35 | /*************************************************************************** 36 | * * 37 | * MySQL is the world's most popular relational database. * 38 | * http://en.wikipedia.org/wiki/MySQL * 39 | * * 40 | * Run: npm install sails-mysql * 41 | * * 42 | ***************************************************************************/ 43 | someMysqlServer: { 44 | adapter: 'sails-mysql', 45 | host: 'YOUR_MYSQL_SERVER_HOSTNAME_OR_IP_ADDRESS', 46 | user: 'YOUR_MYSQL_USER', 47 | password: 'YOUR_MYSQL_PASSWORD', 48 | database: 'YOUR_MYSQL_DB' 49 | }, 50 | 51 | /*************************************************************************** 52 | * * 53 | * MongoDB is the leading NoSQL database. * 54 | * http://en.wikipedia.org/wiki/MongoDB * 55 | * * 56 | * Run: npm install sails-mongo * 57 | * * 58 | ***************************************************************************/ 59 | someMongodbServer: { 60 | adapter: 'sails-mongo', 61 | host: 'localhost', 62 | port: 27017, 63 | // user: 'username', 64 | // password: 'password', 65 | // database: 'your_mongo_db_name_here' 66 | }, 67 | 68 | /*************************************************************************** 69 | * * 70 | * PostgreSQL is another officially supported relational database. * 71 | * http://en.wikipedia.org/wiki/PostgreSQL * 72 | * * 73 | * Run: npm install sails-postgresql * 74 | * * 75 | * * 76 | ***************************************************************************/ 77 | somePostgresqlServer: { 78 | adapter: 'sails-postgresql', 79 | host: 'YOUR_POSTGRES_SERVER_HOSTNAME_OR_IP_ADDRESS', 80 | user: 'YOUR_POSTGRES_USER', 81 | password: 'YOUR_POSTGRES_PASSWORD', 82 | database: 'YOUR_POSTGRES_DB' 83 | }, 84 | 85 | 86 | /*************************************************************************** 87 | * * 88 | * More adapters: https://github.com/balderdashy/sails * 89 | * * 90 | ***************************************************************************/ 91 | 92 | mysql1: { 93 | adapter: 'sails-mysql', 94 | host: '127.0.0.1', 95 | user: 'webapp', 96 | password: 'pa55word', 97 | database: 'sails' 98 | } 99 | 100 | 101 | }; 102 | -------------------------------------------------------------------------------- /views/homepage.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 12 | 13 |
14 |
15 |

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

16 |

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

17 |
18 |
19 | 21 |
    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 controllerapi/controllers/UserController.js. 28 |

    29 |
    30 |
  • 31 |
  • 32 |
    33 |
    34 |

    35 | Lift your app. 36 |

    37 |

    38 | Run sails lift to start up your app server. If you visit http://localhost:1337/user in your browser, you'll see a WebSocket-compatible user API. 39 |

    40 |
    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 | -------------------------------------------------------------------------------- /tasks/config/sails-linker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Autoinsert script tags (or other filebased tags) in an html file. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Automatically inject ', 22 | appRoot: '.tmp/public' 23 | }, 24 | files: { 25 | '.tmp/public/**/*.html': require('../pipeline').jsFilesToInject, 26 | 'views/**/*.html': require('../pipeline').jsFilesToInject, 27 | 'views/**/*.ejs': require('../pipeline').jsFilesToInject 28 | } 29 | }, 30 | 31 | devJsRelative: { 32 | options: { 33 | startTag: '', 34 | endTag: '', 35 | fileTmpl: '', 36 | appRoot: '.tmp/public', 37 | relative: true 38 | }, 39 | files: { 40 | '.tmp/public/**/*.html': require('../pipeline').jsFilesToInject, 41 | 'views/**/*.html': require('../pipeline').jsFilesToInject, 42 | 'views/**/*.ejs': require('../pipeline').jsFilesToInject 43 | } 44 | }, 45 | 46 | prodJs: { 47 | options: { 48 | startTag: '', 49 | endTag: '', 50 | fileTmpl: '', 51 | appRoot: '.tmp/public' 52 | }, 53 | files: { 54 | '.tmp/public/**/*.html': ['.tmp/public/min/production.min.js'], 55 | 'views/**/*.html': ['.tmp/public/min/production.min.js'], 56 | 'views/**/*.ejs': ['.tmp/public/min/production.min.js'] 57 | } 58 | }, 59 | 60 | prodJsRelative: { 61 | options: { 62 | startTag: '', 63 | endTag: '', 64 | fileTmpl: '', 65 | appRoot: '.tmp/public', 66 | relative: true 67 | }, 68 | files: { 69 | '.tmp/public/**/*.html': ['.tmp/public/min/production.min.js'], 70 | 'views/**/*.html': ['.tmp/public/min/production.min.js'], 71 | 'views/**/*.ejs': ['.tmp/public/min/production.min.js'] 72 | } 73 | }, 74 | 75 | devStyles: { 76 | options: { 77 | startTag: '', 78 | endTag: '', 79 | fileTmpl: '', 80 | appRoot: '.tmp/public' 81 | }, 82 | 83 | files: { 84 | '.tmp/public/**/*.html': require('../pipeline').cssFilesToInject, 85 | 'views/**/*.html': require('../pipeline').cssFilesToInject, 86 | 'views/**/*.ejs': require('../pipeline').cssFilesToInject 87 | } 88 | }, 89 | 90 | devStylesRelative: { 91 | options: { 92 | startTag: '', 93 | endTag: '', 94 | fileTmpl: '', 95 | appRoot: '.tmp/public', 96 | relative: true 97 | }, 98 | 99 | files: { 100 | '.tmp/public/**/*.html': require('../pipeline').cssFilesToInject, 101 | 'views/**/*.html': require('../pipeline').cssFilesToInject, 102 | 'views/**/*.ejs': require('../pipeline').cssFilesToInject 103 | } 104 | }, 105 | 106 | prodStyles: { 107 | options: { 108 | startTag: '', 109 | endTag: '', 110 | fileTmpl: '', 111 | appRoot: '.tmp/public' 112 | }, 113 | files: { 114 | '.tmp/public/index.html': ['.tmp/public/min/production.min.css'], 115 | 'views/**/*.html': ['.tmp/public/min/production.min.css'], 116 | 'views/**/*.ejs': ['.tmp/public/min/production.min.css'] 117 | } 118 | }, 119 | 120 | prodStylesRelative: { 121 | options: { 122 | startTag: '', 123 | endTag: '', 124 | fileTmpl: '', 125 | appRoot: '.tmp/public', 126 | relative: true 127 | }, 128 | files: { 129 | '.tmp/public/index.html': ['.tmp/public/min/production.min.css'], 130 | 'views/**/*.html': ['.tmp/public/min/production.min.css'], 131 | 'views/**/*.ejs': ['.tmp/public/min/production.min.css'] 132 | } 133 | }, 134 | 135 | // Bring in JST template object 136 | devTpl: { 137 | options: { 138 | startTag: '', 139 | endTag: '', 140 | fileTmpl: '', 141 | appRoot: '.tmp/public' 142 | }, 143 | files: { 144 | '.tmp/public/index.html': ['.tmp/public/jst.js'], 145 | 'views/**/*.html': ['.tmp/public/jst.js'], 146 | 'views/**/*.ejs': ['.tmp/public/jst.js'] 147 | } 148 | }, 149 | 150 | devJsJade: { 151 | options: { 152 | startTag: '// SCRIPTS', 153 | endTag: '// SCRIPTS END', 154 | fileTmpl: 'script(src="%s")', 155 | appRoot: '.tmp/public' 156 | }, 157 | files: { 158 | 'views/**/*.jade': require('../pipeline').jsFilesToInject 159 | } 160 | }, 161 | 162 | devJsRelativeJade: { 163 | options: { 164 | startTag: '// SCRIPTS', 165 | endTag: '// SCRIPTS END', 166 | fileTmpl: 'script(src="%s")', 167 | appRoot: '.tmp/public', 168 | relative: true 169 | }, 170 | files: { 171 | 'views/**/*.jade': require('../pipeline').jsFilesToInject 172 | } 173 | }, 174 | 175 | prodJsJade: { 176 | options: { 177 | startTag: '// SCRIPTS', 178 | endTag: '// SCRIPTS END', 179 | fileTmpl: 'script(src="%s")', 180 | appRoot: '.tmp/public' 181 | }, 182 | files: { 183 | 'views/**/*.jade': ['.tmp/public/min/production.min.js'] 184 | } 185 | }, 186 | 187 | prodJsRelativeJade: { 188 | options: { 189 | startTag: '// SCRIPTS', 190 | endTag: '// SCRIPTS END', 191 | fileTmpl: 'script(src="%s")', 192 | appRoot: '.tmp/public', 193 | relative: true 194 | }, 195 | files: { 196 | 'views/**/*.jade': ['.tmp/public/min/production.min.js'] 197 | } 198 | }, 199 | 200 | devStylesJade: { 201 | options: { 202 | startTag: '// STYLES', 203 | endTag: '// STYLES END', 204 | fileTmpl: 'link(rel="stylesheet", href="%s")', 205 | appRoot: '.tmp/public' 206 | }, 207 | 208 | files: { 209 | 'views/**/*.jade': require('../pipeline').cssFilesToInject 210 | } 211 | }, 212 | 213 | devStylesRelativeJade: { 214 | options: { 215 | startTag: '// STYLES', 216 | endTag: '// STYLES END', 217 | fileTmpl: 'link(rel="stylesheet", href="%s")', 218 | appRoot: '.tmp/public', 219 | relative: true 220 | }, 221 | 222 | files: { 223 | 'views/**/*.jade': require('../pipeline').cssFilesToInject 224 | } 225 | }, 226 | 227 | prodStylesJade: { 228 | options: { 229 | startTag: '// STYLES', 230 | endTag: '// STYLES END', 231 | fileTmpl: 'link(rel="stylesheet", href="%s")', 232 | appRoot: '.tmp/public' 233 | }, 234 | files: { 235 | 'views/**/*.jade': ['.tmp/public/min/production.min.css'] 236 | } 237 | }, 238 | 239 | prodStylesRelativeJade: { 240 | options: { 241 | startTag: '// STYLES', 242 | endTag: '// STYLES END', 243 | fileTmpl: 'link(rel="stylesheet", href="%s")', 244 | appRoot: '.tmp/public', 245 | relative: true 246 | }, 247 | files: { 248 | 'views/**/*.jade': ['.tmp/public/min/production.min.css'] 249 | } 250 | }, 251 | 252 | // Bring in JST template object 253 | devTplJade: { 254 | options: { 255 | startTag: '// TEMPLATES', 256 | endTag: '// TEMPLATES END', 257 | fileTmpl: 'script(type="text/javascript", src="%s")', 258 | appRoot: '.tmp/public' 259 | }, 260 | files: { 261 | 'views/**/*.jade': ['.tmp/public/jst.js'] 262 | } 263 | } 264 | }); 265 | 266 | grunt.loadNpmTasks('grunt-sails-linker'); 267 | }; 268 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/sockets.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WebSocket Server Settings 3 | * (sails.config.sockets) 4 | * 5 | * These settings provide transparent access to the options for Sails' 6 | * encapsulated WebSocket server, as well as some additional Sails-specific 7 | * configuration layered on top. 8 | * 9 | * For more information on sockets configuration, including advanced config options, see: 10 | * http://sailsjs.org/#/documentation/reference/sails.config/sails.config.sockets.html 11 | */ 12 | 13 | module.exports.sockets = { 14 | 15 | /*************************************************************************** 16 | * * 17 | * This custom onConnect function will be run each time AFTER a new socket * 18 | * connects (To control whether a socket is allowed to connect, check out * 19 | * `authorization` config.) Keep in mind that Sails' RESTful simulation for * 20 | * sockets mixes in socket.io events for your routes and blueprints * 21 | * automatically. * 22 | * * 23 | ***************************************************************************/ 24 | onConnect: function(session, socket) { 25 | 26 | // By default, do nothing. 27 | 28 | }, 29 | 30 | 31 | /*************************************************************************** 32 | * * 33 | * This custom onDisconnect function will be run each time a socket * 34 | * disconnects * 35 | * * 36 | ***************************************************************************/ 37 | onDisconnect: function(session, socket) { 38 | 39 | // By default: do nothing. 40 | }, 41 | 42 | 43 | /*************************************************************************** 44 | * * 45 | * `transports` * 46 | * * 47 | * A array of allowed transport methods which the clients will try to use. * 48 | * The flashsocket transport is disabled by default You can enable * 49 | * flashsockets by adding 'flashsocket' to this list: * 50 | * * 51 | ***************************************************************************/ 52 | // transports: [ 53 | // 'websocket', 54 | // 'htmlfile', 55 | // 'xhr-polling', 56 | // 'jsonp-polling' 57 | // ], 58 | 59 | /*************************************************************************** 60 | * * 61 | * Use this option to set the datastore socket.io will use to manage * 62 | * rooms/sockets/subscriptions: default: memory * 63 | * * 64 | ***************************************************************************/ 65 | 66 | // adapter: 'memory', 67 | 68 | /*************************************************************************** 69 | * * 70 | * Node.js (and consequently Sails.js) apps scale horizontally. It's a * 71 | * powerful, efficient approach, but it involves a tiny bit of planning. At * 72 | * scale, you'll want to be able to copy your app onto multiple Sails.js * 73 | * servers and throw them behind a load balancer. * 74 | * * 75 | * One of the big challenges of scaling an application is that these sorts * 76 | * of clustered deployments cannot share memory, since they are on * 77 | * physically different machines. On top of that, there is no guarantee * 78 | * that a user will "stick" with the same server between requests (whether * 79 | * HTTP or sockets), since the load balancer will route each request to the * 80 | * Sails server with the most available resources. However that means that * 81 | * all room/pubsub/socket processing and shared memory has to be offloaded * 82 | * to a shared, remote messaging queue (usually Redis) * 83 | * * 84 | * Luckily, Socket.io (and consequently Sails.js) apps support Redis for * 85 | * sockets by default. To enable a remote redis pubsub server, uncomment * 86 | * the config below. * 87 | * * 88 | * Worth mentioning is that, if `adapter` config is `redis`, but host/port * 89 | * is left unset, Sails will try to connect to redis running on localhost * 90 | * via port 6379 * 91 | * * 92 | ***************************************************************************/ 93 | 94 | // adapter: 'redis', 95 | // host: '127.0.0.1', 96 | // port: 6379, 97 | // db: 'sails', 98 | // pass: '' 99 | 100 | 101 | /*************************************************************************** 102 | * * 103 | * `authorization` * 104 | * * 105 | * Global authorization for Socket.IO access, this is called when the * 106 | * initial handshake is performed with the server. * 107 | * * 108 | * By default (`authorization: false`), when a socket tries to connect, * 109 | * Sails allows it, every time. If no valid cookie was sent, a temporary * 110 | * session will be created for the connecting socket. * 111 | * * 112 | * If `authorization: true`, before allowing a connection, Sails verifies * 113 | * that a valid cookie was sent with the upgrade request. If the cookie * 114 | * doesn't match any known user session, a new user session is created for * 115 | * it. (In most cases, the user would already have a cookie since they * 116 | * loaded the socket.io client and the initial HTML page.) * 117 | * * 118 | * However, in the case of cross-domain requests, it is possible to receive * 119 | * a connection upgrade request WITHOUT A COOKIE (for certain transports) * 120 | * In this case, there is no way to keep track of the requesting user * 121 | * between requests, since there is no identifying information to link * 122 | * him/her with a session. The sails.io.js client solves this by connecting * 123 | * to a CORS endpoint first to get a 3rd party cookie (fortunately this * 124 | * works, even in Safari), then opening the connection. * 125 | * * 126 | * You can also pass along a ?cookie query parameter to the upgrade url, * 127 | * which Sails will use in the absense of a proper cookie e.g. (when * 128 | * connection from the client): * 129 | * io.connect('http://localhost:1337?cookie=smokeybear') * 130 | * * 131 | * (Un)fortunately, the user's cookie is (should!) not accessible in * 132 | * client-side js. Using HTTP-only cookies is crucial for your app's * 133 | * security. Primarily because of this situation, as well as a handful of * 134 | * other advanced use cases, Sails allows you to override the authorization * 135 | * behavior with your own custom logic by specifying a function, e.g: * 136 | * * 137 | * authorization: function authSocketConnectionAttempt(reqObj, cb) { * 138 | * * 139 | * // Any data saved in `handshake` is available in subsequent * 140 | * requests from this as `req.socket.handshake.*` * 141 | * * 142 | * // to allow the connection, call `cb(null, true)` * 143 | * // to prevent the connection, call `cb(null, false)` * 144 | * // to report an error, call `cb(err)` * 145 | * } * 146 | * * 147 | ***************************************************************************/ 148 | 149 | // authorization: false, 150 | 151 | /*************************************************************************** 152 | * * 153 | * Whether to run code which supports legacy usage for connected sockets * 154 | * running the v0.9 version of the socket client SDK (i.e. sails.io.js). * 155 | * Disabled in newly generated projects, but enabled as an implicit default * 156 | * (i.e. legacy usage/v0.9 clients be supported if this property is set to * 157 | * true, but also if it is removed from this configuration file or set to * 158 | * `undefined`) * 159 | * * 160 | ***************************************************************************/ 161 | 162 | // 'backwardsCompatibilityFor0.9SocketClients': false, 163 | 164 | /*************************************************************************** 165 | * * 166 | * Whether to expose a 'get /__getcookie' route with CORS support that sets * 167 | * a cookie (this is used by the sails.io.js socket client to get access to * 168 | * a 3rd party cookie and to enable sessions). * 169 | * * 170 | * Warning: Currently in this scenario, CORS settings apply to interpreted * 171 | * requests sent via a socket.io connection that used this cookie to * 172 | * connect, even for non-browser clients! (e.g. iOS apps, toasters, node.js * 173 | * unit tests) * 174 | * * 175 | ***************************************************************************/ 176 | 177 | // grant3rdPartyCookie: true, 178 | 179 | /*************************************************************************** 180 | * * 181 | * Match string representing the origins that are allowed to connect to the * 182 | * Socket.IO server * 183 | * * 184 | ***************************************************************************/ 185 | 186 | // origins: '*:*', 187 | 188 | }; 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sails-tutorial-1 2 | 3 | a [Sails](http://sailsjs.org) application 4 | 5 | ## Who Are These Tutorials For? 6 | 7 | This tutorial is aimed at the professional programmer, who already knows a 8 | language or two well, coming from a classical OOP background, and wants a 9 | quick, pragmatic introduction to the things that matter in the Javascript 10 | world. It's essentially a cleaned up journal of my own investigations 11 | for that purpose. 12 | 13 | If you want a gentle intro to Javascript, this probably isn't it. There are 14 | lots of tutorials for that. This is the condensed notes for going from 15 | classical OOP to pragmatic, pretty good, Node.js/Javascript programmer. 16 | The mechanism for doing this is learning about a particular framework, 17 | Sails.js, which is built on Node.js, using packages like Lodash, Express, 18 | Waterline and Bluebird. You'll also learn some of the most important 19 | aspects of Node.js, Sails.js and Javascript code organization, mostly 20 | in the context of backend programming. Javascript frontend programming 21 | is an entirely different beast which requires much more knowledge of 22 | browsers and their particular quirks. While many of the things we'll cover 23 | are relevant, there is a huge other piece to understanding how to do 24 | frontend well. 25 | 26 | I'll try to hit all of the things I find important about getting started 27 | with a good foot forward in the Javascript culture. So we'll try to make 28 | judicious choices for the libraries we learn, as well as what we focus 29 | on. You should be comfortable reading lots of code. I try to focus on 30 | the important things - how everything connects in a system, as well as 31 | things that might seem strange to someone coming from say, a Java 32 | background. My method for this is by example - we go over real world, 33 | pragmatic and pithy examples, pointing out interesting issues. 34 | 35 | Also, there are essentially step-by-step instructions to help you exercise 36 | "muscle memory." 37 | 38 | Also, this is a learning experience for me as well, so I'll definitely 39 | make some mistakes along the way. But you'll benefit from my inquiry 40 | as a good programmer, who is a novice in another language/culture. 41 | Also, this isn't my first rodeo, so the conventions I arrive at should 42 | be pretty good. 43 | 44 | ## Sails.js Tutorial and More 45 | 46 | I created this tutorial to help me learn Sails.js as well as advance my knowledge 47 | of Javascript and Node.js. My intent is to explore using Sails as a rapid prototyping 48 | tool for standards-compliant REST services. 49 | 50 | It's my experience that when you try to explain things to others, you arrive 51 | at a much deeper understanding of the material. Another benefit of documenting 52 | this as a series of tutorials is I can point programmers I may be mentoring to 53 | them. This is a fairly time consuming but important aspect of running a team 54 | of developers. 55 | 56 | While I had started intending for this to be strictly about Sails, since Sails is 57 | an integrated framework, it made sense to expand my inquiry as I went to include 58 | important libraries, tools, practices, and Node itself. So I use Sails as a 59 | particular view into the Node.js world. If you don't know much about Node.js 60 | and the related tools, this is a good way to dip a toe in and learn about these 61 | things within a useful context. I'd argue learning sails is a good way to learn 62 | about Node itself as it touches so many important concepts in a non-trivial way. 63 | 64 | Note, I am a Node.js novice but an experienced programmer, and so that's the 65 | intended audience for this tutorial. The goal is to learn Sails.js for 66 | backend prototyping, as well as all of the normal stuff that a professional 67 | programmer wants to know. For example, I go over promises, which arguably seem 68 | "advanced," (they're quite simple actually) because they make the usual 69 | rat's nest of continuation passing style programming, a lot saner. I also 70 | go through the trouble of selecting and hooking up an automated code documentation 71 | system - not the sexiest of topics, but one every good programmer will want 72 | to have in place. 73 | 74 | Thus it's my hope that by the end of this tutorial I'll have arrived at a good 75 | coding setup in terms of style, organization, tool selection and use, etc. At 76 | least that's the intent. 77 | 78 | I will (git) tag the project in its finished state after each tutorial, so one 79 | can see the changes that were made. 80 | 81 | Each successive tutorial is located in TUTORIALS/TUTORIALXX.md. 82 | 83 | 84 | ## Note on Prerequisites 85 | 86 | I take an incremental approach while paying attention to the flow of the tutorials. 87 | But my intent is to reach a level of fluency with the tools. Thus, I'll keep things 88 | simple, but won't dumb things down, as I attempt to reach a pragmatic balance for 89 | maintainable and scalable production coding practices and processes. 90 | 91 | ## Finished Tutorial State 92 | 93 | At the end of each tutorial, the project will have incrmental changes reflecting 94 | the tutorial. The easiest way to get those changes is to look at the tags: 95 | 96 | ```ScriptSession 97 | $ git pull 98 | $ git tag --list 99 | tutorial.01 100 | tutorial.01.1 101 | tutorial.01.2 102 | ... 103 | ``` 104 | 105 | The tag you should get is the latest one with the same number as the tutorial. 106 | For example, in the tag list above, if you want the project in its state at the 107 | end of TUTORIAL01.md, you should get the tag "tutorial.01.2". The minor number 108 | reflects a minor update which is usually a documentation or tiny code fix. In the 109 | listing of tags above it shows I updated the tutorial twice (with minor fixes). 110 | 111 | To make a new branch from the tag "tutorial.01.2" you can do: 112 | 113 | ```ScriptSession 114 | $ git checkout -b new_branch_name tutorial.01.2 115 | ``` 116 | 117 | You can then compare the results of this branch to your existing branch if you 118 | are having problems, or to a previous tag to see all the differences. 119 | 120 | ## Tutorial Content 121 | 122 | ### Tutorial 01 123 | 124 | Take a look at [TUTORIAL01.md](https://github.com/grokible/sails-tutorial-1/blob/master/TUTORIALS/TUTORIAL01.md). 125 | You'll learn: 126 | 127 | * Installing Sails.js and optional libraries using npm. 128 | * Starting and stopping Sails.js and the bootstrap process. 129 | * Modifying Sails configuration, and an intro to the global config object 130 | which represents the bootstrapped Sails system. Important configuration 131 | such as connectors (mysql) and migration type. 132 | * Using Sails model and controller generator scripts to create modules with stub functions. 133 | * Using blueprint convenience routes. 134 | * Using the sails-disk and sails-mysql persistent stores. 135 | * Defining Sails model schema and the mapping to MySQL database column attributes. Interacting 136 | with the MySQL database, and debugging. Dealing with schema changes. 137 | 138 | To Start: just follow [TUTORIAL01.md](https://github.com/grokible/sails-tutorial-1/blob/master/TUTORIALS/TUTORIAL01.md). 139 | To Get Finished Tutorial State: `git checkout -b new_branch_name tutorial.01.2` 140 | 141 | **References:** 142 | 143 | * [Sails to Meteor Comparison](http://stackoverflow.com/questions/22202286/sails-js-vs-meteor-what-are-the-advantages-of-both). 144 | * [Sails docs on Models](http://sailsjs.org/#!documentation/models) 145 | * [Sails docs on Controllers](http://links.sailsjs.org/docs/controllers) 146 | * [Sails docs on ORM Attributes of Models](http://sailsjs.org/#/documentation/concepts/ORM/Attributes.html). 147 | * [Note on migration settings](http://sailsjs.org/#/documentation/concepts/ORM/model-settings.html?q=migrate) 148 | * [Sails blueprint api reference](http://sailsjs.org/#/documentation/reference/blueprint-api) 149 | * [Convention over Configuration, Wikipedia](http://en.wikipedia.org/wiki/Convention_over_configuration) 150 | * [Validator library](https://github.com/chriso/validator.js) 151 | * [Sails validations, not recommended](http://sailsjs.org/#/documentation/concepts/ORM/Validations.html) 152 | 153 | ### Tutorial 02 154 | 155 | Take a look at [TUTORIAL02.md](https://github.com/grokible/sails-tutorial-1/blob/master/TUTORIALS/TUTORIAL02.md). 156 | You'll learn: 157 | 158 | * Use declarative parameter validation for controllers via the Joi library. This gives us full control over 159 | error handling and preparing parameters before updating our models, which is crucial. 160 | * Use bluebird promises for the asynchronous portion which improves maintainability and 161 | readability of tricky code. 162 | * Take direct control of interaction with the Waterline ORM (User.create), which gives us a lot of 163 | expressive power. 164 | * Do proper error handling for returning JSON error information. 165 | * Code organization through both modules and OOP prototype classes, as well as physical 166 | organization in the Sails application directory structure. We have a way of intermingling 167 | our Local code, with external dependencies. Our conventions play well with Node, Sails and 168 | GitHub. 169 | * Routing in Sails, as well as how to selectively turn routes on/off to incrementally move 170 | from a prototyping situation to a more robust, full-featured, fully controlled app. 171 | * Waterline ORM's basic CRUD operations and expressive power via it's query expression system, and ability to 172 | quickly experiment using the sails console (REPL). 173 | * Using basics of npm for installation and update of external packages, as well as 174 | automating common tasks such as running unit tests. 175 | * Very basic unit testing capabilities, appropriate for simple utility libraries. 176 | * Documentation tagging standards (JSDoc) and automatic API documentation generation. 177 | * Basic debugging using simple HTTP tools like Postman. 178 | * Exposure to some high-quality, low-level Javascript libraries such as Lodash, Express, Bluebird, 179 | Joi, Waterline, Bcrypt. 180 | 181 | To Start: `git checkout -b new_branch_name tutorial.01.2` and then follow [TUTORIAL02.md](https://github.com/grokible/sails-tutorial-1/blob/master/TUTORIALS/TUTORIAL02.md). 182 | To Get Finished Tutorial State: `git checkout -b new_branch_name tutorial.02.5` 183 | 184 | **References:** 185 | 186 | * [Sails blueprint api reference](http://sailsjs.org/#/documentation/reference/blueprint-api) 187 | * [Sails Custom Routing docs](http://sailsjs.org/#/documentation/concepts/Routes/RouteTargetSyntax.html) 188 | * [Express API](http://expressjs.com/api.html) 189 | * [Blog post on better abstractions, e.g. Promises](http://jeditoolkit.com/2012/04/26/code-logic-not-mechanics.html#post) 190 | * [Futures and Promises, Wikipedia](http://en.wikipedia.org/wiki/Futures_and_promises). 191 | * [Bluebird Promises, GitHub](https://github.com/petkaantonov/bluebird/blob/master/API.md) 192 | * [Blog Post comparing Async Library to Bluebird](http://spion.github.io/posts/why-i-am-switching-to-promises.html) 193 | * [Sails Response Object docs](http://sailsjs.org/#/documentation/reference/res) 194 | * [Express Response Object docs](http://expressjs.com/3x/api.html) 195 | * [JSDoc documentation](http://usejsdoc.org/) 196 | * [Detailed sails.config reference](http://sailsjs.org/#/documentation/reference/sails.config) 197 | * [Introduction to Waterline](http://sailsjs.org/#/documentation/concepts/ORM) 198 | * [Github Waterline project docs](https://github.com/balderdashy/waterline-docs). 199 | * [Waterline Queries](https://github.com/balderdashy/waterline-docs/blob/master/query.md) 200 | * [Waterline Query Methods (CRUD)](https://github.com/balderdashy/waterline-docs/blob/master/query-methods.md) 201 | * [Waterline query language docs](https://github.com/balderdashy/waterline-docs/blob/master/query-language.md) 202 | * [Mocha Testing Framework docs](http://mochajs.org/) 203 | * [Joi Validation Library API](https://www.npmjs.com/package/joi) 204 | * [NPM script field documentation](https://docs.npmjs.com/misc/scripts) 205 | 206 | ### Tutorial 03 207 | 208 | Take a look at [TUTORIAL03.md](https://github.com/grokible/sails-tutorial-1/blob/master/TUTORIALS/TUTORIAL03.md). 209 | You'll learn: 210 | 211 | * Standardizing on ECMAScript 5.1 and some of the details of what that means. 212 | * Understand the basic mechanisms for prototypal inheritance. 213 | * Apply prototypal inheritance mechanisms to construct some reliable classical 214 | OOP patterns: inheritance, private implementation details, singletons. 215 | * Demonstrating OOP properties using simple assert tests. 216 | * Several methods of insulating our classes private implementation details 217 | including class level static variables, and ES6 Symbol for instance variables. 218 | * Apply our new OOP patterns to solve some basic object problems for our own 219 | Error class, and some other classes which make working with Sails controllers 220 | simple. 221 | * Investigation, critique and adjustments/enhancements to some of these OOP 222 | patterns. 223 | * Intermediate Javascript patterns that rely on functions as first class objects, 224 | such as the "interception pattern". 225 | * Use of the interception pattern to provide a "missing hook" in the Sails 226 | controller design. This allowed us to create our own wrapper for any 227 | controller. 228 | * Howto use the IntelliJ IDEs (Ultimate or WebStorm) to do basic interactive 229 | debugging both for browser interception and unit tests. 230 | * Clean and well thought-out conventions for controller code, and for our own OOP 231 | libraries and Sails enhancements. 232 | 233 | To Start: `git checkout -b new_branch_name tutorial.02.5` and then follow [TUTORIAL03.md](https://github.com/grokible/sails-tutorial-1/blob/master/TUTORIALS/TUTORIAL03.md). 234 | To Get Finished Tutorial State: `git checkout -b new_branch_name tutorial.03.2` 235 | 236 | **References:** 237 | 238 | * [Wikipedia ECMAScript Version History](http://en.wikipedia.org/wiki/JavaScript#Version_history) 239 | * [Mozilla JavaScript Implementation History](https://developer.mozilla.org/en-US/docs/Web/JavaScript/New_in_JavaScript) 240 | * [Kangax ECMAScript 5 Implementation Support/Availability](http://kangax.github.io/compat-table/es5/) 241 | * [Mozilla Developer JavaScript Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 242 | * [ECMASCript 5.1 specification, ECMA-262/5.1](http://www.ecma-international.org/ecma-262/5.1/) 243 | 244 | ## Bon Voyage 245 | 246 | Hope you enjoy the tutorials and happy sailing! 247 | 248 | You can start with the first [TUTORIAL01.md](https://github.com/grokible/sails-tutorial-1/blob/master/TUTORIALS/TUTORIAL01.md) 249 | -------------------------------------------------------------------------------- /TUTORIALS/TUTORIAL01.md: -------------------------------------------------------------------------------- 1 | # TUTORIAL01 2 | 3 | ## Why Sails? 4 | 5 | I wanted a "rails-like" fast prototyping framework for backend work, and liked the 6 | idea of using Javascript and Node.js as a learning experience. 7 | 8 | Sails is designed for creating RESTful apis quickly. While one can create websites 9 | using Sails, the focus is clearly not on display, and by not trying to solve this, 10 | Sails can specialize. 11 | 12 | The author compares Sails to other frameworks such as Meteor, by making the distinction 13 | that Sails was created to get client work done, whereas Meteor was created as a framework. 14 | Without reading too much into this, I'm guessing this means Sails tries to be pragmatic 15 | and sparse. My initial thoughts are that Sails seems to have some rough edges, which 16 | don't matter so much (e.g. lack of good error messages for newbies). I personally love 17 | toward frameworks that are super lean, and so between the two candidates, Sails looked 18 | like a better fit for my personal taste. 19 | 20 | It's been my observation that the best frameworks are those that are designed out of 21 | necessity, as they tend to be very practical and sparse. 22 | 23 | The author of Sails, Mike McNeil compares [Sails to Meteor](http://stackoverflow.com/questions/22202286/sails-js-vs-meteor-what-are-the-advantages-of-both). 24 | 25 | I also appreciate the author's desire for a pure Node.js implementation of everything, 26 | in that if one wants to learn reasonable idiomatic Javascript and Node.js through osmosis, 27 | going through the code provides that benefit as well (not to say this isn't true for 28 | Meteor, but we will assume it is true for Sails based on the stackoverflow article). 29 | 30 | I glanced over the information for Meteor, and it is a very different type of 31 | framework that seems to be aimed at rapid application construction using some 32 | of its own paradigms. In particular, it seems to be attempting to model the 33 | declarative, two-binding style of UI/model updating popularized by front-end 34 | frameworks like Angular. It takes this type of interaction and pushes it even 35 | deeper down the stack, and into the persistence layer, and even the messaging 36 | protocols. This is a very different model from most frameworks, which has it's pros 37 | and cons. Sails seems much more tuned to backend services, especially since, 38 | out-of-the-box it has no rendering/template engine. 39 | 40 | ## Tutorial Overview 41 | 42 | The first thing we are going to try is to make a simple User account class and handle 43 | all of the real things we'd need to handle to use this for login and authentication. 44 | It's a pretty simple example, but one that should introduce us to many of the basic 45 | concepts. Once that's done, we'll take a look around to see what to achieve next. 46 | 47 | The tutorials are arranged in bite-sized chunks, and each TUTORIALXX.md file 48 | corresponds to the sequence of tutorials. I'll check-in the finished solution to 49 | what we try to achieve in each tutorial. These will be tagged or otherwise 50 | available by looking at the checkin history and pulling the correct version. 51 | 52 | 53 | ## Installing Sails 54 | 55 | I installed Sails globally as it seems like it is evolving quickly, and the notion of 56 | having different versions of Sails installed, seems like it will just cause problems at 57 | this early stage (also having the bin files, such as generate, go to the right place is nice). 58 | This is also what the docs recommend. 59 | 60 | ```ShellSession 61 | $ npm install -g sails 62 | ``` 63 | 64 | Once that was done I created the scaffolding for tutorial: sails-tutorial-1 65 | 66 | ```ShellSession 67 | $ sails new sails-tutorial-1 68 | $ ls sails-tutorial-1 69 | Gruntfile.js api/ assets/ node_modules/ tasks/ 70 | README.md app.js config/ package.json views/ 71 | ``` 72 | 73 | ## Starting sails for the first time 74 | 75 | Do this from within the top-level project directory: 76 | 77 | ```ShellSession 78 | $ sails lift 79 | ``` 80 | 81 | You should see this: 82 | 83 | ```ShellSession 84 | 85 | info: Starting app... 86 | 87 | info: 88 | info: 89 | info: Sails <| 90 | info: v0.10.5 |\ 91 | info: /|.\ 92 | info: / || \ 93 | info: ,' |' \ 94 | info: .-'.-==|/_--' 95 | info: `--'-------' 96 | info: __---___--___---___--___---___--___ 97 | info: ____---___--___---___--___---___--___-__ 98 | info: 99 | info: Server lifted in `/Users/rogerbush8/Documents/workspace/node/learning/sails/sails-tutorial-1` 100 | info: To see your app, visit http://localhost:1337 101 | info: To shut down Sails, press + C at any time. 102 | 103 | debug: -------------------------------------------------------- 104 | debug: :: Thu Dec 11 2014 16:25:58 GMT-0800 (PST) 105 | 106 | debug: Environment : development 107 | debug: Port : 1337 108 | debug: -------------------------------------------------------- 109 | 110 | ``` 111 | 112 | When you start Sails, this is what it should look like if everything is configured properly. 113 | Since starting Sails does a bunch of preparatory work, there are a lot of configuration errors 114 | that will be caught at the start of the application. In these cases we generally get a 115 | stack trace with the thrown exception. 116 | 117 | Now point our web browser to: 118 | 119 | ```ShellSession 120 | http:/localhost:1337/ 121 | ``` 122 | 123 | You should see a homepage with basic Sails information. So at this point, even though it doesn't 124 | do anything useful, we have a web server. 125 | 126 | 127 | ## Creating the first model and controller 128 | 129 | Sails comes with some rudimentary generator plugins, which can be executed from the CLI. 130 | We'll create a user model and controller to implement the CRUD REST methods. 131 | 132 | ```ShellSession 133 | # We must be in the top-level-directory for the project to execute generator commands 134 | # N.B. Bad things happen if you're in a subdir 135 | 136 | $ cd sails-tutorial-1 137 | 138 | $ sails generate model user firstName:string lastName:string 139 | $ sails generate controller user 140 | 141 | # We just created two files in the api subdirs 142 | 143 | $ ls api/controllers api/models 144 | api/controllers: 145 | UserController.js 146 | 147 | api/models: 148 | User.js 149 | ``` 150 | 151 | Let's take a look at the User.js file. 152 | 153 | ```Javascript 154 | /** 155 | * User.js 156 | * 157 | * @description :: TODO: You might write a short summary of how this model works and what it represents here. 158 | * @docs :: http://sailsjs.org/#!documentation/models 159 | */ 160 | 161 | module.exports = { 162 | 163 | attributes: { 164 | 165 | firstName : { type: 'string' }, 166 | 167 | lastName : { type: 'string' } 168 | 169 | } 170 | } 171 | ``` 172 | 173 | And now the UserController.js file. 174 | 175 | ```Javascript 176 | /** 177 | * UserController 178 | * 179 | * @description :: Server-side logic for managing users 180 | * @help :: See http://links.sailsjs.org/docs/controllers 181 | */ 182 | 183 | module.exports = { 184 | 185 | } 186 | ``` 187 | 188 | ## Let's start the application again 189 | 190 | ```ShellSession 191 | $ sails lift 192 | 193 | info: Starting app... 194 | 195 | ----------------------------------------------------------------- 196 | 197 | Excuse my interruption, but it looks like this app 198 | does not have a project-wide "migrate" setting configured yet. 199 | (perhaps this is the first time you're lifting it with models?) 200 | 201 | In short, this setting controls whether/how Sails will attempt to automatically 202 | rebuild the tables/collections/sets/etc. in your database schema. 203 | You can read more about the "migrate" setting here: 204 | http://sailsjs.org/#/documentation/concepts/ORM/model-settings.html?q=migrate 205 | 206 | In a production environment (NODE_ENV==="production") Sails always uses 207 | migrate:"safe" to protect inadvertent deletion of your data. 208 | However during development, you have a few other options for convenience: 209 | 210 | 1. safe - never auto-migrate my database(s). I will do it myself (by hand) 211 | 2. alter - auto-migrate, but attempt to keep my existing data (experimental) 212 | 3. drop - wipe/drop ALL my data and rebuild models every time I lift Sails 213 | 214 | What would you like Sails to do? 215 | 216 | info: To skip this prompt in the future, set `sails.config.models.migrate`. 217 | info: (conventionally, this is done in `config/models.js`) 218 | 219 | warn: ** DO NOT CHOOSE "2" or "3" IF YOU ARE WORKING WITH PRODUCTION DATA ** 220 | 221 | prompt: ?: 222 | ``` 223 | 224 | OK, so we are to our first important decision. For now, since we are prototyping/learning 225 | we are going to set the migration type to "alter". What alter does is it automatically 226 | adjusts your schema to conform to your code. This can be extremely dangerous, particularly 227 | if it somehow gets out into production. The reason for this is alter will drop any 228 | columns that are not represented in the model. So if you comment out a model attribute 229 | and then do "sails lift", Sails will drop the column and data. 230 | 231 | Note that alter is experimental. It works really well for prototyping, however, in 232 | situations where we are being very careful about data, it's not the right choice. 233 | If alter were changed to not drop columns, things might be different. That is, 234 | if the behavior of alter could be channged so that it didn't delete columns, but 235 | just left them, it would make it less dangerous for full time use. 236 | 237 | For now, for the purposes of prototyping, we are going to set "migrate: alter". 238 | 239 | Rather than enter a number at the prompt, let's learn how to set a config setting. 240 | 241 | So hit CTRL-C to stop Sails. 242 | 243 | ## Setting our first config setting 244 | 245 | Note that the warning says "set sails.config.models.migrate". The first component in 246 | this dotted property is "sails" which is the top level global. Next is "config" 247 | which corresponds to the config directory, then models which is the file models.js 248 | and migrate which is the migrate property in that file. 249 | 250 | We can see that the file has a commented out line. Just leave this line and add 251 | a line below it (the commented lines act as documentation for the config). Note 252 | that generally the commented lines specify the default, but in this case the 253 | default is an unset value (which Sails asks us for if there are any models): 254 | 255 | ```Javascript 256 | // migrate: alter 257 | migrate: alter 258 | ``` 259 | 260 | Now when we start Sails all should be well. Always make sure you are in the top-level 261 | application directory when you attempt to start Sails. 262 | 263 | ``` 264 | $ sails lift 265 | ``` 266 | 267 | You should get the happy sailboat ASCII art signifying things are working. Let's now 268 | see what functionality we've added by creating our simple User model, default controller, 269 | and change of config. 270 | 271 | ## First attempt using automatic routes 272 | 273 | Recall the generated files we just saw were very sparse, but already, due to built-in 274 | default behavior that Sails provides, 275 | we have a set of RESTful and shortcut routes that can be used for simple CRUD on the new 276 | User model we've created. Let's give it a try. 277 | 278 | First we need to start the sails server: 279 | ```ShellSession 280 | $ sails lift 281 | ``` 282 | 283 | You should have seen a nice bit of ASCII art with a little sailboat and this on the end: 284 | 285 | ```ShellSession 286 | debug: Environment : development 287 | debug: Port : 1337 288 | debug: -------------------------------------------------------- 289 | ``` 290 | 291 | Now we'll use a shortcut route to create an User entry. Shortcut routes are not RESTful, 292 | per se, as they are designed to be used in the browser address bar in lieu of a more full 293 | featured tool like Postman. I find the shortcut routes helpful and pragmatic, but they 294 | may be slightly controversial with REST purists. They are very helpful for tutorials, 295 | since they occupy a single line and can be readily cut and pasted. So we'll use them 296 | for these tutorials. 297 | 298 | ```ShellSession 299 | # Type this URL into your browser address bar 300 | http://localhost:1337/user/create?firstName=Roger&lastName=Bush 301 | 302 | # Response I got: 303 | { 304 | firstName: "Roger", 305 | lastName: "Bush", 306 | createdAt: "2014-12-10T15:10:17.370Z", 307 | updatedAt: "2014-12-10T15:10:17.370Z", 308 | id: 1 309 | } 310 | 311 | # Now let's try to retrieve this 312 | http://localhost:1337/user/1 313 | 314 | # Cool! 315 | { 316 | firstName: "Roger", 317 | lastName: "Bush", 318 | createdAt: "2014-12-10T15:10:17.370Z", 319 | updatedAt: "2014-12-10T15:10:17.370Z", 320 | id: 1 321 | } 322 | ``` 323 | 324 | Note that any additional parameters we supply to the shortcut routes are silently 325 | ignored. 326 | 327 | ## Sensible defaults and automatic routes 328 | 329 | Based on the minimal amount of work we've done so far, we've yielded some quick results. 330 | Let's make a few observations: 331 | 332 | * Although we didn't write any code, or even specify any REST methods, we have a set of them 333 | that have been generated. These were not generated as code (e.g. in the UserController) but are 334 | part of Sails framework default behavior. 335 | * Our object was persisted, in spite of the fact that we didn't set up a database or other persistent store. 336 | * Several fields were added to our object automatically, and they have built-in behavior (id, createdAt, updatedAt). 337 | 338 | We will definitely want to make some changes to the system since: 339 | 340 | * We would like to specify a particular datastore that is fitting for our project. 341 | * We don't necessarily want to expose all these default routes. 342 | * The create method has no field validation. 343 | * There is no authentication mechanism, so all these routes are fully exposed. 344 | 345 | Sails gives us a nice, simple, incremental path to doing each of these. It's pretty good about letting 346 | you get started quickly, and then surgically amending the default behavior as you go. Being able to work 347 | in this way is crucial for rapid-prototyping, as well as being able to incrementally amend the code to 348 | a full working system. 349 | 350 | ## sails-disk the default datastore and changing the datastore 351 | 352 | Sails out-of-the-box has sails-disk as the default datastore. The entire database schema and data 353 | is persisted to a local JSON file representation, in .tmp/localDiskDb.db. This is extremely useful 354 | for demo and test, as well as a nice out-of-the-box experience, but probably not for production use. 355 | 356 | Sails takes a "lazy creation" approach to datastores, tables, files, etc. which is extremely 357 | useful in prototyping. If we delete this file, it will simply get recreated. You'll note that if 358 | you delete the .tmp/localDiskDb.db file in this case, it will regenerate it on the next attempt 359 | to update something (so the data is cached in memory). 360 | 361 | Sails is pretty simple, and so I've found that deleting files doesn't get you into trouble. This 362 | isn't to say that you won't get into trouble with the occasional dangling reference here or there, 363 | but it seems fairly simple and robust. You should generally shutdown Sails and restart it if 364 | you delete a file. 365 | 366 | Sails will typically complain and exit with a stack trace for any number of configuration issues, 367 | which is nice in that you can typically find out what it's mad about. This does typically require 368 | you to look through some code. But this is far better than the system not detecting something 369 | is wrong and putting you on the "muddle through" path. 370 | 371 | 372 | ## Changing the default datastore 373 | 374 | Changing the datastore requires the following: 375 | 376 | * Adding a connection for our specific datastore to config/connections.js 377 | * Specifying a connection for each model, either in the model file (e.g. api/models/User.js), or 378 | changing the default in config/models.js 379 | 380 | I'll be using MySQL for the remaining tutorials. Note that the waterline ORM does a great job 381 | at abstracting away most of the details so that datastores are interchangeable. However, there 382 | will always be important differences. So be aware of this if you have a different datastore 383 | while following the tutorial. 384 | 385 | Here's what I added at the end of config/connections.js: 386 | 387 | ```Javascript 388 | 389 | // insert as last entry of config/connection.js: 390 | 391 | mysql1: { 392 | adapter: 'sails-mysql', 393 | host: '127.0.0.1', 394 | user: 'webapp', 395 | password: 'pa55word', 396 | database: 'sails' 397 | } 398 | ``` 399 | 400 | Next, we'll change the default connection from localDiskDb to mysql1 in config/models.js. 401 | While we can do this on a per-table basis, it makes more sense to set the default once and 402 | override it in the unlikely case it is different. 403 | 404 | ```Javascript 405 | 406 | // change at top of config/models.js 407 | 408 | // connection: 'localDiskDb', 409 | 410 | connection: 'mysql1', 411 | ``` 412 | 413 | Note the convention of a comment block followed by a comment-disabled property 414 | along with it's default value. From the existing comment we can see that 415 | the default model connection is 'localDiskDb'. If you do replace a value, make sure to leave 416 | the comment-disabled property. Don't delete it as this internal self-documentation 417 | is important for understanding the system. 418 | 419 | Next, we will need to install the sails-mysql adapter (I chose to do this locally in 420 | my project directory, but globally should be fine too). 421 | 422 | ```ShellSession 423 | # Make sure your working directory is sails-tutorial-1 424 | $ npm install sails-mysql 425 | sails-mysql@0.10.8 node_modules/sails-mysql 426 | ├── waterline-errors@0.10.1 427 | ├── waterline-cursor@0.0.5 428 | ├── waterline-sequel@0.0.19 429 | ├── async@0.9.0 430 | ├── lodash@2.4.1 431 | └── mysql@2.3.2 (require-all@0.0.8, bignumber.js@1.4.0, readable-stream@1.1.13) 432 | ``` 433 | 434 | Finally, we need to startup mysql, create a new database, create a user, and grant 435 | database permissions. Note that we do not have to create the User table, as sails 436 | will do that for us. 437 | 438 | ```ShellSession 439 | # If you don't have mysql running, you can start it this way 440 | $ sudo /usr/local/mysql/bin/mysqld_safe & 441 | 442 | $ mysql -u root -p 443 | 444 | Enter password: 445 | Welcome to the MySQL monitor. Commands end with ; or \g. 446 | Your MySQL connection id is 24 447 | Server version: 5.5.28 MySQL Community Server (GPL) 448 | ... 449 | 450 | mysql> CREATE DATABASE sails; 451 | mysql> CREATE USER sails IDENTIFIED BY PASSWORD('pa55word'); 452 | mysql> GRANT USAGE ON sails.* TO 'webapp'@'localhost'; 453 | mysql> FLUSH PRIVILEGES; 454 | ``` 455 | 456 | Now lets restart sails. You can stop it by doing CTRL-C. 457 | 458 | ```ShellSession 459 | $ sails lift 460 | ``` 461 | 462 | Since we have selected "migrate: alter," Sails will attempt to do an automatic 463 | data migration on startup. It does this by examining our User table schema, and comparing 464 | it to the User model (in models/User.js), and then making changes which more or less 465 | preserves our existing data. 466 | 467 | We'll see that it is not as conservative with our data as we might like in certain important cases. 468 | 469 | Note that if we've done anything wrong, Sails will complain here by dumping a stack 470 | trace to the console and exiting. It typically takes a little detective work to figure out what is 471 | wrong (but at this stage it's likely to be something simple, so just start by double 472 | checking everything). 473 | 474 | Let's check the database. Note that if we show tables we can see that user has 475 | been created. Let's also see what the created schema looks like. 476 | 477 | ```ShellSession 478 | mysql> SHOW TABLES; 479 | +-----------------+ 480 | | Tables_in_sails | 481 | +-----------------+ 482 | | user | 483 | +-----------------+ 484 | 1 row in set (0.00 sec) 485 | 486 | SHOW CREATE TABLE user; 487 | ... 488 | | user | CREATE TABLE `user` ( 489 | `firstName` varchar(255) DEFAULT NULL, 490 | `lastName` varchar(255) DEFAULT NULL, 491 | `id` int(10) NOT NULL AUTO_INCREMENT, 492 | `createdAt` datetime DEFAULT NULL, 493 | `updatedAt` datetime DEFAULT NULL, 494 | PRIMARY KEY (`id`) 495 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8 | 496 | ... 497 | 0 row in set (0.00 sec) 498 | ``` 499 | 500 | Now let's add a record using a "shortcut route," one of the automatic types of routes that 501 | Sails creates for us automatically (when we have a model and a controller). 502 | 503 | 504 | ```ShellSession 505 | # Type this URL into your browser address bar 506 | http://localhost:1337/user/create?firstName=Roger&lastName=Bush 507 | 508 | # Response I got: 509 | { 510 | firstName: "Roger", 511 | lastName: "Bush", 512 | createdAt: "2014-12-10T17:11:19.000Z", 513 | updatedAt: "2014-12-10T17:11:19.000Z", 514 | id: 1 515 | } 516 | 517 | # Now let's try to retrieve this 518 | http://localhost:1337/user/1 519 | 520 | # Seems to work 521 | { 522 | firstName: "Roger", 523 | lastName: "Bush", 524 | createdAt: "2014-12-10T17:11:19.000Z", 525 | updatedAt: "2014-12-10T17:11:19.000Z", 526 | id: 1 527 | } 528 | ``` 529 | 530 | Let's check the database: 531 | 532 | ```ShellSession 533 | mysql> SELECT * FROM user; 534 | +-----------+----------+----+---------------------+---------------------+ 535 | | firstName | lastName | id | createdAt | updatedAt | 536 | +-----------+----------+----+---------------------+---------------------+ 537 | | Roger | Bush | 1 | 2014-12-10 09:11:19 | 2014-12-10 09:11:19 | 538 | +-----------+----------+----+---------------------+---------------------+ 539 | 1 row in set (0.00 sec) 540 | ``` 541 | 542 | ## Automatic Routes 543 | 544 | Despite the small amount of work we've done, we already have a reasonable set of REST calls 545 | which are defined on the User model. We have both RESTful routes (GET, POST, DELETE) and 546 | shortcut routes. Shortcut routes are utility routes where GET is the only verb, and what 547 | might go into a POST body is concatenated into the query parameters. This is great for 548 | one-liners from the command-line or in the browser address bar. 549 | 550 | Automatic routes are enabled by default, and the corresponding implementation provided. 551 | The implementation may be selectively overridden by adding a corresponding method in 552 | the controller. Automatic routes may be turned off for the application using configuration in 553 | config/blueprint.js. More information on blueprints can be found in the 554 | [sails blueprint api reference](http://sailsjs.org/#/documentation/reference/blueprint-api). 555 | 556 | 557 | ## Adding a field 558 | 559 | Adding a field is a common operation, and is fairly simple. Note that we would like to 560 | add an email field to the User. Since adding fields is the most common change we will 561 | make to a db schema, we should take a little time to understand how it works. 562 | 563 | To add a field, we will add a property directly in models/User.js: 564 | 565 | ```Javascript 566 | module.exports = { 567 | 568 | attributes: { 569 | 570 | firstName : { type: 'string' }, 571 | 572 | lastName : { type: 'string' }, 573 | 574 | // Add this line, and the comma ^ up here 575 | email : { type: 'string' } 576 | } 577 | } 578 | 579 | ``` 580 | 581 | Now restarting sails should add the column. 582 | 583 | ```ScriptSession 584 | $ sails lift 585 | ``` 586 | 587 | Checking the database shows the column is added, and the data is preserved. 588 | 589 | ```ShellSession 590 | mysql> SELECT * FROM user; 591 | +-----------+----------+-------+--------------------------+---------------------+ 592 | | firstName | lastName | email | id | createdAt | updatedAt | 593 | +-----------+----------+-------+--------------------------+---------------------+ 594 | | Roger | Bush | NULL | 1 | 2014-12-10 09:11:19 | 2014-12-10 09:11:19 | 595 | +-----------+----------+-------+--------------------------+---------------------+ 596 | 1 row in set (0.00 sec) 597 | ``` 598 | 599 | Note that adding a field works well because all of the fields are NULL-able by 600 | default, when created by the sails-mysql adapter. Thus there is no need to specify 601 | a default in the database. In most cases, no default can be given anyway, as the 602 | opportunity to gather the data has passed, and thus NULL is the most reasonable 603 | value for the data. Also note that updatedAt does not change. 604 | 605 | 606 | ## Deleting fields 607 | 608 | Let's try removing the email field from models/User.js, and running sails lift to see what happens. 609 | 610 | ```Javascript 611 | module.exports = { 612 | 613 | attributes: { 614 | 615 | firstName : { type: 'string' }, 616 | 617 | lastName : { type: 'string' } 618 | 619 | } 620 | } 621 | 622 | ``` 623 | 624 | Let's restart sails: 625 | 626 | ```ScriptSession 627 | $ sails lift 628 | ``` 629 | 630 | And examine the db: 631 | 632 | ```ScriptSession 633 | mysql> select * from user; 634 | +-----------+----------+----+---------------------+---------------------+ 635 | | firstName | lastName | id | createdAt | updatedAt | 636 | +-----------+----------+----+---------------------+---------------------+ 637 | | Roger | Bush | 1 | 2014-12-11 18:20:48 | 2014-12-11 18:20:48 | 638 | +-----------+----------+----+---------------------+---------------------+ 639 | 1 row in set (0.00 sec) 640 | ``` 641 | 642 | Note that the email column has been dropped, along with any data it had. 643 | Fortunately we had no data. From a prototyping perspective this makes sense 644 | as it is quick and easy. 645 | 646 | However, once we start adding other developers, and the possibility of a production 647 | environment, we start running into trouble. The reason for this is that data, starts to 648 | matter a lot, once you have any that you care about. 649 | 650 | Consider the following scenario: 651 | 652 | * Developer comments out an attribute in the model file temporarily and checks in the file. 653 | * Other developers do a git pull, and sails lift. 654 | * Sadness ensues as the other developers realize their test data is deleted. 655 | 656 | It would be even worse if somehow code got into production that silently deleted things. 657 | 658 | This is why, typically, it is better to mutate your schema using a mechanism like 659 | migrations. For a little bit more trouble, we can easily change our schema, as well 660 | as retain control over when those changes are done (and perhaps even be able to roll 661 | them back). Changing schema in this way is fairly standard and takes the guesswork 662 | out of what is happening automatically. 663 | 664 | We'll look at migrations, which are not a standard part of Sails, in a future installment. 665 | 666 | For now, just realize that a mechanism like migrations is probably the way to go right 667 | from the get-go so that you don't get lazy and accidentally have "migrate: alter" 668 | wreaking havoc. It's slightly more complex, but we start out with the mindset of 669 | deploying to production. 670 | 671 | Secondly, changing/deleting fields happens automatically, with "migrate: alter", and it 672 | is a little unconservative in its current approach (although we've been warned, the 673 | authors did mark it experimental). 674 | 675 | ## Get it back 676 | 677 | Go ahead and go through the steps to add the email column back in. Verify that 678 | the column gets added once again after running sails lift. 679 | 680 | ## Update 681 | 682 | Now that we have an email field, let's use another convenience route to surgically 683 | update the email field. 684 | 685 | Type the following URL into your browser: 686 | 687 | ```ShellSession 688 | # Type this URL into your browser address bar 689 | http://localhost:1337/user/update/1?email=rogerbush8@yahoo.com 690 | 691 | # Here is the result I got. 692 | { 693 | "firstName": "Roger", 694 | "lastName": "Bush", 695 | "email": "rogerbush8@yahoo.com", 696 | "id": 1, 697 | "createdAt": "2014-12-12T02:20:48.000Z", 698 | "updatedAt": "2014-12-10T15:10:17.370Z", 699 | } 700 | ``` 701 | 702 | Note that the field was added, and the updatedAt timestamp was also changed. 703 | 704 | 705 | ## Attributes and simple data constraints 706 | 707 | Note that we still haven't written what could be considered a single line of code. The Javascript we 708 | are writing is just property settings that amount to overriding configuration. So we are getting a tremendous amount of power 709 | without much effort through Sails adherence to the familiar principle of 710 | [convention over configuration](http://en.wikipedia.org/wiki/Convention_over_configuration). 711 | 712 | We would now like to evolve our system incrementally, by first adding some constraints. We can see 713 | from the MySQL user table created by the sails-mysql adapter what the conventions are, namely: 714 | 715 | * Strings are varchar(255) and default to NULL. 716 | * Added fields createdAt and updatedAt are datetime and default to NULL. 717 | * id is a autoincrement int (typical MySQL key - it is the PRIMARY key). 718 | * There are no other indexes by default. 719 | 720 | For basic validation we will want to make sure that firstName and lastName are valid names, that 721 | email is a valid email, and further that email is unique. 722 | 723 | The attributes of a model are described in the [Sails ORM Attributes](http://sailsjs.org/#/documentation/concepts/ORM/Attributes.html). 724 | Attributes for the MySQL datastore, map to columns in the table schema. 725 | We can use the 'unique' property of the attribute to handle uniqueness of the email. 726 | 727 | Note in the current online documentation, there is an example that shows type=email, which 728 | does not seem like a fundamental type. For now, we are going to avoid using this, and instead use a validation 729 | for email. Validations are data rules that can be applied to incoming values to make sure we don't 730 | persist garbage in our datastore. 731 | 732 | Here are some of the more important attribute properties: 733 | 734 | * required: - field must be specified. This means it can't be null. 735 | * unique: - field must be unique in the table. Unique is a bit of an oddball as far as these properties are concerned. 736 | Since it is a global feature of the model, it's nontrivial to guarantee. In a relational database, like MySQL, this will just be a 737 | uniqueness constraint on the column. In a NoSQL database, this might just be ignored. So this useful feature probably 738 | breaks the abstraction slightly, making our model SQL dependent. 739 | * defaultsTo: - default value 740 | * type: - data type for the column (string, text, integer, float, date, datetime, boolean, binary, array, json) 741 | 742 | It would be a useful exercise to create each of these fields and see what the sails-mysql adapter generates in the 743 | mysql schema, particularly for boolean, binary, array and json. 744 | 745 | ## Validations 746 | 747 | Sails uses validations from the [Validator library](https://github.com/chriso/validator.js). Looking at the current list 748 | of supported [Sails validations](http://sailsjs.org/#/documentation/concepts/ORM/Validations.html), we see one 749 | for email. 750 | 751 | Note that we can also create our own validation types, by adding a types section to our model, with 752 | truth functions. However, I suspect there is a better project-wide place to override this, as we 753 | are not going to want to define a type just for one model (but it's useful to know we can do it here for 754 | experimentation). Also, it's unfortunate that these in model file custom validations are called "types" since that name 755 | refers to the underlying data type for the attribute. 756 | 757 | Note that validations just prior to persisting in the database are of limited value. We really need to do these 758 | validations on the incoming parameters. We'll see how to do REST parameter validation in TUTORIAL02. 759 | 760 | 761 | ## Updated User.js 762 | 763 | So now we are ready to update our api/models/User.js file. The finished product should be this: 764 | 765 | ```Javascript 766 | /** 767 | * User.js 768 | * 769 | * @description :: User account information 770 | * @docs :: http://sailsjs.org/#!documentation/models 771 | */ 772 | 773 | module.exports = { 774 | 775 | attributes: { 776 | 777 | firstName : { type: 'string', required: true }, 778 | 779 | lastName : { type: 'string', required: true }, 780 | 781 | email : { type: 'string', required: true, unique: true, email: true } 782 | 783 | } 784 | } 785 | ``` 786 | 787 | And we should restart sails and test it. 788 | 789 | ```ShellSession 790 | $ sails lift 791 | ``` 792 | 793 | Note that in general, no changes will be made to the schema to support validations. This is 794 | because they are done, mostly, in code in Sails. Note, in particular, that the table schema 795 | has not changed due to the required fields (they are still NULL-able): 796 | 797 | ```ShellSession 798 | mysql> SHOW CREATE TABLE user; 799 | ... 800 | | user | CREATE TABLE `user` ( 801 | `firstName` varchar(255) DEFAULT NULL, 802 | `lastName` varchar(255) DEFAULT NULL, 803 | `email` varchar(255) DEFAULT NULL, 804 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 805 | `createdAt` datetime DEFAULT NULL, 806 | `updatedAt` datetime DEFAULT NULL, 807 | PRIMARY KEY (`id`), 808 | UNIQUE KEY `email` (`email`) 809 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 | 810 | ``` 811 | 812 | Most validations can be done using Sails code (and are done via Validator). The one exception 813 | to this is the unique property, which is a global property over the table. Unique is achieved 814 | in the sails-mysql adapter by adding a uniqueness constraint to the table schema. In a NoSQL 815 | database this might simply be ignored (YMMV). 816 | 817 | We can see it added here: 818 | 819 | ```ShellSession 820 | mysql> SHOW INDEXES FROM user; 821 | ... 822 | | user | 0 | PRIMARY | 1 | id | A | 1 | NULL | NULL | | BTREE | | | 823 | | user | 0 | email | 1 | email | A | 1 | NULL | NULL | YES | BTREE | | | 824 | ... 825 | 2 rows in set (0.00 sec) 826 | ``` 827 | 828 | 829 | Now let's try to cause some mischief by adding a duplicate email, missing fields, etc: 830 | 831 | ```ShellSession 832 | # Let us try duplicate email 833 | # Type URL into your browser address bar 834 | http://localhost:1337/user/create?firstName=Roger&lastName=Bush&email=rogerbush8@yahoo.com 835 | 836 | # Response I get 837 | { 838 | error: "E_VALIDATION", 839 | status: 400, 840 | summary: "1 attribute is invalid", 841 | invalidAttributes: { 842 | email: [ 843 | { 844 | value: "rogerbush8@yahoo.com", 845 | rule: "unique", 846 | message: "A record with that `email` already exists (`rogerbush8@yahoo.com`)." 847 | } 848 | ] 849 | } 850 | } 851 | 852 | # Type URL into your browser address bar 853 | http://localhost:1337/user/create?firstName=Sam&email=sam@yahoo.com 854 | 855 | # Response I get 856 | { 857 | error: "E_VALIDATION", 858 | status: 400, 859 | summary: "1 attribute is invalid", 860 | model: "User", 861 | invalidAttributes: { 862 | lastName: [ 863 | { 864 | rule: "string", 865 | message: "`undefined` should be a string (instead of "null", which is a object)" 866 | }, 867 | { 868 | rule: "required", 869 | message: ""required" validation rule failed for input: null" 870 | } 871 | ] 872 | } 873 | } 874 | ``` 875 | 876 | Cool, seems to work fine. Note that the error message in the second case is slightly misleading as it 877 | looks like we've failed two validation rules instead of one. The type 'string', if specified without 878 | 'required: true', will allow a create call without specifying the parameter, and save with a NULL 879 | value (as expected). So the rule 'string' above is independent of the type 'string' (i.e. type = 880 | string does not have a rule = string). 881 | 882 | I find the multiple validation rules returned to not be helpful as this requires the 883 | client to have smarts put into it in terms of error handling and display. I find it better to 884 | return the most salient single error, in order of importance. If the order of validation rules 885 | can be specified, this lets the server logic determine and return the most fitting error for a 886 | single field. Of course, there should be up to one error per submitted field (useful for UI 887 | decoration to show errors in a post form), but having multiple errors per field is 888 | unnecessarily complicated. 889 | 890 | Note I am just nitpicking though. Sails did a fantastic job overall as it made this 891 | extremely simple. Note that it did not add a "NOT NULL" constraint to the database, prefering 892 | to handle this sort of thing in the application - the right thing to do, IMHO. 893 | 894 | 895 | ## Convention over configuration 896 | 897 | We get a lot of useful default behavior with minimal effort, which is par for the 898 | course with frameworks such as Sails.js. Learning the system requires understanding 899 | what the default behaviors are, when to override them, and then where in the system 900 | we must go to make our overriding change. 901 | 902 | We have seen hints of this in the config system. The first hint was to effectively 903 | make this change: 904 | 905 | sails.config.models.migrate: alter 906 | 907 | We saw that the values for this came from config/models.js and the attribute 908 | "migrate". This happens when Sails starts up. Once Sails is started, this config 909 | object is initialized with the appropriate values, and may be read and used 910 | for our own purposes. In the case of models.js (and many other files), a particular 911 | model file in the api/models directory, can override these configuration values. 912 | 913 | 914 | ## Conclusion 915 | 916 | We've seen that Sails provides a lot of power out-of-the-box, without too much 917 | typing. In fact, in this lesson, we didn't really write any code (true, we 918 | made changes to .js files, but these were acting as config files). In the 919 | next tutorial, we'll add specific code to our controller and show how to 920 | incrementally evolve and override behavior to get exactly what we want. 921 | 922 | Here are some of the things we've learned: 923 | 924 | * Installing Sails.js and optional libraries using npm. 925 | * Starting and stopping Sails.js and the bootstrap process. 926 | * Modifying Sails configuration, and an intro to the global config object 927 | which represents the bootstrapped Sails system. Important configuration 928 | such as connectors (mysql) and migration type. 929 | * Using Sails model and controller generator scripts to create modules with stub functions. 930 | * Using blueprint convenience routes. 931 | * Using the sails-disk and sails-mysql persistent stores. 932 | * Defining Sails model schema and the mapping to MySQL database column attributes. Interacting 933 | with the MySQL database, and debugging. Dealing with schema changes. 934 | 935 | ## References 936 | 937 | * [Sails to Meteor Comparison](http://stackoverflow.com/questions/22202286/sails-js-vs-meteor-what-are-the-advantages-of-both). 938 | * [Sails docs on Models](http://sailsjs.org/#!documentation/models) 939 | * [Sails docs on Controllers](http://links.sailsjs.org/docs/controllers) 940 | * [Sails docs on ORM Attributes of Models](http://sailsjs.org/#/documentation/concepts/ORM/Attributes.html). 941 | * [Note on migration settings](http://sailsjs.org/#/documentation/concepts/ORM/model-settings.html?q=migrate) 942 | * [Sails blueprint api reference](http://sailsjs.org/#/documentation/reference/blueprint-api) 943 | * [Convention over Configuration, Wikipedia](http://en.wikipedia.org/wiki/Convention_over_configuration) 944 | * [Validator library](https://github.com/chriso/validator.js) 945 | * [Sails validations, not recommended](http://sailsjs.org/#/documentation/concepts/ORM/Validations.html) 946 | 947 | --------------------------------------------------------------------------------