How to be a Full Stack Dev with LoopBack + JSPM + Angular2

Francesco Soncina aka phra

  • Bachelor degree in Computer Science
  • Full Stack Developer
  • DevOps
  • JavaScript Enthusiast

Abstract

  1. LoopBack
  2. Gulp
  3. PostCSS
  4. JSPM
  5. Angular2
  6. Demo

Biological Evolution

Mutation

Selection

Inheritance

Software Evolution

Feature

Popularity

Standard

LoopBack

The Node.js API Framework "

Features

  • Based on Express Framework
  • Based on Swagger API Framework
  • Authentication via JSON Web Tokens
  • Authorization via RBAC security model
  • Provides own ORM/ODM implementation
  • Support for database-agnostic relationships
  • Auto-generation of REST endpoints

Features

  • Support for custom methods
  • Support for static and dynamic roles
  • Permits focusing foremost on business login
  • Provides a Command Line Interface
  • Native SDKs for AngularJS, Android and iOS
  • AngularJS 2 support coming soon via swagger-codegen
  • Provides enterprise features and commercial support

Prerequisites

sudo npm i -g strongloop

Install StrongLoop

Create a new project

slc loopback

Models

slc loopback:model

Built-in models

  • PersistedModel

  • AccessToken

  • ACL

  • Relation

  • Role

  • User

  • Mail

  • File

RBAC

LoopBack is based on Role-Based Access Control

"acls": [
    {
      "principalType": "ROLE", "principalId": "admin",
      "accessType": "*", "permission": "ALLOW"
    },
    {
      "principalType": "ROLE", "principalId": "$owner",
      "accessType": "*","permission": "ALLOW"
    },
    {
      "principalType": "ROLE", "principalId": "$authenticated",
      "accessType": "READ", "property": "*"
      "permission": "ALLOW"
    },
    {
      "principalType": "ROLE", "principalId": "$everyone",
      "accessType": "*", "permission": "DENY"
    }
],

Built-in roles

  • $everyone
  • $unauthenticated
  • $authenticated
  • $owner

To qualify a $owner, the target model needs to have a belongsTo relation to the User model (or a model extends from User) and property matching the foreign key of the target model instance. The check for $owner is only performed for a remote method that has ':id' on the path, for example, GET /api/users/:id.''

Dynamic role definition

module.exports = function(app) {
  var Role = app.models.Role;
  Role.registerResolver('teamMember', function(role, context, cb) {
    if (context.modelName !== 'project') {
      return reject();
    }
    if (!context.accessToken.userId) {
      return cb(null, false); // do not allow anonymous users
    }
    context.model.findById(context.modelId, function(err, project) {
      if(err || !project) {
        cb(err);
      }
      app.models.Team.count({
        ownerId: project.ownerId,
        memberId: context.accessToken.userId
      }, function(err, count) {
        if (err) {
          return cb(err);
        }
        cb(null, count > 0); // true = is a team member
      });
    });
  });
};

ACLs

slc loopback:acl

Relations

slc loopback:relation
  • hasOne
  • hasMany
  • belongsTo
  • embedsOne
  • embedsMany
  • hasAndBelongsToMany

Hooks

var redis = require('redis');
var client = redis.createClient();

module.exports = function(accessToken) {
    accessToken.observe('after save',
            function updateTimestamp(ctx, next) {
                console.log('aftersave');
                client.expire('accessToken:'
                    + ctx.instance.id, 1209600);
                client.expire('i:accessToken:userId:' 
                    + ctx.instance.userId, 1209600);
                next();
            }
    );
};

Custom methods

module.exports = function(accessToken) {    
    accessToken.check = function(req, cb) {
        req.accessToken.userId =
            parseInt(req.accessToken.userId);
        req.accessToken.created =
            req.accessToken.updated = new Date();
        req.accessToken.save(cb);
    };

    accessToken.remoteMethod('check', {
        accepts: {
            arg: 'req',
            type: 'object',
            http: { source: 'req'},
        },
        http: {path:'/check', verb: 'put'},
    });
};

Boot scripts

slc loopback:boot-script

Boot scripts example

sync

async

module.exports = function(app, cb) {
  /*
   * The `app` object provides access to a variety of LoopBack resources such as
   * models (e.g. `app.models.YourModelName`) or data sources (e.g.
   * `app.datasources.YourDataSource`).
   * See http://docs.strongloop.com/display/public/LB/Working+with+LoopBack+objects
   * for more info.
   */
  process.nextTick(cb); // Remove if you pass `cb` to an async function yourself
};
module.exports = function(app) {
  /*
   * The `app` object provides access to a variety of LoopBack resources such as
   * models (e.g. `app.models.YourModelName`) or data sources (e.g.
   * `app.datasources.YourDataSource`). 
   * See http://docs.strongloop.com/display/public/LB/Working+with+LoopBack+objects
   * for more info.
   */
};

API Explorer

  • Based on SwaggerUI
  • Very useful for testing and debugging

API Explorer

Gulp

Grunt

Gulp

vs

  • Configuration over coding
  • Slower
  • Uses temporary files
  • Sequential execution
  • Coding over configuration
  • Faster
  • Uses vinyl virtual file system
  • Asynchronous streams

gruntfile.js

grunt.initConfig({
  sass: {
    dist: {
      files: [{
        cwd: 'app/styles', src: '**/*.scss',
        dest: '../.tmp/styles', expand: true, ext: '.css'
      }]
    }
  },
  autoprefixer: {
    options: ['last 1 version'],
      dist: {
        files: [{
          expand: true, cwd: '.tmp/styles',
          src: '{,*/}*.css', dest: 'dist/styles'
        }]
      }
  },
  watch: {
    styles: {
      files: ['app/styles/{,*/}*.scss'],
      tasks: ['sass:dist', 'autoprefixer:dist']
    }
  }}); grunt.registerTask('default', ['styles', 'watch']);
gulp.task('sass', function() {
  gulp.src('app/styles/**/*.scss')
    .pipe(sass())
    .pipe(autoprefixer('last 1 version'))
    .pipe(gulp.dest('dist/styles'));
});
gulp.task('default', function() {
  gulp.run('sass');
  gulp.watch('app/styles/**/*.scss', function() {
    gulp.run('sass');
  });
});

gulpfile.js

PostCSS

A tool for transforming CSS with JavaScript "

Features

  • Written in JavaScript
  • By default it does nothing, yay!
  • Everything is a plugin
  • It parses your CSS in an AST...
  • ... and it executes plugins to modify the AST...
  • ... finally it stringifies the AST in a brand new CSS file
  • It can do stuff that is impossible with preprocessors
  • In fact it is a transpiler
  • Support for SASS, LESS and Stylus syntaxes via plugins

Popular plugins

  • autoprefixer
  • cssnext
  • cssnano
  • precss
  • colorblind
  • doiuse
  • cssgrace
  • rtlcss
  • and many others!

JSPM

Frictionless browser

package management"

Features

  • Aims to supersede Bower, Browserify, Webpack
  • Native support for NPM registry and GitHub
  • Loads ES6, AMD, CommonJS and globals modules
  • Based on ES6 Module Loader syntax
  • Based on SystemJS

Features

  • Support loading of css, json, sass, text, etc via plugins
  • Native support for transpiling and minification
  • Can serve each files individually (ideal for development)
  • Can bundle each files into one file (ideal for production)
  • Can bundle into different chunks (ideal for large SPAs)
  • jspm.io provides a CDN over HTTP/2 for dependencies loading

Usage

<!-- index.html -->
<html>
  <head>
    <title>Angular 2 JSPM Seed</title>
    <base href="./">

    <!--removeIf(production)-->
    <script src="jspm/system.js"></script>
    <script src="jspm-config/config.js"></script>
    <script>
      System.import('src/boot')
        .then(null, console.error.bind(console));
    </script>
    <!--endRemoveIf(production)-->
    <!--removeIf(development)-->
    <script defer src="app.min.js"></script>
    <!--endRemoveIf(development)-->
  </head>
  <body>
    <my-app>Loading...</my-app>
  </body>
</html>

Example

We want to include Bootstrap in our project.

bower install --save bootstrap

Now in our index.html:

<link
    href="bower_components/dist/css/bootstrap.css">
<script
    src="bower_components/dist/js/bootstrap.js">
</script>

Example

We want to include Bootstrap in our project.

jspm install bootstrap

Now in our boot script:

import 'bootstrap/dist/css/bootstrap.css!';
import 'bootstrap/dist/js/bootstrap';

Bootstrap is successfully included, yay!

Angular 2

One framework.
Mobile and desktop. "

Features

  • Entirely rewritten from scratch
  • Written in TypeScript
  • Supports JavaScript, TypeScript and Dart languages
  • Shorter learning curve than AngularJS 1
  • Everything is an ES6 class!
  • Pure Dependency Injection / Inversion of Control pattern

Features

  • No more $scope.$apply(), yay!
  • No more .service(), .factory() and .provider(), yay!
  • Supports encapsulation (emulated / shadow DOM)
  • Encourages use of modular components
  • Encourages use of module loaders
  • Encourages use of transpilers
  • Based on Zone.js
  • Based on Observables

Components

// login.ts
import {Component, Inject} from 'angular2/core';
import {User} from '../../services/user';
import {UserApi} from '../../lib/lb-services';

@Component({
    selector: 'login',
    templateUrl: 'src/components/login/login.html',
    styleUrls: ['src/components/login/login.css'],
    providers: [UserApi]
})

...

Components

// login.ts

...

export class Login {
    private email: string;
    private password: string;
    constructor(@Inject(User) public user: User, 
                @Inject(UserApi) public userApi: UserApi) {
    }
    public login() {
        this.userApi.login({email: this.email, password: this.password}).subscribe(
            (response: any) => { this.user.user = response.user; },
            (error: any) => { this.user.clearUser();
                                console.error('login KO', error); },
            () => { console.log('Login COMPLETE', this.user); }
        );
    }
    public logout() {
        this.userApi.logout().subscribe(
            (response: any) => { this.user.clearUser(); },
            (error: any) => { this.user.clearUser();
                                console.log('Logout KO'); },
            () => { console.log('Logout COMPLETE'); }
        );
    }
}

Templates

<!-- login.html -->
<input *ngIf="!user.user" 
        [(ngModel)]="email" 
        placeholder="Email">
<input *ngIf="!user.user"
        [(ngModel)]="password"
        placeholder="Password">
<button *ngIf="!user.user"
        (click)="login()">LOGIN</button>
<button *ngIf="user.user"
        (click)="logout()">LOGOUT</button>
<p *ngIf="user.user">
    Welcome {{user.user.nome}} {{user.user.cognome}}!
</p>

Templates

<!-- items.html -->
<input type="button" value="Load items" (click)="getItems()">
<p>{{ text }}</p>
<table class="table" *ngIf="items.length">
    <thead><tr><td>ID</td><td>NAME</td><td>DESC</td></tr></thead>
    <tbody>
        <tr *ngFor="#item of items">
            <td>
                {{ item.id }}
            </td>
            <td>
                {{ item.name }}
            </td>
            <td>
                {{ item.desc }}
            </td>
        </tr>
    </tbody>
</table>

Providers

import {Injectable} from 'angular2/core';

@Injectable()
export class User {
    private _user: any;

    constructor() {
    }

    set user(user: any) {
        this._user = user;
    }

    get user() {
        return this._user;
    }

    clearUser() {
        this._user = undefined;
    }
}

Inputs

// login.ts
export class Login {
    @Input() input: string;

    printInput() {
        console.log(this.input);
    }
}
<!-- header.html -->
...
<login [input]="parameter"></login>
...

Outputs

// login.ts
export class Login {
    @Output() logged: EventEmitter<any>
        = new EventEmitter();
}
<!-- header.html -->
...
<login (logged)="loggedin($event)">
</login>
...

Testability

  • Everything is an ES6 class
  • We do not have to bootstrap the angular application
  • We just import the class and instantiate it...
  • ... mocking the dependencies of the constructor

Now we are ready to unit test our code!

Demo

Available at this URL:

Question time

LoopBack, JSPM, Angular2

By Francesco Soncina

LoopBack, JSPM, Angular2

  • 989