This document describes adding custom data models and object relational mapping (ORM) functionality to a Node.js application using StrongLoop. It details creating a Video model, defining a one-to-many relationship between Video and User, adding access controls, and connecting the models to a PostgreSQL database for persistent storage. Key steps include generating a Video model, relating it to User, adding ACLs, connecting to a PostgreSQL data source, and using automigration to generate tables.
Integration and Automation in Practice: CI/CD in Mule Integration and Automat...
Add Custom Model and ORM to Node.js App with StrongLoop
1. remkohdev 1/10/2016
QAVideos (2) – Add Custom Model and ORM to Node.js
remkohde.com/2016/01/10/add-custom-objects-and-user-management-to-nodejs-2/
This is part 2 in a series to build a sample application called QAVideos using StrongLoop.
In part 1 ‘QAVideos (Part 1), Adding User Management to Node.js with StrongLoop ‘, I showed how to add User
Management to a Node.js app using StrongLoop.
In this part 2, I will add a custom data model, i.e. a Video, Question and Answer models and use ORM to persist
data to a PostGreSQL database.
Part 3 is found here, which adds model extensions and uses Swagger (now Open API Initiative) support.
Requirements:
Install Node.js and npm,
Install StrongLoop.
Check if the ‘slc’ tool is installed, by running ‘slc’ from the commandline. If not, follow the installation
instructions, here.
Get the source code for part 1 of this tutorial and follow the installation instructions here.
Table of Contents:
1. Create Data Model
2. Define Relation
3. Adding ACL
4. Add Video Functionality
5. Add Data Source
1. Create Data Model
First, test if QAVideos (part 1) is running correctly by typing ‘node .’ in the root directory and browsing to
‘http://localhost:3000/explorer’ in your browser.
Now add a custom model ‘Video’ so that users can manage a list of videos. To do this, I create a model for the
Video, define the relationship between Video and User (a User can have many videos), and specify the access level
of users to the Video object using an Access Control List (ACL).
To create models with StrongLoop you can use the ‘slc loopback:model’ command to run the model generator. I will
create a Video model with the following properties:
title (string; required),
url (string; required),
username (string; not required),
date_published (date; not required),
1/17
2. likes (number; not required),
dislikes (number; not required).
$ slc loopback:model Video
2/17
3. This creates two files: ~/common/models/video.js and ~/common/models/video.json. The ‘video.js’ file exports the
video.js module as a function that takes a Video model as a parameter. The ‘video.json’ file is the configuration file
for the Video model.
video.js
module.exports = function(Video) {
};
video.json
{
"name": "Video",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {
"title": {
"type": "string",
"required": true
},
"url": {
"type": "string",
"required": true
},
"username": {
"type": "string"
},
"date_published": {
"type": "date"
},
"likes": {
"type": "number"
},
"dislikes": {
"type": "number"
}
},
3/17
4. "validations": [],
"relations": {},
"acls": [],
"methods": {}
}
2. Define Relation
In this case, I want to create a 1-to-many relation between the User model and the Video model.
There are 3 ways to do this, and I will use the second ‘belongs to’ method for a reason:
1. A ‘has many’ relation from User to Video managed by StrongLoop. The custom foreign key is optional if you
plan to only use the memory database, by default StrongLoop links the two object models User and Video by
a ‘video.userId’ property if you use the ‘has many’ relation.
2. A ‘belongs to’ relation from Video to User managed by StrongLoop, or
3. Custom manage the relation by implementing your own code.
If you plan like I do, to switch to a relational database later (see below), then the StrongLoop automigrate tool at this
moment does not support persisting the foreign key relationship from the ‘has many’ relation, and therefor you must
either use the ‘belongs to’ relation or you need to explicitly define it in our code (for create, update and find ‘My
Videos’). I recommend to and in this tutorial use the ‘belongs to’ relation from Video to User.
To define the relation between User and Video, create a one-to-many relation as follows.
$ slc loopback:relation
This results in the following ‘relations’ configuration in the ‘~/common/models/video.json’ file. You can also directly
add the relation configuration to the ‘~/common/models/video.json’ file (for instance if you get an error with the
generator).
"relations": {
"videoBelongsToUser": {
"type": "belongsTo",
"model": "User",
"foreignKey": "videotouserid"
}
},
3. Adding ACL
To define access control to the video object, I will use StrongLoop’s ACL tool. I want to create the following access
controls:
Deny everyone all endpoints, as the default behavior.
Allow everyone to view videos by adding a ‘READ’ permission.
Allow authenticated users to ‘EXECUTE.create’ videos.
Allow the video owner to edit and thus delete videos by adding a ‘WRITE’ permission.
$ slc loopback:acl
4/17
5. This results in the modification of the ‘~/common/models/video.json’ file, the acl generator will add the following
lines.
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "READ",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$authenticated",
"permission": "ALLOW",
"property": "create"
},
{
"accessType": "WRITE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
5/17
6. }],
Regenerate the Angular Services
With a new model added to our app, from the command-line re-run the ‘lb-ng’ command to add an Angular services
SDK for the models in the QAVideos app.
$ lb-ng server/server.js client/js/lb-ng-services.js
4. Add Video Functionality
Now, we are ready to add the model back into the web functionality and list all videos, list videos by user, add
videos, and edit videos.
Modify the ‘~/client/index.html’ template and add the following list items to the menu, under the logout list item.
Sign up
Log in
Log out
All Videos
My Videos
Add Video
6/17
8. The ‘Edit Video’ and ‘Delete Video’ functionality is added to each video listed in the ‘My Videos’ page.
Now add the matching pages, states and the video controllers. Modify the app.js Angular client and add the following
states. The ‘all-videos’ state was previously added, but you need to add a controller called ‘AllVideosController’, but
make sure to not create a duplicate definition of state.
.state('my-videos', {
url: '/my-videos',
templateUrl: 'views/my-videos.html',
controller: 'MyVideosController',
authenticate: true
})
.state('add-video', {
url: '/add-video',
templateUrl: 'views/video-form.html',
controller: 'AddVideoController',
authenticate: true
})
.state('edit-video', {
url: '/edit-video/:id',
templateUrl: 'views/video-form.html',
controller: 'EditVideoController',
authenticate: true
})
.state('delete-video', {
url: '/delete-video/:id',
controller: 'DeleteVideoController',
authenticate: true
})
The ‘all-videos’ state was previously already defined, but we need to add a controller object.
.state('all-videos', {
url: '/all-videos',
templateUrl: 'views/all-videos.html',
controller: 'AllVideosController',
authenticate: true
})
Note that in the ‘edit-video’ and ‘delete-video’ states, we also define the video.id in the ‘url’ property that is passed as
a parameter in the ‘edit-video’ and ‘delete-video’ calls as ‘:id’.
In the ‘~/client/views/’ directory add the following pages:
my-videos.html,
video-form.html, and
forbidden.html.
Edit the following views as follows:
all-videos.html
8/17
9. <section>
<article ng-repeat="v in videos.slice().reverse()">
<header>
<h1>{{v.title}}</h1>
</header>
<p>id: {{v.id}}</p>
<p>url: {{v.url}}</p>
<p>by: {{v.username}}</p>
<p>date published: {{v.date_published}}</p>
<p>likes: {{v.likes}}, dislikes {{v.dislikes}}</p>
</article>
</section>
my-videos.html
<section>
<article ng-repeat="v in videos.slice().reverse()">
<header>
<h1>{{v.title}}</h1>
</header>
<p>id: {{v.id}}</p>
<p>url: {{v.url}}</p>
<p>by: {{v.username}}</p>
<p>date: {{v.date_published}}</p>
<p>likes: {{v.likes}}, dislikes {{v.dislikes}}</p>
<div class="actions" ng-show="currentUser">
<button ui-sref="edit-video({ id: v.id })">Edit</button>
<button a ui-sref="delete-video{ id: v.id })">Delete</button>
</div>
</article>
</section>
Note that the video.id is passed as a parameter in the ‘edit-video’ and ‘delete-video’ function call.
video-form.html
<section>
<form name="form" ng-submit="submitVideo()">
<fieldset>
<legend>{{action}} Video Form</legend>
<div class="form-group">
<label>Title</label>
<input type="text" ng-model="video.title">
</div>
<div class="form-group">
<label>URL</label>
<input type="text" ng-model="video.url">
</div>
<div class="form-group">
9/17
10. <label>Username</label>
<input type="text" ng-model="video.username">
</div>
<div class="form-group">
<label>Date Published <i>(yyyy-mm-dd)</i></label>
<input type="text" ng-model="video.date_published">
</div>
<div class="form-group">
<label>Likes</label>
<input type="text" ng-model="video.likes">
</div>
<div class="form-group">
<label>Dislikes</label>
<input type="text" ng-model="video.dislikes">
</div>
<div class="actions">
<button>{{action}} video</button>
</div>
</fieldset>
</form>
<section>
forbidden.html
<section>
<article>
<header>
<h1>Forbidden</h1>
</header>
<p>An error occurred.</p>
</article>
</section>
To add the video controller, create a new file ‘~/client/js/controllers/video.js’. Add the ‘AllVideosController’,
‘MyVideosController’, and ‘AddVideoController’ in the ‘~/client/js/controllers/video.js’ file.
angular.module('app')
.controller('AllVideosController', ['$scope', 'Video',
function($scope, Video) {
$scope.videos = Video.find();
}
])
.controller('MyVideosController', ['$scope', 'Video', '$rootScope',
function($scope, Video, $rootScope) {
$scope.videos = Video.find({
filter: {
where: {
/** note: normally we would use just the built-in userId,
* but for the relational db we need to use the foreign key
10/17
11. 'uservideoid' explicitly
userId: $rootScope.currentUser.id
*/
videotouserid: $rootScope.currentUser.id
}
}
});
}
])
.controller('AddVideoController', ['$scope', 'Video', '$state', '$rootScope',
function($scope, Video, $state, $rootScope) {
$scope.action = 'Add';
$scope.video = {};
$scope.isDisabled = false;
$scope.submitVideo = function() {
Video
.create({
title: $scope.video.title,
url: $scope.video.url,
username: $scope.video.username,
date_published: $scope.video.date_published,
likes: $scope.video.likes,
dislikes: $scope.video.dislikes,
userId: $rootScope.currentUser.id,
videotouserid: $rootScope.currentUser.id
})
.$promise
.then(
// onsuccess
function() {
$state.go('all-videos');
},
// onerror
function(err){
}
);
};
}
])
;
Then, add the link to the new script in the ‘index.html’ file, right below the ‘js/controllers/auth.js’ script.
<script src="js/controllers/video.js"></script>
Also add the ‘EditVideoController’ and the ‘DeleteVideoController’ to the ‘video.js’ file.
11/17
12. .controller('DeleteVideoController', ['$scope', 'Video', '$state', '$stateParams',
function($scope, Video, $state, $stateParams) {
Video
.deleteById({ id: $stateParams.id })
.$promise
.then(function() {
$state.go('my-videos');
});
}
])
.controller('EditVideoController', ['$scope', '$q', 'Video', '$stateParams',
'$state',
function($scope, $q, Video, $stateParams, $state) {
$scope.action = 'Edit';
$scope.video = {};
$scope.isDisabled = true;
$q.all([
Video
.findById({ id: $stateParams.id })
.$promise
])
.then(function(data) {
$scope.video = data[0];
});
$scope.submitVideo = function() {
$scope.video
.$save()
.then(function(video) {
$state.go('all-videos');
},
function(err){
$state.go('forbidden');
});
};
}
])
Test to see if your configuration is running correctly, by running your application from the commandline using ‘node .’
and opening the ‘http://localhost:3000/explorer’ in your browser.
12/17
13. 5. Add Data Source
Instead of using an in-memory database, I want to use a PostGreSQL database for persisted storage, though you
can choose any other data storage supported by StrongLoop. Because we used the default in-memory database so
far, use and video information was lost each time we restarted or stopped the application.
Note: you must have chosen the ‘belongs to’ relation from Video to User in the ‘slc loopback:relation’ command of
the relation generator, cause the ‘has many’ relation at the time of writing this tutorial was not supported in the
automigrate tool.
From the command line, install the database connector, in this case a PostGreSQL connector.
$ npm install --save loopback-connector-postgresql
Install PostGres, either on your local machine or use a remote PostGres installation, and create a new database
‘<db_name>’. On Bluemix there is an ElephantSQL service and a Compose for PostGreSQL service, you can use.
13/17
14. Generate the data source.
$ slc loopback:datasource postgresdb
Don’t use a hyphen in your name, this is not allowed in StrongLoop. This process creates a new data source
reference in the ‘~/server/datasources.json’ file, with the default memory database and the newly configured
postgresdb connector.
14/17
15. {
"db": {
"name": "db",
"connector": "memory"
},
"postgresdb": {
"name": "postgresdb",
"connector": "postgresql",
"host": "db.elephantsql.com",
"port": "5432",
"database": "w",
"username": "w",
"password": "passw0rd"
}
}
Now modify the ‘~/server/model-config.json’ file and replace the ‘db’ value for the ‘dataSource’ properties on the
object models by the new ‘postgresdb’ dataSource.
"User": {
"dataSource": "postgresdb"
},
"AccessToken": {
"dataSource": "postgresdb",
"public": false
},
"ACL": {
"dataSource": "postgresdb",
"public": false
},
"RoleMapping": {
"dataSource": "postgresdb",
"public": false
},
"Role": {
"dataSource": "postgresdb",
"public": false
},
"Video": {
"dataSource": "postgresdb"
}
The last thing that remains to do now, is to use the ‘automigrate’ tool in StrongLoop to generate the tables that map
to our data model. Create a new directory ‘~/server/bin/’ and in it, add a new file ‘~/server/bin/automigrate.js’.
var app = require('../server');
var dataSource = app.dataSources.postgresdb;
dataSource.automigrate([
'User',
'AccessToken',
'ACL',
15/17
16. 'RoleMapping',
'Role',
'Video'
], function(err) {
if (err) throw err;
});
To run the automigrate script, execute the following command from the project root.
node server/bin/automigrate.js
Sometimes you get the following error” ‘too many connections for role’ because you are creating too many models at
once. It can help to comment out some models and run the automigrate for two models at a time. Rerun the tool,
commenting out the models that are already created, and uncomment the models to be created.
var app = require('../server');
var dataSource = app.dataSources.postgresdb;
dataSource.automigrate([
'User',
'AccessToken'/**,
'ACL',
'RoleMapping',
'Role',
'Video'*/
], function(err) {
if (err) throw err;
});
Check your PostGres installation to make sure the tables were created successfully.
16/17
17. Now, start your application again with the ‘node .’ command and in your browser go to http://0.0.0.0:3000.
Now if you sign up with a new user, the user is persisted to the PostGres database. Signing up with the same
username now will display the ‘Forbidden’ state.
To get the source code for QAVideos (part 2) go here.
In Part 3, we will finish the application and extend the built-in User model, add Songs and link the Songs to our
Videos, and add User Groups and Categories to Videos and Songs.
17/17