Skip to content
libre edited this page Jan 16, 2017 · 16 revisions

Adapter

Quick Overview

Adapters expose interfaces, which imply a contract to implement certain functionality. This allows us to guarantee conventional usage patterns across multiple models, developers, apps, and even companies, making app code more maintainable, efficient, and reliable. Adapters are useful for integrating with databases, open APIs, internal/proprietary web services, or even hardware.

At the current moment, Sails has a lot of community adapters so there are no reasons to change or add something.

But, you still can scaffold adapter if you want to write your own.

Running

yo sails-rest-api:adapter Amazing # Scaffolds AmazingAdapter in api/adapters folder
yo sails-rest-api:adapter --help # Prints usage info

Blueprints

Quick Overview

Sails has an amazing feature which is called blueprints.

When you run application with blueprints enabled, the framework inspects your controllers, models, and configuration in order to bind certain routes automatically. These implicit blueprint routes (sometimes called "shadows") allow your app to respond to certain requests without you having to bind those routes manually in your config/routes.js file. By default, the blueprint routes point to their corresponding blueprint actions, any of which can be overridden with custom code.

But it has a few bugs, so that we decided to override blueprints and fix them.

Running

yo sails-rest-api:blueprint # Scaffolds new blueprints, that we've overridden
yo sails-rest-api:blueprint --no-default # The same as above
yo sails-rest-api:blueprint --default # Change back to default blueprints

Blueprints List

add

POST /:model/:id/:attribute/:childId

Associate one record with the collection attribute of another. If the record being added has a primary key value already, it will just be linked. If it doesn't, a new record will be created, then linked appropriately. In either case, the association is bidirectional.

This blueprint is not overridden, but you can do it yourself in api/blueprints/add.js (if you need).

Example of API call:

POST /tickets/12/comments # Create new comment for specified ticket

create

POST /:model

An API call to create and return a single model instance using the specified parameters.

Blueprint was modified in terms of optimisation. We have a clean REST API so that we don't need make a deal with sockets, etc... That's what exactly was removed from default create blueprint.

Example of API call:

POST /tickets # Create new ticket

destroy

DELETE /:model/:id

Destroys the single model instance with the specified id from the data adapter for the given model if it exists.

Blueprint was modified exactly as create.

Example of API call:

DELETE /tickets/12 # Remove ticket #12 from database

find

GET /:model

An API call to find and return model instances from the data adapter using the specified criteria.

find blueprint contains a lot of fixes.

First of all, it optimises select data from database based on required fields and populates only associations, that you really need to populate. It mixins metadata in the root of a response also, except the total count of records. You can get them via GET /tickets/count.

Examples of API call:

GET /tickets # Find all the tickets in database
GET /tickets?state=open # Get only open tickets
GET /tickets?state=open&limit=10 # Get 10 open tickets
GET /tickets?state=open&limit=10&page=4 # Get 10 tickets starting from page #4
GET /tickets?state=open&limit=10&page=4&sort=updatedAt # Get 10 tickets starting from page #4 and sorted
GET /tickets?fields=id,title # Get all tickets only with `id` and `title` fields
GET /tickets?fields=id,title&populate=comments # Get all tickets only with `id`, `title` and populated `comments` fields

findOne

GET /:model/:id

An API call to find and return a single model instance from the data adapter using the specified id.

All the modified logic from find blueprint applies here too.

Examples of API call:

GET /tickets/12 # Get ticket #12 from database
GET /tickets/12?fields=id,title # Get ticket #12 only with `id` and `title` fields
GET /tickets/12?fields=id,title&populate=comments # Get ticket #12 only with `id`, `title` and populated `comments` fields

populate

GET /:model/:parentId/:relation/:id

Expand response with populated data from relations in models.

This blueprint is not overridden. We are using default blueprint from Sails.

Examples of API call:

GET /tickets/12/comments # Get all comments that assigned to ticket #12
GET /tickets/12/comments/15 # Get comment #15 that assigned to ticket #12

remove

DELETE /:model/:parentId/:collectionAttr/:id

Removes associated record from collection.

This blueprint is not overridden. We are using default blueprint from Sails.

Example of API call:

DELETE /tickets/12/comments/15 # Remove comment #15 that assigned to ticket #12

update

PUT /:model/:id

An API call to update a model instance with the specified id, treating the other unbound parameters as attributes.

Blueprint was modified exactly as create and destroy blueprints.

Example of API call:

PUT /tickets/12 # Update ticket #12 with new information

Configuration

Quick Overview

Sails supports easy maintainable configuration.

Sails apps can be configured programmatically, by specifying environment variables or command-line arguments, by changing the local or global .sailsrc files, or (most commonly) using the boilerplate configuration files conventionally located in the config/ folder of new projects. The authoritative, merged-together configuration used in your app is available at runtime on the sails global as sails.config.

We have a lot of REST APIs built with Sails, so that we know the better configuration than provided by default.

Running

yo sails-rest-api:config # Scaffolds default configuration
yo sails-rest-api:config --help # Prints out usage help

Here is a little example, how you can generate configuration with PostgreSQL database and credentials to it:

yo sails-rest-api:config --database-adapter PostgreSQL --database-host db.project.com --database-name db-name --database-username secure --database-password password

Or, how you can configure DynamoDB support in your project:

yo sails-rest-api:config --database-adapter DynamoDB --dynamo-access-key-id ACCESS_KEY_ID --dynamo-secret-access-key SECRET_ACCESS_KEY --dynamo-region us-west-1

Environment Configuration

Development

Development environment is running with the following configuration:

export default {
  port: 3000,
  log: {
    level: 'silly'
  },
  models: {
    connection: 'disk'
  }
};

disk connection is provided because of easiest starting with project. You don't need to install other global dependencies like MySQL server or Mongo. It will be enough just clone the project and start it.

Production

Production environment is running with the following configuration:

export default {
  port: 80,
  log: {
    level: 'info'
  }
};

We've chosen 80 port because when you start a project, usually, you don't need use nginx or HAProxy. Without them NodeJS is processing requests more faster.

Only if you need load-balancer or proxy-server, you can change this configuration and update your environment.

Test

Test environment is running with the following configuration:

export default {
  log: {
    level: 'silent'
  },
  models: {
    connection: 'memory',
    migrate: 'drop'
  }
};

We're using memory adapter for fixtures and drop them when tests are running. Following from that, we always have clean database for tests.

Configuration Files

blueprints

Blueprints is configured to work great with REST API routes. Here is the list what've done:

  • All controller's actions is automatically binds to GET, POST, PUT and DELETE methods;
  • REST methods are automatically binds to find, findOne, create, update and destroy actions in your controller;
  • Prefix /v1 for all routes;
  • Enabled pluralized form, so that model User binds to route /users;
  • Populate is disabled by default in terms of optimisation (until fields will be explicitly set with request);
  • Disabled autoWatch because we don't need that (stateless API);
  • Default limit of records in the response is 20;

bootstrap

Bootstrap function is not modified.

connections

Connections configuration file was modified with dictionary of all popular adapters. You don't need to remember all the properties for adapters' configuration.

cors

CORS allows to you make requests from client-side JavaScript. By default, it's disabled, but you can enable it if you wish.

yo sails-rest-api:config --cors # Enables cron by default
yo sails-rest-api:config --no-cors # Disables cron by default

errors

We've added this configuration because we faced the issues with respond detailed message to client. This file is contains custom error codes for API that are available via sails.config.errors.USER_NOT_FOUND, for instance.

This configuration file can be expanded with yours error codes and provide consistent error codes to the client.

globals

By default, Sails has enabled all the globals. We've changed that and we have following globals now:

  • lodash and async is disabled (Sails is using old lodash version and async can be replaced with bluebird/Q);
  • sails, services and models is exposed to the global context;

hooks

Hooks configuration is simple. We've disabled unused parts of Sails in .sailsrc file and test environment:

export default {
  hooks: {
    csrf: false,
    grunt: false,
    i18n: false,
    pubsub: false,
    session: false,
    sockets: false,
    views: false
  }
}

http

HTTP configuration file has a lot of changes. Here is the list:

  • Default port is 3000;
  • It has ssl property now, so that you know where you can configure SSL connection;
  • Added serverOptions property, if you want to send specific options to http(s).createServer method;
  • Added customMiddleware function that allows to you include specific middleware;
  • Added bodyParser property where you can override default body parser;
  • Added keepAlive middleware that keep all connections alive;
  • Optimize order of middleware and removed unused;

models

Models is also untouched. migrate property is set to alter.

routes

The same applies to routes configuration. All the routes binds automatically thanks to blueprints.

Controller

Quick Overview

Controllers are the principal objects in your Sails application that are responsible for responding to requests from a web browser, mobile application or any other system capable of communicating with a server. They often act as a middleman between your models and views. For many applications, the controllers will contain the bulk of your project’s business logic.

We didn't change anything here, just added few controllers and simplify their scaffolding.

This generator will only create a Controller, if you need both model and controller, use Model generator instead

Running

yo sails-rest-api:controller Ticket create find # Scaffolds TicketController with 2 actions: `create` and `find`
yo sails-rest-api:controller --help # Get help and all possible options\arguments

List of Controllers

This list contains predefined controllers that we've implemented.

PingController

PingController tests if your Sails application is live.

For scaffolding this controller just call generator with Ping argument:

yo sails-rest-api:controller Ping

Example of API call:

GET /v1/ping

SearchController

Also added SearchController that allows to you make full-text searching within your records.

yo sails-rest-api:controller Search

Example of API call:

GET /v1/search?q=text # Find all models and records where `text` is exists

Cron

Quick Overview

We often need to implement few cron tasks within Sails application. For this purposes I've built sails-hook-cron package that reads the config/cron.js configuration and starts jobs based on configuration.

Running

You can easily add this feature into your existing application just calling sub-generator:

yo sails-rest-api:cron # With empty cron jobs
yo sails-rest-api:cron firstJob secondJob # With two jobs named accordingly

Configuration

When cron's configuration is scaffolded you just need to update schedule and onTick methods.

export default {
  cron: {
    firstJob: {
      schedule: '* * 3 * * *',
      onTick: AnySailsService.doSomething
    }
  }
};

For more documentation you can refer to sails-hook-cron repository.

Hook

Quick Overview

A hook is a Node module that adds functionality to the Sails core. The hook specification defines the requirements a module must meet for Sails to be able to import its code and make the new functionality available. Because they can be saved separately from the core, hooks allow Sails code to be shared between apps and developers without having to modify the framework.

We've implemented some hooks and improved custom hooks scaffolding.

Running

yo sails-rest-api:hook MyHookName # Generates custom hook with MyHookNameHook name
yo sails-rest-api:hook --help # Prints out usage info

Hooks List

We have additional hooks implemented right from the box.

Pluralize

This hook checks, if Model has assigned Controller (with the same name) then we pluralize route to it. Otherwise, it remains the same.

yo sails-rest-api:hook pluralize

Logger

Quick Overview

Sails has a nice logger, but if you want to configure it with support of writing it to files, for instance, you will faced the problems.

This generator helps you to configure logging tools in a Sails application.

Running

yo sails-rest-api:logger

Model

Quick Overview

A model represents a collection of structured data, usually corresponding to a single table or collection in a database. Models are usually defined by creating a file in an app's api/models/ folder.

We do nothing here, just scaffolds the structure for tests and api.
This generator will act like sails generate api which means that it will generate both api/models/Model.js and api/controllers/ModelController.js.

Running

yo sails-rest-api:model Ticket # Scaffold Ticket model with REST interface
yo sails-rest-api:model --no-rest Ticket # Scaffolds Ticket model without REST interface
yo sails-rest-api:model --help # Help in usage

Policy

Quick Overview

Policies in Sails are versatile tools for authorization and access control - they let you allow or deny access to your controllers down to a fine level of granularity. For example, if you were building Dropbox, before letting a user upload a file to a folder, you might check that she isAuthenticated, then ensure that she canWrite (has write permissions on the folder.) Finally, you'd want to check that the folder she's uploading into hasEnoughSpace.

We change nothing here, just scaffolding the structure.

Running

yo sails-rest-api:policy isAdmin # Scaffolds a new policy with isAdmin as a name
yo sails-rest-api:policy --help # Prints out usage info

Response

Quick Overview

We have overridden the default Sails responses so they are fit to REST API needs. These responses are located in api/responses folder so that you can modify them if you need.

Each response responds with the following JSON structure:

{
  "code": "OK",
  "message": "Operation is successfully executed",
  "data": [{}, {}, {}]
}
  • code contains constant that identify status of the response. For instance OK or E_BAD_REQUEST;
  • message contains detailed message what exactly was happened on the server;
  • data contains result of the response, for instance array of records;

List of Responses

We have a few responses already implemented with predefined HTTP status, code and message. This list of the responses contains the following: badRequest, created, forbidden, negotiate, notFound, ok, serverError, unauthorized.

You are able to call each of them from your controller/policy/etc with calling it from res variable. For instance, res.badRequest() or res.ok().

badRequest

By default badRequest respond with HTTP status code 400 and following structure:

{
  "code": "E_BAD_REQUEST",
  "message": "The request cannot be fulfilled due to bad syntax",
  "data": {}
}

created

By default created respond with HTTP status code 201 and following structure:

{
  "code": "CREATED",
  "message": "The request has been fulfilled and resulted in a new resource being created",
  "data": "<RESOURCE>"
}

forbidden

By default forbidden respond with HTTP status code 403 and following structure:

{
  "code": "E_FORBIDDEN",
  "message": "User not authorized to perform the operation",
  "data": {}
}

negotiate

This is generic error handler. When you are calling this response at fact it calls appropriate response for given error. Best of all use it in cases when you want to catch the error from the database, for instance. A little example of using this response:

User.find().then(res.ok).catch(res.negotiate);

notFound

By default notFound respond with HTTP status code 404 and following structure:

{
  "code": "E_NOT_FOUND",
  "message": "The requested resource could not be found but may be available again in the future",
  "data": {}
}

ok

By default ok respond with HTTP status code 200 and following structure:

{
  "code": "OK",
  "message": "Operation is successfully executed",
  "data": "<RESPONSE>"
}

serverError

By default serverError respond with HTTP status code 500 and following structure:

{
  "code": "E_INTERNAL_SERVER_ERROR",
  "message": "Something bad happened on the server",
  "data": {}
}

unauthorized

By default unauthorized respond with HTTP status code 401 and following structure:

{
  "code": "E_UNAUTHORIZED",
  "message": "Missing or invalid authentication token",
  "data": {}
}

API

Each of the responses has the following API (except negotiate):

data - {Array|String|Object} Data that you want to send to data in the response;

config - {Object} Configuration object for overriding code, message and root in the response;

  • config.code - {String} Custom code in the response;
  • config.message - {String} Custom message in the response;
  • config.root - {Object} This param can assign more fields to the root of the response;

negotiate has a little different API. It have only one argument error:

error - {Object} Object that will be parsed via response.

  • error.code - {String} Custom code in the response;
  • error.message - {String} Custom message in the response;
  • error.root - {Object} This param can assign more fields to the root of the response;
  • error.status - {Number} HTTP status that need to respond;

Examples

Respond with a data and catch error

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    User
      .find()
      .then(res.ok)
      .catch(res.negotiate);
  }
};

Respond with a custom message

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    User
      .find()
      .then(function(users) {
        return [users, {code: 'CUSTOM_CODE', message: 'CUSTOM_MESSAGE'}];
      })
      .spread(res.ok)
      .catch(res.negotiate);
  }
};

Call the response directly

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    var myData = {};

    res.badRequest(myData, {code: 'CODE', message: 'MESSAGE'});
  }
};

Service

Quick Overview

We are facing with a problems with Amazon S3 integration or sending Push Notification to Android\iOS very often. When integrating any provider\service you need to read their documentation and this documentation is different in each of them.

So we decided to make reusable Sails services for these tasks and published them to the npm.

Each service has own unified API so that when you decide to move from Amazon S3 to Google Cloud Storage you don't need to rewrite your project.

List of these Sails services contains the following:

How to use them

Each service has own repository (check the links above) where describes their API and configuration. All of these services is already included in generator in api/services so you can call it right away from your code.

List of the Services

CipherService

CipherService is helping you to encode\decode ciphers.

We are using this service when working with JSON Web Tokens for authenticating users. When the user logs in the system, we've just call CipherService.jwt.encodeSync(user.id) for obtaining JWT. When JWT goes within request we call CipherService.jwt.decodeSync(token) for decoding and getting the user.id assigned to this token.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    CipherService.jwt.encode(req.param('data')).then(res.ok).catch(res.negotiate);
  }
};

HashService

HashService is helpful for hashing\comparing passwords.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    HashService.bcrypt.hash(req.param('password')).then(res.ok).catch(res.negotiate);
  }
};

ImageService

ImageService lets you to work with images. Simple functionality: crop image, resize or get IPTC data from an image.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    ImageService
      .thumbnail('my-image.jpg')
      .then(StorageService.upload)
      .then(res.ok)
      .catch(res.negotiate)
  }
};

LocationService

LocationService needs when you are working with addresses and geocodes. It allows to you quickly get geocode from an address or otherwise get address from geocode.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    LocationService
      .geocode('Kirovohrad, Ukraine')
      .then(res.ok)
      .catch(res.negotiate);
  }
};

MailerService

MailerService sends mails to the specified recipient.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    MailerService
      .send({
        to: req.param('to'),
        text: req.param('text')
      })
      .then(res.ok)
      .catch(res.negotiate);
  }
};

PaymentService

PaymentService can quickly steal the money from a card 😃. Basically, it uses providers like Stripe or BrainTreePayments.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    PaymentService
      .checkout({
        amount: req.param('amount'),
        cardNumber: req.param('cardNumber'),
        expMonth: req.param('expMonth'),
        expYear: req.param('expYear'),
        cvv: req.param('cvv')
      })
      .then(res.ok)
      .catch(res.negotiate);
  }
};

PusherService

PusherService allows to you send push notification to the phones just with one line of code.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    PusherService.ios
      .send(['PUSH_TOKEN_1', 'PUSH_TOKEN_2'], {
        title: req.param('title'),
        body: req.param('body')
      })
      .then(res.ok)
      .catch(res.negotiate);
  }
};

SmsService

SmsService is the same as MailerService just sends to the phone numbers.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    SmsService
      .send({
        recipient: req.param('phoneNumber'),
        message: req.param('message')
      })
      .then(res.ok)
      .catch(res.negotiate);
  }
};

SocialService

SocialService is a simple wrapper around http requests and SDK of social networks. Allows to you grab info from a profile.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    SocialService.facebook
      .getFriends(req.param('fb_access_token'))
      .then(res.ok)
      .catch(res.negotiate);
  }
};

StorageService

StorageService is responsible for storing files on different providers. It has simple API that allows to you upload files to storages.

Example:

// api/controllers/AnyController.js
module.exports = {
  index: function(req, res) {
    StorageService
      .upload('<SOURCE>', '<BUCKET>:<KEY>')
      .then(res.ok)
      .catch(res.negotiate);
  }
};