Chapter 6. Using models

This chapter covers

  • Using models, attributes, methods, and connections
  • Creating user identity management
  • Transitioning from the default sails-disk database to PostgreSQL
  • An introduction to Waterline and ORMs

Chad dropped by today. It seems he received a panicked call from the investor last night. She was using Brushfire and to her horror found hundreds of dog videos littering the site. Chad said she insisted in the strongest possible terms, “This will not stand.” We explained to Chad that although we couldn’t prevent dog videos from being added to Brushfire, we could require users to be logged in to be able to add videos. That way, if a user violated the Terms of Service (ToS), his mom/investor could ban the user’s account with extreme prejudice. To accomplish this requirement, we’ll need the user to establish their identity and prove that they’re the person who created that identity. We can then personalize the frontend and control access to the backend (based on that proven identity). With few exceptions, applications we’ve built for clients inevitably involve this sort of requirement and the features displayed in figure 6.1.

Figure 6.1. The four components of an identity, authentication, personalization, and access control system

We’ll use this figure as a recurring map for chapters 6 through 10 to show where we are in the process of building out each component. We divided the implementation of user identity, authentication, personalization, and access control into four basic components in table 6.1.

Table 6.1. Components of user identity, authentication, personalization, and access control

Component

Description

User identity management In chapters 6 and 7, you’ll create user identity management that enables a user to claim and manage an identity. This component will also enable a super user, referred to as administrator, to manage all identities. The subcomponents of user identity management will be implemented in two parts. The first part is setting up the model, whereas the second part is using the model to fulfill requirements of the backend API.
Frontend personalization Once you can determine whether a person is logged in or logged out, you’ll want to use that state to control what’s displayed on the frontend, also known as personalization. In chapter 7, you’ll communicate the authenticated state to the frontend, controlling which assets are displayed. In chapter 8, you’ll bootstrap the user’s authenticated state on the page using server-rendered views. You’ll then control what’s displayed on the frontend using a combination of server-rendered views and client-side JavaScript.
Authentication In chapter 9, you’ll create the authentication component. The authentication component provides a way to challenge the authenticity of a user’s claim to a specific identity and determine whether it’s genuine on the backend. You’ll store the results of the challenge (the authenticated state) between requests using sessions on the backend. Finally, using controller actions, you’ll route requests between pages on the backend, which takes the authenticated state from the session and bootstraps that state onto server-rendered views that can be used by the frontend.
Backend API access control Once a user is authenticated, you’ll turn to what they have access to in terms of the backend API. For example, only a user who is authenticated and is the owner of a profile may restore it.

If some of these concepts are unclear, don’t despair. By the time you’ve completed each chapter, you’ll have a thorough understanding of not only the concepts but the practical application of using them in real-world examples.

Your first requirement is understanding and implementing user identity management. This involves creating and using a model of a user. We’ll make sure you have a firm understanding of the model itself. You’ll then concentrate on determining the requirements of the user model and (based on those requirements) implementing the model for identity management. Having an actual model also gives you an opportunity to examine more closely the databases where model records are stored. You’ll then transition Brushfire from using the default sails-disk database to a PostgreSQL database. Finally, you’ll get to know the main model methods via examples we’ll use throughout the book.

6.1. Understanding Sails models

You now need a reliable way to create, store, and manage information about a user in Brushfire. But how do you create a new user record and where will it be stored? What will the user record contain and how can you control what’s in it? The answers to these questions and more can be found in a model.

We first discussed models in chapter 1. If you skipped over that section and are unfamiliar with the concepts of models and databases, now would be a good time to go back and check it out before moving on. Let’s start with the highest-level abstraction of a model definition. In Sails, a model is defined by a JavaScript dictionary. For review, figure 6.2 provides a high-level overview of the properties and methods in a model definition.

Figure 6.2. Model definitions consist of attributes, methods, and settings. In addition, every model is connected with a particular adapter.

Here are the takeaways of figure 6.2:

  1. Model attributes are the properties of a user like the username, email address, and password.
  2. Model methods are functions you use to find and manipulate database records.
  3. Model settings are configuration settings for the model.
  4. Adapters are npm packages that you can install in your project to add support for a particular database. Behind the scenes, the adapter is what allows Sails to provide a unified way of configuring, accessing, and managing a model. Sails takes care of translating this unified approach to the specific requirements of each database system.

Let’s transition from talking about the model to identifying the attributes you’ll use in it for user identity management.

6.2. Managing user data

How do you keep track of users? User identity management generally involves the creation, display, update, and removal of information about a specific user’s identity. That identity is then used to distinguish the user within the broader application. To make this work, you need to create the subcomponents listed in table 6.2.

Table 6.2. User identity management components

Component

Description

Creating a profile with a signup process The signup component allows a user to create an identity. Identity is based on one or more unique pieces of information. In your user identity management, you’ll prompt the user for a unique email address and unique username. Either of these pieces of information can be used to identify a particular user. The signup process will also prompt the user to create a password. This password will later be used as proof of a claim to a particular identity.
Displaying a user profile The user profile component displays information about the user via the user record. The user also has the ability to edit, delete, and later restore their profile.
Editing a user profile Editing a user profile allows the user to edit various aspects of their user record.
Restoring a user profile After a user authenticates successfully, this component allows them to restore their deleted user profile.
Administering a user profile This component allows a designated admin user to perform administrative duties such as adding admin privileges to other users as well as banning users from accessing the system.

Now that you know the general requirements of your frontend, you need to transition those requirements into interactive mockups. Because this book is primarily about the backend and not building a frontend, we’ve provided the mockups for you in a GitHub repo.

6.2.1. Obtaining the example materials for this chapter

To prevent this book from becoming a multivolume set, you’ll start with a fully baked frontend. That is, instead of describing the creation of the frontend, you’ll concentrate on the backend and obtain the frontend assets via a GitHub repo. Navigate your browser to https://github.com/sailsinaction/brushfire-ch6-start, and you should see something similar to figure 6.3.

Figure 6.3. Clone the repo using the URL from the main repo page for the start of chapter 6.

Copy the clone URL from the repo page. From the terminal window, type the following command:

~/brushfire $ git clone https://github.com/sailsinaction/brushfire-ch6-start

Change into the brushfire-ch6-start folder:

~/brushfire $ cd brushfire-ch6-start

Next, you’ll install the Node modules listed in the brushfire-ch6-start/package.json file. From the terminal window, type

~/brushfire-chp6-start $ npm install

Finally, in chapter 5 you used the bootstrap function in Sails to add YouTube videos via machinepack-youtube, which requires a Google API key. In Sublime, copy the brushfire/config/local.js file you created in chapter 5. If you haven’t completed chapter 5, you’ll need to add a brushfire/config/local.js file and add the API key you created in chapter 5. Once you add in the API key, you’re all set.

6.2.2. A frontend-first approach to data modeling

We’ll again turn to our frontend-first approach for guidance on gathering model requirements. This will involve reviewing each interactive mockup to identify user model attributes and any validation or transformation requirements. You’ll find a list of model requirements as a link from the chapter 6 hub, http://sailsinaction.github.io/chapter-6/, or directly from Error! Hyperlink reference not valid.//mng.bz/5yzx.

Validations check the value of a particular model attribute before it’s stored in a record. You can specify that a value must exist for a username and, if it doesn’t, produce an error when attempting to store it.

Note

As you’ll see later in this chapter, because requests can be made outside the browser, any validation that you perform on the frontend will also be implemented on the backend to assure compliance.

A transformation changes the format of a model attribute to comply with some requirement. For example, when a user signs up, you’ll use the user’s email to create a Gravatar URL on the backend.

You want to take a systematic approach to reviewing the frontend mockups. To get an overview of what you need to review, take a look at the current organization of Brushfire’s mockup pages illustrated in figure 6.4.

Figure 6.4. Current organization of the interactive mockups. The profile, edit-profile, and restore-profile pages haven’t yet been connected via links.

This site map and interactive mockups can be found via link from the main chapter 6 hub page, http://sailsinaction.github.io/chapter-6/index.htmlor, or directly here: http://sailsinaction.github.io/chapter-6/mockups.html. The first mockup we’ll review is the signup page.

6.2.3. Building a signup page

The signup page will establish a user’s initial identity, as shown in figure 6.5.

Figure 6.5. The signup page contains four user model attributes: email, username, password, and confirmation.

email

To achieve identity, you need a minimum of two pieces of information, or attributes, in your model: a unique identifier and a password. For the unique identifier, a user’s email address is a logical choice because it can serve multiple purposes. In addition to identity, the email address will be essential for tasks like notification when there’s a forgotten password, which you’ll implement in chapter 11. You’ll want to validate that the email address has the proper syntax and require it to create a user record.

username

You also have a requirement to display a Brushfire user profile to other users of Brushfire. Displaying an email address in such a profile would violate a user’s privacy. Therefore, you’ll prompt the user to create a unique username during the signup process that will display in the profile. The username will be required to create a user record and it must contain at least six characters. Finally, you want to restrict the username to using Aa–Zz and 0–9 only.

password

You’ve set a minimum length of six characters for a user’s password. You won’t be storing passwords as clear text in the user model. Instead, you’ll encrypt the user’s password and label the resulting value of the encryption process as an attribute named encryptedPassword.

encryptedPassword

The encryptedPassword will be a required attribute to create a user record. The confirmation input field won’t be stored in the database. Our review of the signup mockup page has produced the model requirements outlined in table 6.3.

Table 6.3. The signup page’s model requirements

Input field

Attribute name

Req?

Type

Frontend and backend validations

Transformations

username username Yes string Must be unique. The attribute is required to create a record. The username can contain only Aa–Zz and 0–9. None.
email email Yes string Must be unique. Must be a valid email address. The attribute is required to create a record. None.
password encryptedPassword Yes string Password must be at least six characters. The attribute is required to create a record. Password should be encrypted.

6.2.4. Building a user profile page

The profile page contains information about the user, as shown figure 6.6.

Figure 6.6. The profile page contains an additional user model attribute we’ve named gravatarURL. This attribute will store the URL linking the Gravatar image.

The profile page adds the gravatarURL attribute to your user model. The investor wants each user to have a nice picture to represent them on the profile page. You’ll use WordPress’s ubiquitous Gravatar system for these profile pictures. Gravatar images are accessed via a generated URL based on the user’s email address. You’ll use the email to create and store a transformed URL in an attribute named gravatarURL. Table 6.4 contains the profile page’s model requirements.

Table 6.4. The profile page’s additional model requirements

Input field

Attribute name

Req?

Type

Backend validations

Transformation

email gravatarURL No string None Create Gravatar URL from the email address.

6.2.5. Building an admin interface

This administration page allows a designated admin user to perform administrative duties such as add admin privileges to other users as well as ban users from accessing Brushfire, as shown in figure 6.7.

Figure 6.7. The User Administration page adds two user model attributes: admin and banned.

banned

When a user has been restricted from using the site for a violation of the site’s Terms of Service agreement, you need a way to store the state of that user’s access to Brushfire. You’ll use an attribute named banned to store whether a user has restricted access.

admin

You also want to limit the right to ban a user to only those users with administrator privileges. To accomplish this, you’ll store whether a user has administrator privileges in an attribute named admin.

After reviewing the administration page, you have the model requirements listed in table 6.5.

Table 6.5. The administration page’s additional model requirements

Input field

Attribute name

Req?

Type

Backend validations

Transformations

banned banned No boolean When a record is created, the field should be set to false. None
admin admin No boolean When a record is created, the field should be set to false. None

6.2.6. Recovering data after a soft delete

Your app includes a restore-profile page, which allows a user to undelete their profile. Let’s examine the Restore a Profile page shown in figure 6.8.

Figure 6.8. The restore-profile page allows a user to remove the deleted state of their profile. This will necessitate adding a deleted attribute to the user model.

Although the restore-profile page doesn’t add any additional fields in the UI, you know you’ll need a deleted attribute to hold the soft-deleted state of a user record.

Note

We’ll examine what a soft delete system entails in chapter 7.

Finally, you’ll skip the edit-profile page because it doesn’t add any additional model requirements. Now that you have your requirements, let’s set about implementing them in an actual model.

6.3. Creating a new model

The first step in implementing the model is to generate a default model configuration file. In chapter 4, you created a video API that generated empty model and controller files. This enabled you to start using blueprint routes and actions to create and update video records. After the user API is generated, you’ll create a user record with the supplied signup page that uses the blueprint RESTful create route and action. In chapter 7, you’ll transform your blueprint routes and blueprint actions into explicit routes and custom controller actions.

6.3.1. Running the generator

Because you know you’ll want both a user model and some way to manage it via controller actions, let’s generate an API. Head over to the terminal window, and from the command line, type

~/brushfire-chp6-start $ sails generate api user
info: Created a new api!

Sails generates an empty controller and model similar to what it generated in chapter 4. Let’s use the blueprint create route and action to generate your first user record.

6.3.2. Creating your first record

Now that you have a user API, let’s use the knowledge you gained in chapter 4 to create a user record with blueprints. Figure 6.9 illustrates the request the Angular controller will execute when the user clicks the Create User Account button.

Figure 6.9. Clicking this button sends a POST request to /user.

Not surprisingly, the purpose of this page is to collect the necessary information to create a record using attributes and methods in the user model. The form within the page contains four input fields: email, username, password, and confirmation.

The Angular controller will execute an AJAX POST request to /user and include three input fields as parameters when the Create User Account button is clicked.

Note

The confirmation input field won’t be sent in the POST request.

In figure 6.10, the request will match the blueprint RESTful create route that will in turn trigger the blueprint create action that uses the User.create() model method to create a record . Let’s see this in action. Restart Sails using sails lift and navigate your browser to localhost:1337/#/signup.

Figure 6.10. The Sails router listens for an incoming request, matches it to a RESTful blueprint route, executes the create blueprint action, and responds with a 200 status code and the newly created user record as JSON.

Note

Why use the hash symbol in the path of your signup browser request? HTTP will ignore anything after the hash symbol (#). This allows other frameworks like Angular to come up with their own routing strategy. So, the /signup path of /#/signup is actually being processed by Angular’s router and not by the backend Sails router. The Angular router then determines which template file to display. In this case, it’s your brushfire/assets/templates/signup.html file.

Next, sign up a user with an email address of [email protected], a username of sailsinaction, and a password of abc123. After clicking the Create User Account button, you should see something similar to figure 6.11 in your browser.

Figure 6.11. This is the result of the blueprint POST request to /user and the redirect to localhost:1337/user/1. In addition to the input fields that were added to the record, createdAt, updatedAt, and id were also added.

A user record was created using the three input fields from the signup form . Three other attributes were added to the record , including id, createdAt, and updatedAt. Where did these other attributes come from? The additional attributes are created by default with each new record in the database. We’ll take a closer look at these three additional attributes in the next section.

You might be wondering how email, username, and password were added to the user record without being defined first as attributes in the user model. By default, Sails uses sails-disk for the database in a new project. The sails-disk database doesn’t require a predefined set of attributes to save records using those attributes. This type of database is also referred to as a schemaless database.

Definition

A database schema is a description of the database’s structure: how data is organized and how it’s constructed. In the case of a relational database, part of its structure is how it’s divided into tables and columns. Because SQL databases require a schema, defining attributes provides the added benefit of enabling a model to be SQL compatible.

Let’s next take a closer look at databases and the records they store on your behalf.

6.4. Demystifying databases

In chapter 1, we introduced a database as simply an application that stores an organized collection of data into records. The power of Sails models is that they abstract away many of the details you’d normally have to understand when it comes to creating, finding, updating, and destroying records in a database. It’s helpful, however, to “follow the turtles all the way down” to the database level at least once. So let’s go on a short turtle safari.

6.4.1. Models, connections, and adapters

Figure 6.12 illustrates the relationships between a model and its connection to a database through an adapter.

Figure 6.12. Sails looks for the database connection it will use to store and manipulate records for a particular model in brushfire/api/models/User.js . If it doesn’t find a connection, it then looks to model settings . If no connection exists, Sails looks to the internal core default connection, localDiskDb . The default connection uses the sails-disk adapter to access the sails-disk database.

Each layer in figure 6.12 consists of three columns: the component, the location of the file that configures that component, and an example of a configuration. To determine which database a model will use to store records, Sails first looks in the model definition (for example, brushfire/api/models/User.js) for a connection property. The following listing shows an empty model definition.

Example 6.1. An empty model definition
module.exports.models = {

  attributes: {

  }

};

This connection property is also referred to as the model’s datastore. We’ll use connection and datastore interchangeably in the book. The user model is currently empty and doesn’t contain a connection property. So Sails will search for a connection property in the top-level model settings, which are typically defined by a configuration file located at brushfire/config/models.js. The next listing shows the global settings for all models.

Example 6.2. The global settings for all models

When Sails generated your project, one of the files it generated was brushfire/config/models.js with the connection property commented out.

Tip

Many of the default configuration settings in Sails have corresponding commented parameters for convenience. This makes it easier to recognize the availability of a setting that can be overridden within a file.

Because your models.js file doesn’t contain a connection property, Sails will instead rely on a default core connection named localDiskDb.

Note

We say that localDiskDb is a core connection because the property isn’t currently defined outside of Sails’ core code base.

But what does the connection point to? The connection property is a key name lookup for dictionaries located in brushfire/config/connections.js. These dictionaries contain the connection instructions Sails uses to access a desired database. The connections.js file provides a central location for all potential database connections you’ll use for a project. One of the connection dictionaries is localDiskDb. Open brushfire/config/connections.js in Sublime and take a look at the default connection dictionaries, including the localDiskDb connection in the next listing.

Example 6.3. The default connection settings

Typically, the connection dictionary will contain information including the host, the database access credentials username and password, the name of the database, and the Sails adapter to use.

Note

The database name we refer to here isn’t the actual name of a database system like PostgreSQL, MySQL, or MongoDB. It's whatever arbitrary name you provide for your database like brushfire, mydatabase, and the like.

Here, the localDiskDb property is a dictionary that contains the adapter Sails will use to connect to the database. In this case, the adapter and database are set to sails-disk. Recall that Sails uses adapters to abstract away the complexity of using different syntax to access and manage each database.

Definition

An adapter is a bit of code that maps model methods like find() and create() to a lower-level syntax like SELECT * FROM and INSERT INTO. The Sails core team maintains open source adapters for a handful of the most popular databases, and a wealth of community adapters is also available at Error! Hyperlink reference not valid.//mng.bz/67lJ.

sails-disk is an adapter that talks directly to the sails-disk database. It’s unfortunate that the adapter and the database have the same names, but you can handle it. You’ve come this far, so you might as well look at the last turtle—the database itself; see figure 6.13. sails-disk is a simple database that stores data as JSON. It’s unique in that it stores the data as a text file you can access located in brushfire/.tmp/local-DiskDb.db. Opening this file reveals the first user record you added as well as the video records added in the bootstrap.

Figure 6.13. The sails-disk database located in brushfire/.tmp/local-DiskDb.db reveals the first user record and the first video record .

Each record must have some way of uniquely identifying itself, typically through a unique id. Sails adapters automatically add this id as a primary key or unique key of the database. The id is autoincremented (meaning that Sails will take care of making sure it’s unique). The details of the primary key vary between adapters. For example, PostgreSQL uses an autoincrementing integer primary key, whereas MongoDB uses a randomized string UUID. The adapter also adds attributes when the record is created—createdAt—and updated—updatedAt—to the model attributes.

Other databases might be more sophisticated than sails-disk, but the principles remain the same. A database can store data on disk or in memory and has an API you can use to talk to it. Sails provides a higher-level, easier-to-understand, consistent layer on top of that API that’s called a model.

You’ve made it relatively unscathed, exploring all the steps of how models connect to actual databases. Although Sails is your trusted intermediary and shields you from many tasks, it remains your responsibility to convey which databases you want to employ. This includes providing configuration information like username, password, host, and database name, as well as the appropriate Sails adapter to use before the database can communicate with your models and vice versa.

6.4.2. Configuring a database

So far, you’ve been using models without configuring any information about model attributes, like the email, username, and password properties of the records you’ve added. In addition, the video model and user model haven’t configured a connection. Therefore, all models are using the default sails-disk database. sails-disk is a NoSQL or schemaless database and therefore doesn’t require defined attributes to store records. This is extremely useful in keeping you nimble during the design phase of your application. For example, you were able to start creating user records immediately after generating the API with Sails blueprints.

There are times, however, when you want to use an SQL database. For the user model, you’ll store records in a PostgreSQL database. PostgreSQL is a popular, open source SQL database that runs on a variety of different platforms and can be downloaded at http://www.postgresql.org/download/. For our OS X environment, we use an all-in-one installation solution called Postgress.app, found at http://postgresapp.com/.

Note

For now, you’ll run the database locally, but in later chapters when you go to production you’ll use a hosted version of PostgreSQL.

After you’ve installed PostgreSQL, launch the Postgres application. OS X users should see an elephant—yes, I said elephant—in the upper-right navbar. Click it and open psql, which is the PostgreSQL terminal. Next, create a database named brushfire by typing

CREATE DATABASE brushfire;

You learned that Sails uses adapters to abstract away the complexity of using different syntax to access and manage each database. To start the transition, you need to install the PostgreSQL adapter. Head over to the terminal window, not the PostgreSQL terminal, and type

~/brushfire-chp6-start $ npm install sails-postgresql --save

Installing a module via npm install installs the module in the brushfire-chp6-start/node_modules folder. In Sublime, open brushfire-chp6-start/package.json and take a look at the dependencies property, shown in the next listing.

Example 6.4. The dependencies property of a package.json file

A module dependency is just a fancy name for a key/value pair in a module’s package.json file that identifies the module name and a version npm uses to find and install the module. This becomes essential when you start deploying Brushfire. For example, when you push a Sails application to a hosted service like Heroku, you’re sending the applications files and folders without the node_modules folder. Heroku then completes the installation of your app like any other Node module using npm install. If the dependency isn’t in the package.json file, npm won’t install the necessary modules.

Now that you have the adapter installed, you need to let Sails know that you want your user model to store records using it. In Sublime, open the user model located in brushfire-ch6-start/api/models/User.js. Add a connection property as shown here.

Example 6.5. Adding a connection property to the user model

The connection property contains an arbitrary name you’ve given to the connection that points to some configuration information about your PostgreSQL database in /brushfire-chp6-start/config/connections.js. In Sublime, open /brushfire-chp6-start/config/connections.js and add the following configuration information to myPost-gresqlServer.

Example 6.6. Adding a connection to the connections.js file
...
myPostgresqlServer: {
    adapter: 'sails-postgresql',
    host: 'localhost',
    database: 'brushfire'
  },
...

Your myPostgresqlServer connection contains a dictionary of configuration information, including the adapter, host, and database name.

Note

We could have provided a username and password, but during development we chose not to do so.

Because PostgreSQL is an SQL database, you must provide defined attributes before you can create records using them in your model. That’s not a problem because you established what attributes you need earlier in the chapter.

6.4.3. Defining attributes

Let’s add attribute definitions to the user model, also known as a database schema, based on the requirements created earlier in the chapter. From Sublime, open brushfire-chp6-start/api/models/User.js and add the following model attributes.

Example 6.7. Defining user attributes in the model
module.exports = {

  connection: 'myPostgresqlServer',

  attributes: {

    email: {
      type: 'string',
    },

    username: {
      type: 'string',
    },

    encryptedPassword: {
      type: 'string'
    },

    gravatarURL: {
      type: 'string'
    },

    deleted: {
      type: 'boolean'
    },

    admin: {
      type: 'boolean'
    },

    banned: {
      type: 'boolean'
    }
  }
}

Sails bundles support for automatic validations of your models’ attributes. Any time a record is updated or a new record is created, the data for each attribute will be checked against all your predefined validation rules. This provides a convenient failsafe to ensure that invalid entries don’t make their way into your app’s database(s). Every attribute definition must have a built-in data type (or typeclass) specified. For example, you’ll use the string data type for the email, username, encryptedPassword, and gravatarURL attributes. For the deleted, admin, and banned attributes, you’ll use the boolean data type.

Except for unique (which is implemented as a database-level constraint), all validations are implemented in JavaScript and run in the same Node.js server process as Sails. Validations can be a huge timesaver, preventing you from writing many hundreds of lines of repetitive code. But keep in mind that model validations are run for every create or update in your application. Before using a validation rule in one of your attribute definitions, make sure you’re okay with it being applied every time your application calls .create() or .update() to specify a new value for that attribute. For example, let’s say that your Sails app allows users to sign up for an account either by entering an email address and password and then confirming that email address or by signing up with LinkedIn. Now, let’s say your user model has one attribute called linkedInEmail and another attribute called manuallyEnteredEmail. Even though one of those email address attributes is required, which one is required depends on how a user signed up. In that case, your user model can’t use the required: true validation; instead, you’ll need to validate that one email or the other was provided and is valid by manually checking these values before the relevant .create() and .update() methods are executed. In other cases, enforcing the validation on each .create() and .update() call is advantageous. For example, you can set an email validation that enforces the use of valid email syntax. In that case, there’s never an instance when you want to allow an improperly formatted email address. Therefore, applying that restriction as an attribute validation makes sense. So, you’ll use some of the attribute validations to enforce restrictions in the model, and enforce others directly in a controller action in later chapters.

6.4.4. Attribute validation

Your first attribute validation is a requirement for both the email and username attributes to be unique. That means no record can contain an identical email or username in the database. If an attempt is made to create or update a record using a model method with an identical attribute, the method will produce an error. The unique validation is different than other validations. Imagine you have one million user records in your database. If unique was implemented like other validations, every time a new user signed up for your app, Sails would need to search through one million existing records to ensure that no one else was already using the email address provided by the new user. Not only would that be slow, but by the time it finished searching through all those records, someone else could have signed up!

Fortunately, this type of uniqueness check is perhaps the most universal feature of any database. To take advantage of that, Sails relies on the database adapter to implement support for the unique validation—specifically by adding a uniqueness constraint to the relevant field/column/attribute in the database itself during auto-migration.

Note

You first encountered auto-migrations in chapter 4 when you set the mode to alter. As you’ll see in the next section, Sails will automatically generate tables/collections in the underlying database with uniqueness constraints built right in. Once you switch to migrate:'safe' in chapter 15, updating your database constraints will be up to you.

You’ll also add an email validation to the email attribute, which validates incoming values to a valid email address syntax before the email address can be stored or updated as part of a record. Open brushfire-chp6-start/api/models/User.js in Sublime and add the following attribute options.

Example 6.8. Adding attribute validations to the user model

Let’s see this in action. Restart Sails using sails lift and navigate back to the signup page. Once again, sign up with an email address of [email protected], a username of sailsinaction, and a password of abc123. After clicking Create User Account, your browser should look similar to figure 6.14.

Figure 6.14. When you attempt to create a user, a record with an identical email address exists in the database and therefore produces a validation error.

Your new unique validation for the email attribute produces an error when you try to add a user with an email address equal to an existing user record. You could create another user with a different email address, but while you’re developing you really need a way for your database to reset to an empty state each time you restart the Sails server. You can do that by changing the model auto-migration setting.

6.4.5. Handling existing data with Sails auto-migrations

In chapter 4, you had your first encounter with Sails auto-migrations. Each time you start Brushfire, Sails needs to know whether to attempt to rebuild the database and, if records exist, what to do with them. If you set auto-migrations to safe, Sails doesn’t do anything other than create a connection to the database and run queries. It’s the default environment for production and should be used whenever you’re working with production data or any records you don’t want to risk losing.

If you set auto-migrations to drop, instead of trying to migrate the data, this mode drops the database and creates brand-new tables or collections, essentially giving you a fresh start. If you have a bootstrap file that resets your data each time the Sails server starts and you don’t care about existing records, then drop auto-migration is a good way to go when your models are constantly changing in the early stages of development.

If you set migrations to alter, Sails attempts to store all the records in memory before it drops the database. When the table or collection has been re-created, Sails attempts to reinsert the stored records into the new data structure. The alter mode is useful if you have a very small dataset and are making trivial changes to model attributes. You’ll ultimately be using safe mode because it will ensure data integrity when you go into production, but during the design phase of the app you’ll be using drop mode.

So far, you’ve set the global migrate property for all models to alter in brushfire-chp6-start/config/models.js. For the next chapter, it will be important to start with new data for your user model each time the Sails server starts. Using the alter mode keeps your user records between server restarts. Therefore, you’ll change the migrate property to drop. But instead of setting this property globally in brushfire-chp6-start/config/models.js, you’ll add the property specifically to the user model. In Sublime, open brushfire-chp6-start/api/models/User.js and change the migrate property to drop, similar to the following listing.

Example 6.9. Adding the migrate property to drop for a specific model

Restart the Sails server using sails lift and sign up a user with an email address of [email protected], a username of sailsinaction, and a password of abc123. After you click the Create User Account button, your browser will display the new user record. There’s no violation of the unique validation because the database was reset and no user records exist when the Sails server is restarted. The user record created, however, contains properties that you don’t want returned to the frontend. You’ll fix that in the next section.

6.4.6. Filtering data returned by blueprints

As you can see from figure 6.15, the blueprint create action returned all the stored parameters to the requesting frontend, including the encryptedPassword.

Figure 6.15. Using the blueprint actions, all parameters are returned to the frontend.

You also may have noticed that the password was not returned. Earlier the password was returned by the blueprint create action because you were using sails-disk, which didn’t require that an attribute be defined before it could be used. Now that you’re using PostgreSQL, any parameter not defined as an attribute won’t be stored in the user record. You can limit the attributes defined in the model and returned by a blueprint action by overriding the .toJSON() method in the model. Open brushfire-chp6-start/api/models/User.js in Sublime and add the following toJSON method.

Example 6.10. Preventing certain attributes from being returned to the client

After overriding the toJSON() method, the blueprint create action will no longer return the password, confirmation, or encryptedPassword parameters to the frontend.

6.5. Understanding model methods

In chapter 4, you used the create() and find() methods indirectly to list and create video records via the blueprint create action and blueprint find action. In chapter 5, you used the Video.create() and Video.count() methods directly. Let’s now look at the model methods you’ll use most in Brushfire, listed in table 6.6.

Table 6.6. Model methods

Method

Description

.create() Creates a new record in the database
.find() Finds and returns all records that match a certain criteria
.findOne() Attempts to find a particular record in your database that matches the given criteria
.update() Updates existing records in the database that match the specified criteria
.destroy() Destroys records in your database that match the given criteria
.count() Returns the number of records in your database that meet the given search criteria

Not surprisingly, all but one of these are part of the ubiquitous create, read, update, and delete (CRUD) operations you learned about in chapter 1. Typically, you’ll use model methods in a custom controller action. But in this section you’ll use them in the Sails console. The Sails console is a way to start the Sails server in a project and then interact with it in the Node read-eval-print loop (REPL).

Definition

The REPL is an interactive tool that allows you to interact with a programming environment, in this case Node and Sails.

This means you can access and use all of your models to try out various queries during development without having to add them in a controller action and restart the Sails server each time. If your Sails server is currently running, close it by pressing Ctrl-C (twice). To start the Sails console, open a terminal window, and from the root of your project type

~/brushfire-chp6-start $ sails console
info: Starting app in interactive mode...

info: Welcome to the Sails console.
info: ( to exit, type <CTRL>+<C> )

sails>

Let’s use the Sails console to explore some model methods.

6.5.1. Anatomy of a Sails model method

First, we’ll look at the essential syntax of a Sails model method, as illustrated in figure 6.16.

Figure 6.16. The generic syntax of a model method includes the model name , model method , criteria , values , the query method , the callback method with error and result arguments , and the callback body .

Methods like .find, .create, .update, and .destroy are the initial methods to start a database query that finds and/or manipulates a record in a database. We use the .update() method as an overall example because it uses both criteria and values as arguments. To use the .update() method, start with the name of the model dictionary, user. Next, add the model method name, update. Most model methods use a criteria, which contains values the query uses to find existing records. The second argument is the values that will be updated. We’ll also look at query methods that can be chained on these initial methods to help configure the query with the .exec() method being the last in this chain. So, .exec() passes all the instructions for the query to the adapter, which executes the query and returns results. When the query is completed, Sails will respond using a familiar pattern of returning any errors as the first argument, and a result as the second argument of the callback.

6.5.2. The .create() model method

The create model method doesn’t require a criteria, as illustrated in figure 6.17.

Figure 6.17. The create model method uses a syntax similar to the find, update, and destroy methods, but without criteria.

Let’s create a few records you can use to explore your model methods. Make sure the Sails console is running, and type or copy each query in listing 6.11.

Example 6.11. Using the create method to create user records

6.5.3. The .find() model method

The find method returns all records that meet the criteria passed as the first argument of the method. The criteria can be a dictionary, a string, or a number of the id you’re trying to find. If no criteria argument is given, all records will be returned. Give this a try. Make sure the Sails console is running, and then type or copy and paste the query shown here.

Example 6.12. Returning all user records using the find method with no arguments
User.find().exec(function(err, foundRecords){
  if (err) console.log(err);

  console.log('The user records: ', foundRecords);

});

The terminal window should return results similar to the following listing.

Example 6.13. Results from the find query
[ { email: '[email protected]',
    username: 'sailsinaction',
    encryptedPassword: null,
    gravatarURL: null,
    deleted: false,
    admin: false,
    banned: false,
    id: 1,
    createdAt: '2016-03-10T04:25:29.000Z',
    updatedAt: '2016-03-10T04:25:29.000Z' },
  { email: '[email protected]',
    username: 'nikolateslaidol',
    encryptedPassword: null,
    gravatarURL: null,

    deleted: false,
    admin: false,
    banned: false,
    id: 2,
    createdAt: '2016-03-10T04:25:39.000Z',
    updatedAt: '2016-03-10T04:25:39.000Z' },
  { email: '[email protected]',
    username: ' franksinatra',
    encryptedPassword: null,
    gravatarURL: null,
    deleted: true,
    admin: false,
    banned: false,
    id: 3,
    createdAt: '2016-03-10T04:25:45.000Z',
    updatedAt: '2016-03-10T04:25:45.000Z' } ]

The find method returns an array of dictionary user records.

Note

Even if there’s a single record returned, that dictionary will be within an array.

Next, let’s find a particular user record by passing in a criteria dictionary as the first argument: {username: ['sailsinaction', 'nikolateslaidol']}. Copy and paste the query shown here into the Sails console.

Example 6.14. Using an IN query
User.findOne({username: ['sailsinaction', 'nikolateslaidol']}).exec(function(err, foundRecords){if (err) console.log(err); console.log(foundRecords); });

The console should return results similar to this.

Example 6.15. Results from the IN query
[ { email: '[email protected]',
    username: 'sailsinaction',
    encryptedPassword: null,
    gravatarURL: null,
    deleted: false,
    admin: false,
    banned: false,
    id: 1,
    createdAt: '2016-03-10T04:25:29.000Z',
    updatedAt: '2016-03-10T04:25:29.000Z' },
  { email: '[email protected]',
    username: 'nikolateslaidol',
    encryptedPassword: null,
    gravatarURL: null,
    deleted: false,
    admin: false,

    banned: false,
    id: 2,
    createdAt: '2016-03-10T04:25:39.000Z',
    updatedAt: '2016-03-10T04:25:39.000Z' }]

This query is also referred to as an IN query, where each value in the array is treated as or, so sailsinaction or nikolateslaidol. Because you have 15 video records available to query, you’ll use the video model for this next example.

Note

The videos you return in your project may be different than those in the example. The contents of YouTube are constantly changing and, therefore, the search results from the .searchVideos machine in bootstrap.js may vary from those shown in the book.

Let’s say you want to query on a value that’s a fragment of what’s contained within a record attribute. That is, you want to find records that contain the value The in the title attribute of the video model. With the Sails console running, type or (copy and paste) the query in the next listing into the Sails console.

Example 6.16. Using contains in a query
Video.find({title: {'contains': 'The'}}).exec(function(err, found) {if (err) console.log(err);console.log(found);});

The console should return results similar to the following.

Example 6.17. Results from the contains query
[ { title: 'The Original Grumpy Cat!',
    src: 'https://www.youtube.com/embed/INscMGmhmX4',
    createdAt: '2016-03-10T04:25:24.846Z',
    updatedAt: '2016-03-10T04:25:24.846Z',
    id: 271 },
  { title: 'GRUMPY CAT! | The Subscriber City Challenge | Ep.21',
    src: 'https://www.youtube.com/embed/EW_gDH5IqTA',
    createdAt: '2016-03-10T04:25:24.849Z',
    updatedAt: '2016-03-10T04:25:24.849Z',
    id: 275 },
  { title: 'Oscar the Grouch vs. Grumpy Cat | Mashable',
    src: 'https://www.youtube.com/embed/QDUyazvnLkc',
    createdAt: '2016-03-10T04:25:24.851Z',
    updatedAt: '2016-03-10T04:25:24.851Z',
    id: 279 },
  { title: 'Grumpy Cat In The Sky!?!',
    src: 'https://www.youtube.com/embed/iinQDhsdE9s',
    createdAt: '2016-03-10T04:25:24.852Z',
    updatedAt: '2016-03-10T04:25:24.852Z',
    id: 280 },
  { title: 'Minecraft Modded Mini-Game : FEED THE GRUMPY CAT!',
    src: 'https://www.youtube.com/embed/gxfWnVS3U2M',
    createdAt: '2016-03-10T04:25:24.854Z',
    updatedAt: '2016-03-10T04:25:24.854Z',

    id: 283 },
  { title: 'Minecraft Mini-Game : PLEASE THE GRUMPY CAT!',
    src: 'https://www.youtube.com/embed/AezV3epQLpE',
    createdAt: '2016-03-10T04:25:24.854Z',
    updatedAt: '2016-03-10T04:25:24.854Z',
    id: 284 } ]

You can find a complete guide to criteria language options at http://sailsjs.org/documentation/concepts/models-and-orm/query-language. Finally, let’s say you want to find a single record returned as a dictionary instead of a dictionary within an array. For that, you can use the findOne model method. With the Sails console running, type or copy and paste the following query into the Sails console.

Example 6.18. Finding a single record with findOne
User.find({email: '[email protected]'}).exec(function(err, found) {if (err) console.log(err);console.log(found);});

The console should return results similar to the following listing.

Example 6.19. Results from the findOne query
{ email: '[email protected]',
  username: 'sailsinaction',
  encryptedPassword: null,
  gravatarURL: null,
  deleted: false,
  admin: false,
  banned: false,
  id: 1,
  createdAt: '2016-03-10T04:25:29.000Z',
  updatedAt: '2016-03-10T04:25:29.000Z' }

As expected, the findOne method responds with a single record dictionary.

6.5.4. The .update() model method

You already explored the syntax of the update model method at the beginning of this section. Now let’s see it in action. For example, you’ll make the user record with the username sailsinaction an administrator by updating the admin property to true. With the Sails console running, type or copy and paste the following query into the Sails console.

Example 6.20. Updating a user record with the update model method
User.update({username: 'sailsinaction'}, {admin: true}).exec(function(err,
 updatedRecord){if (err) console.log(err);console.log(updatedRecord);
});

The console should return results similar to these.

Example 6.21. Results from the update query
[ { email: '[email protected]',
    username: 'sailsinaction',
    encryptedPassword: null,
    gravatarURL: null,
    deleted: false,
    admin: true,
    banned: false,
    id: 1,
    createdAt: '2016-03-10T04:25:29.000Z',
    updatedAt: '2016-03-10T05:30:24.000Z' } ]

The update model method returns an array with the dictionary of the record you updated as expected.

6.5.5. The .destroy() model method

As its name implies, the destroy model method can delete one or more existing records in the model based on the criteria provided as the first argument. The criteria can be a dictionary or an array of dictionaries. The criteria can also be a string or number of the id you’re trying to destroy. Let’s say you want to delete any records that have their deleted property set to true. Ensure that the Sails console is running, and then type or copy and paste the following query into the Sails console.

Example 6.22. Deleting a user record with the destroy model method
User.destroy({deleted: true}).exec(function(err, deletedRecord){
if (err) console.log(err); console.log(deletedRecord);});

Your terminal window should look similar to this.

Example 6.23. Results from the destroy query
[ { email: '[email protected]',
    username: 'franksinatra',
    encryptedPassword: null,
    gravatarURL: null,
    deleted: true,
    admin: false,
    banned: false,
    id: 1,
    createdAt: Wed Mar 09 2016 23:51:04 GMT-0600 (CST),
    updatedAt: Wed Mar 09 2016 23:51:04 GMT-0600 (CST) } ]

The destroy model method returns an array with the dictionary of the record that was destroyed as expected.

6.5.6. The .count() model method

The count model method returns the number of records in a particular model. Ensure that the Sails console is running, and then type or copy and paste the following query into the Sails console.

Example 6.24. Counting the records in a model with the .count() method
Video.count().exec(function(err, count){if (err) console.log(err);
 console.log(count);});

Your terminal window should display the number 15, which is the number of records in the video model.

6.6. Summary

  • Sails models contain attributes, methods, settings, and an adapter named around a common resource.
  • Model requirements consist of attributes, validations, and transformations.
  • Model requirements are identified using the frontend-first approach by reviewing interactive mockups.
  • Models connect to a database using a connection that points to an adapter, which translates a common query interface into the specific syntax of the underlying database.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset