├── 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 | *
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 |
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.
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 |
--------------------------------------------------------------------------------