-
Notifications
You must be signed in to change notification settings - Fork 52
Sub Generators
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.
yo sails-rest-api:adapter Amazing # Scaffolds AmazingAdapter in api/adapters folder
yo sails-rest-api:adapter --help # Prints usage info
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.
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
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
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
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
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
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
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
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
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
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 theconfig/
folder of new projects. The authoritative, merged-together configuration used in your app is available at runtime on the sails global assails.config
.
We have a lot of REST APIs built with Sails, so that we know the better configuration than provided by default.
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
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 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 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.
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
anddestroy
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 function is not modified.
Connections configuration file was modified with dictionary of all popular adapters. You don't need to remember all the properties for adapters' configuration.
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
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.
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 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 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 tohttp(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 is also untouched. migrate
property is set to alter
.
The same applies to routes configuration. All the routes binds automatically thanks to blueprints.
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
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
This list contains predefined controllers that we've implemented.
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
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
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.
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
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.
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.
yo sails-rest-api:hook MyHookName # Generates custom hook with MyHookNameHook name
yo sails-rest-api:hook --help # Prints out usage info
We have additional hooks implemented right from the box.
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
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.
yo sails-rest-api:logger
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
.
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
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 shecanWrite
(has write permissions on the folder.) Finally, you'd want to check that the folder she's uploading intohasEnoughSpace
.
We change nothing here, just scaffolding the structure.
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
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 instanceOK
orE_BAD_REQUEST
; -
message
contains detailed message what exactly was happened on the server; -
data
contains result of the response, for instance array of records;
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()
.
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": {}
}
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>"
}
By default forbidden respond with HTTP status code 403 and following structure:
{
"code": "E_FORBIDDEN",
"message": "User not authorized to perform the operation",
"data": {}
}
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);
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": {}
}
By default ok respond with HTTP status code 200 and following structure:
{
"code": "OK",
"message": "Operation is successfully executed",
"data": "<RESPONSE>"
}
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": {}
}
By default unauthorized respond with HTTP status code 401 and following structure:
{
"code": "E_UNAUTHORIZED",
"message": "Missing or invalid authentication token",
"data": {}
}
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} Customcode
in the response; -
config.message
- {String} Custommessage
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} Customcode
in the response; -
error.message
- {String} Custommessage
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;
// api/controllers/AnyController.js
module.exports = {
index: function(req, res) {
User
.find()
.then(res.ok)
.catch(res.negotiate);
}
};
// 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);
}
};
// api/controllers/AnyController.js
module.exports = {
index: function(req, res) {
var myData = {};
res.badRequest(myData, {code: 'CODE', message: 'MESSAGE'});
}
};
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:
- CipherService (encodes\decodes ciphers)
- HashService (hashes and compares with plain data)
- ImageService (crops\resizes image, reads IPTC block)
- LocationService (geocoding address and reverse geocoding from coordinates)
- MailerService (sends mails)
- PaymentService (checkout\refund payments from credit\debit cards)
- PusherService (sends push notification to Android\iOS)
- SmsService (sends SMS to a phone number)
- SocialService (gets info from social networks)
- StorageService (uploads\downloads\removes files from storage)
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.
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 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 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 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 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 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 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 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 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 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);
}
};