LoopBack+Ionic
CI/CD Bitbucket+Shippable+Heroku
Hello, I am Juan
@jpizarrom
https://github.com/jpizarrom
Loyalty Rewards Club Sample app
- auth
- add points
- use points
- balance
App
- Rest API with LoopBack
- Front-end with Ionic
- Testing
- CI/CD Shippable
- Deploy a Heroku
Project Structure
.
├── app
│ ├── index.html
│ └── js
│ ├── app.js
│ └── modules
├── bower.json
├── common
│ └── models
│ ├── shop.js
│ ├── shop.json
│ ├── transaction.js
│ └── transaction.json
├── config.xml
├── coverage
├── gulpfile.js
├── ionic.project
├── package.json
├── Procfile
├── README.md
├── scss
│ └── ionic.app.scss
├── server
│ ├── boot
│ │ ├── authentication.js
│ │ ├── explorer.js
│ │ ├── push.js
│ │ ├── rest-api.js
│ │ ├── role-resolver.js
│ │ ├── root.js
│ │ └── routes.js
│ ├── config.json
│ ├── config.production.js
│ ├── datasources.json
│ ├── datasources.production.js
│ ├── lib
│ │ └── loopback-component-passport.js
│ ├── middleware.json
│ ├── model-config.json
│ ├── models
│ │ ├── user.js
│ │ └── user.json
│ ├── providers.js
│ ├── push-demo.js
│ ├── server.js
│ └── views
│ ├── password-reset.ejs
│ ├── password-reset-request.ejs
│ ├── response.ejs
│ ├── verified.ejs
│ └── verify.ejs
├── shippable.yml
├── slack_notifier.py
├── templates
│ ├── credit.ejs
│ └── debit.ejs
└── test
├── api
│ └── unit
└── client
├── controllers
├── e2e
├── example-spec.js
├── karma.mocha.conf.js
├── karma.ng-scenario.conf.js
├── lib
├── protractor.conf.js
└── services
.
├── index.html
└── js
├── app.js
└── modules
├── common
│ └── lb-services.js
├── home
│ ├── home.controllers.js
│ ├── home.module.js
│ └── templates
│ └── home.template.html
├── profile
│ ├── profile.controllers.js
│ ├── profile.directives.js
│ ├── profile.module.js
│ ├── profile.services.js
│ └── templates
│ └── login.html
└── shop
├── shop.controllers.js
├── shop.directives.js
├── shop.module.js
└── templates
├── balance.directive.template.html
├── balance.html
├── credit.directive.template.html
├── credit.html
├── debit.directive.template.html
├── debit.html
└── mybalance.html
Data Sources
{
"db": {
"name": "db",
"connector": "mongodb",
"database": "foodpoints"
},
"mail": {
"name": "mail",
"defaultForType": "mail",
"connector": "mail",
"transports": [
{
"type": "smtp",
"host": "smtp.mandrillapp.com",
"port": 587,
"auth": {
"user": "Your-server-api-key",
"pass": "Your-server-api-key"
}
}
]
}
}
exports = {
"db" : {
"url": process.env.MONGOLAB_URI,
"name": "db",
"connector": "mongodb",
}
}
mail_provider = process.env.MAIL_PROVIDER || 'mandrill';
if (mail_provider=='mandrill'){
console.log(mail_provider);
exports["mail"]= {
"name": "mail",
"defaultForType": "mail",
"connector": "mail",
"transports": [
{
"type": "smtp",
"host": "smtp.mandrillapp.com",
"port": 587,
"auth": {
"user": process.env.MANDRILL_USERNAME,
"pass": process.env.MANDRILL_APIKEY
}
}
]
};
} else if (mail_provider=='mailgun') {
console.log(mail_provider);
exports["mail"]= {
"name": "mail",
"defaultForType": "mail",
"connector": "mail",
"transports": [
{
"type": "smtp",
"host": process.env.MAILGUN_SMTP_SERVER,
"port": process.env.MAILGUN_SMTP_PORT,
"auth": {
"user": process.env.MAILGUN_SMTP_LOGIN,
"pass": process.env.MAILGUN_SMTP_PASSWORD
}
}
]
};
} else if (mail_provider=='postmark') {
console.log(mail_provider);
exports["mail"]= {
"name": "mail",
"defaultForType": "mail",
"connector": "mail",
"transports": [
{
"type": "smtp",
"host": process.env.POSTMARK_SMTP_SERVER,
"port": 25,
"auth": {
"user": process.env.POSTMARK_API_TOKEN,
"pass": process.env.POSTMARK_API_TOKEN
}
}
]
};
} else if (mail_provider=='sendgrid') {
console.log(mail_provider);
exports["mail"]= {
"name": "mail",
"defaultForType": "mail",
"connector": "mail",
"transports": [
{
"type": "smtp",
"host": "smtp.sendgrid.net",
"port": 587,
"auth": {
"user": process.env.SENDGRID_USERNAME,
"pass": process.env.SENDGRID_PASSWORD
}
}
]
};
}
module.exports = exports;
Model
{
"name": "shop",
"plural": "shops",
"base": "PersistedModel",
"strict": false,
"idInjection": false,
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
},
"created": {
"type": "date"
},
"modified": {
"type": "date"
}
},
"validations": [],
"relations": {},
"acls": [],
"methods": []
}
Model Relations
{
"name": "user",
"plural": "users",
"base": "User",
"properties": {},
"validations": [],
"relations": {
"shops": {
"type": "hasMany",
"model": "shop",
"foreignKey": "ownerId"
},
"transactions": {
"type": "hasMany",
"model": "transaction",
"foreignKey": "userId"
}
},
"acls": [],
"methods": []
}
{
"name": "shop",
"plural": "shops",
"base": "PersistedModel",
"strict": false,
"idInjection": false,
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
},
"created": {
"type": "date"
},
"modified": {
"type": "date"
}
},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "ownerId"
},
"transactions": {
"type": "hasMany",
"model": "transaction",
"foreignKey": "shopId"
}
},
"acls": [],
"methods": []
}
Users, Roles, and ACL's
{
"name": "user",
"plural": "users",
"base": "User",
"properties": {},
"validations": [],
"relations": {
"shops": {
"type": "hasMany",
"model": "shop",
"foreignKey": "ownerId"
},
"transactions": {
"type": "hasMany",
"model": "transaction",
"foreignKey": "userId"
}
},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW"
},
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY",
"property": "create"
},
{
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"property": "create"
},
{
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "__get__shops"
},
{
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "balance"
}
],
"methods": []
}
{
"name": "shop",
"plural": "shops",
"base": "PersistedModel",
"strict": false,
"idInjection": false,
"properties": {
"name": {
"type": "string",
"required": true
},
"description": {
"type": "string"
},
"created": {
"type": "date"
},
"modified": {
"type": "date"
}
},
"validations": [],
"relations": {
"user": {
"type": "belongsTo",
"model": "user",
"foreignKey": "ownerId"
},
"transactions": {
"type": "hasMany",
"model": "transaction",
"foreignKey": "shopId"
}
},
"acls": [
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "credit"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "debit"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "shopOwner",
"permission": "ALLOW",
"property": "balance"
}
],
"methods": []
}
Extending Models
Shop.balance = function(email, cb) {
var Model = this;
var User = Model.app.models.user;
var Transaction = Model.app.models.transaction;
var query = {};
query.email = email;
User.findOne({where: query}, function(err, user) {
if (err || !user) return cb(err,0);
Transaction.find({where: {userId: user.id}}, function(err, items) {
var total = items
.map(function(item) {
return item.amount;
})
.reduce(function(cur, prev) {
return prev + cur;
}, 0);
cb(null, total);
});
});
}
Shop.remoteMethod('balance', {
accepts: [
{arg: 'email', type: 'string', required: true},
],
returns: {arg: 'amount', type: 'number'},
http: [
{path:'/balance', verb: 'post'}
]
});
API Explorer
AngularJS JavaScript SDK
lb-ng
/**
* @ngdoc method
* @name lbServices.Shop#debit
* @methodOf lbServices.Shop
*
* @description
*
* <em>
* (The remote method definition does not provide any description.)
* </em>
*
* @param {Object=} parameters Request parameters.
*
* This method does not accept any parameters.
* Supply an empty object or omit this argument altogether.
*
* @param {Object} postData Request data.
*
* - `id` – `{*}` -
*
* - `email` – `{string}` -
*
* - `amount` – `{number}` -
*
* @param {function(Object,Object)=} successCb
* Success callback with two arguments: `value`, `responseHeaders`.
*
* @param {function(Object)=} errorCb Error callback with one argument:
* `httpResponse`.
*
* @returns {Object} An empty reference that will be
* populated with the actual data once the response is returned
* from the server.
*
* Data properties:
*
* - `success` – `{boolean=}` -
*/
"debit": {
url: urlBase + "/shops/:id/debit",
method: "POST"
},
Ionic
<ion-view title="FoodPoints">
<ion-content has-header="false" class="home-view">
<a class="button button-full button-outline" ui-sref="mybalance" ng-show="showMyBalance()">
Mis puntos
</a>
<a class="button button-full button-outline" ui-sref="credit" ng-show="showCredit()">
Acumular
</a>
<a class="button button-full button-outline" ui-sref="debit" ng-show="showDebit()">
Canjear
</a>
<a class="button button-full button-outline" ui-sref="balance" ng-show="showBalance()">
Consultar puntos
</a>
<a class="button button-full button-outline" ui-sref="login" ng-show="showLogin()">
Login
</a>
<button lb-logout>Logout</button>
</ion-content>
</ion-view>
Home.controller('HomeController', [
'$scope',
'$state',
'ProfileService',
function ($scope, $state, ProfileService) {
$scope.viewTitle = 'FoodPoints';
$scope.showMyBalance = function () {
if (ProfileService.getCurrentUserId() && !ProfileService.getCurrentShopId()) {
return true;
}
return false;
};
<ion-view title="Acumular">
<ion-content has-header="true" scroll="true">
<div class="card">
<div lb-credit-directive ng-controller="CreditController"></div>
</div>
</ion-content>
</ion-view>
Shop.directive('lbCreditDirective', [
'$templateCache',
function ($templateCache) {
return {
templateUrl: 'js/modules/shop/templates/credit.directive.template.html',
controller: function ($scope) {
// $scope.viewTitle = 'Search';
}
};
}
]);
<view>
<content has-header="true" scroll="true">
<div class="item item-divider assertive" ng-if="creditSuccess">
Credit!
</div>
<div class="item item-divider assertive" ng-if="creditError">
Could not Credit!
</div>
<form class="item item-text-wrap" ng-submit="submit()">
<div class="list">
<label class="item item-input">
<input type="email" placeholder="Email" ng-model="credentials.email">
</label>
<label class="item item-input">
<input type="number" placeholder="Amount" ng-model="credentials.amount">
</label>
<button class="button button-block button-positive">
Acumular
</button>
</div>
</form>
</content>
</view>
Shop.controller('CreditController', [
'$scope',
'$state',
'Shop',
'ProfileService',
'$ionicHistory',
function ($scope, $state, Shop, ProfileService, $ionicHistory) {
$scope.viewTitle = 'CreditController';
$scope.credentials = {
};
if (ProfileService.getCurrentUserId())
$scope.credentials.creatorId = ProfileService.getCurrentUserId();
else {
$ionicHistory.nextViewOptions({
disableBack: true
});
$state.go('login');
}
if (ProfileService.getCurrentShopId())
$scope.credentials.id = ProfileService.getCurrentShopId();
else {
$ionicHistory.nextViewOptions({
disableBack: true
});
$state.go('login');
}
$scope.submit = function () {
$scope.creditError = null;
$scope.creditSuccess = null;
$scope.creditResult = Shop.credit($scope.credentials,
function (value, responseHeaders) {
if(value.success){
delete $scope.credentials.email;
delete $scope.credentials.amount;
$scope.creditSuccess = true;
}else{
$scope.creditError = true;
}
},
function (res) {
$scope.creditError = res.data.error;
}
);
}
}
]);
Auth
Profile.controller('LoginController', [
'$scope',
'$state',
'User',
function ($scope, $state, User) {
$scope.credentials = {
};
$scope.login = function () {
$scope.loginResult = User.login($scope.credentials,
function () {
window.localStorage.setItem('currentUserId', $scope.loginResult.userId);
window.localStorage.setItem('accessToken', $scope.loginResult.id);
User.shops(
{ id: $scope.loginResult.userId },
function(list) {
if (list && list.length>0)
window.localStorage.setItem('shopId', list[0].id);
},
function(errorResponse) { /* error */ }
$state.go('home');
},
function (res) {
$scope.loginError = res.data.error;
}
);
};
}
]);
if (ProfileService.getCurrentUserId())
$scope.credentials.creatorId = ProfileService.getCurrentUserId();
else {
$ionicHistory.nextViewOptions({
disableBack: true
});
$state.go('login');
}
Testing
var lt = require('loopback-testing');
var assert = require('assert');
var app = require('../../../server/server.js'); //path to app.js or server.js
describe('/api/v1/shops/{id}/credit error', function() {
lt.beforeEach.withApp(app);
lt.beforeEach.givenLoggedInUser({id:'1a', email: 'foo2@bar.com',password:"password", "emailVerified": true});
lt.beforeEach.givenModel('shop', {id:'1a', name:'name'});
lt.describe.whenCalledRemotely('POST', '/api/v1/shops/1a/credit', {email: 'foo3@bar.com',password:"password"}, function() {
lt.it.shouldBeDenied();
});
});
describe('/api/v1/shops/{id}/credit ok', function() {
lt.beforeEach.withApp(app);
lt.beforeEach.givenLoggedInUser({id:'1a', email: 'foo2@bar.com',password:"password", "emailVerified": true});
lt.beforeEach.givenModel('shop', {id:'1a', name:'name', ownerId:'1a'});
lt.describe.whenCalledRemotely('POST', '/api/v1/shops/1a/credit', {email: 'foo3@bar.com',amount: 10}, function() {
lt.it.shouldBeAllowed();
it('should', function() {
console.log(this.res.body);
assert.equal('success' in this.res.body, true);
assert.equal(this.res.body['success'], true);
});
});
});
describe('Controllers', function(){
// load the controller's module
// beforeEach(module('app'));
beforeEach(angular.mock.module('app'));
// tests start here
it('should have a Home module', function() {
expect(Home).to.not.be.undefined;
});
it('should have a properly working HomeController controller',
inject(function($rootScope, $controller, $httpBackend) {
window.localStorage.removeItem('currentUserId');
window.localStorage.removeItem('shopId');
var $scope = $rootScope.$new();
var ctrl = $controller('HomeController', {
$scope : $scope,
});
expect($scope.viewTitle).to.equal('FoodPoints');
expect($scope.showMyBalance()).to.equal(false);
expect($scope.showCredit()).to.equal(false);
expect($scope.showDebit()).to.equal(false);
expect($scope.showBalance()).to.equal(false);
expect($scope.showLogin()).to.equal(true);
expect($scope.showRegister()).to.equal(true);
}));
describe('Services', function(){
beforeEach(angular.mock.module('app'));
it('can get an instance of my factory', inject(function(ProfileService) {
expect(ProfileService).to.not.be.undefined;
}));
it('not logged', inject(function(ProfileService) {
window.localStorage.removeItem('currentUserId');
window.localStorage.removeItem('shopId');
expect(ProfileService.getCurrentUserId()).to.equal(null);
expect(ProfileService.getCurrentShopId()).to.equal(null);
}));
it('user', inject(function(ProfileService) {
window.localStorage.removeItem('currentUserId');
window.localStorage.removeItem('shopId');
window.localStorage.setItem('currentUserId', 'currentUserId');
expect(ProfileService.getCurrentUserId()).to.equal('currentUserId');
expect(ProfileService.getCurrentShopId()).to.equal(null);
}));
it('owner', inject(function(ProfileService) {
window.localStorage.removeItem('currentUserId');
window.localStorage.removeItem('shopId');
window.localStorage.setItem('currentUserId', 'currentUserId');
window.localStorage.setItem('shopId', 'shopId');
expect(ProfileService.getCurrentUserId()).to.equal('currentUserId');
expect(ProfileService.getCurrentShopId()).to.equal('shopId');
}));
});
describe('login', function() {
beforeEach(function() {
browser.get('/');
});
it('login ok', function() {
// test user
expect(browser.getTitle()).toEqual('FoodPoints');
expect(element(by.css('[ui-sref="login"]')).getText()).toBe('Login');
element(by.css('[ui-sref="login"]')).click();
expect(browser.getTitle()).toEqual('Login');
element(by.model('credentials.email')).sendKeys('test.user@foodpoints.cl');
element(by.model('credentials.password')).sendKeys('dddddd');
expect(element(by.css('.button.button-block.button-positive')).getText()).toBe('Login');
element(by.css('.button.button-block.button-positive')).click();
expect(browser.getTitle()).toEqual('FoodPoints');
expect(element(by.css('[ui-sref="mybalance"]')).getText()).toBe('Mis puntos');
expect(element(by.css('[ng-click="logout()"]')).getText()).toBe('logout');
element(by.css('[ng-click="logout()"]')).click();
// test owner
expect(browser.getTitle()).toEqual('FoodPoints');
expect(element(by.css('[ui-sref="login"]')).getText()).toBe('Login');
element(by.css('[ui-sref="login"]')).click();
expect(browser.getTitle()).toEqual('Login');
element(by.model('credentials.email')).sendKeys('test.restaurant@foodpoints.cl');
element(by.model('credentials.password')).sendKeys('ddddd');
expect(element(by.css('.button.button-block.button-positive')).getText()).toBe('Login');
element(by.css('.button.button-block.button-positive')).click();
expect(browser.getTitle()).toEqual('FoodPoints');
expect(element(by.css('[ui-sref="credit"]')).getText()).toBe('Acumular');
expect(element(by.css('[ng-click="logout()"]')).getText()).toBe('logout');
element(by.css('[ng-click="logout()"]')).click();
});
});
CI/CD
Shippable+Heroku
language: node_js
node_js:
- 0.10.38
addons:
firefox: "23.0"
services:
- mongodb
- selenium
env:
global:
- MONGOLAB_URI=mongodb://localhost/test
- APP_UUID=aaaaaa
- SLACK_ORG=sssssss
- secure: QGLe0HJ0BXuAWKmjrPWLeeabGAuRGU7YuWM+nW5reb7r5JzEYGUsje5YHlSyigWd/X7cvb/9eG9yTnqS9FRN+WoQABy6X54vcaXpsl8am6ov+qpQL7KhfNduNdQKryaECcV1Jh2rlS3aTe8Dewjo65YJE5M+zhKV5OtJamMSU3DcbMg24vpCH/2dLJnZK9HY1aa9mBpG2jg/iymIjalJyZTCUBwswOLtoFazwMJ9Hj0f0a0Na5oW7ebsN+nK7m62uGD6WxMmwRVKihJcvDtZ/TE87e8weaZ0OcdFl23fvNJapEtaoWcgLrYr0oGHxfqJuODS2nVALVNZGmSKYKmPSA==
- secure: eKxBK5oAkHOcXccAOjt07tZ5A46NjsOT7MjslmwvBFB0Q7EiW5dS+GotH97tF5xBiRku0amMhm0HFAeHDgbbN+hdksrkG8OHsyAYY38Wsp1/wx7VbstxSVFrep5osfCfc9VKitB2yGbdYGJisC1XuZEr1MXpI/+WGKyDU7FQtcy0iAbDiPI1VIk3VymJh58PHGObQyOWjVStVJV9J7cGNofiYajmmqi1SJupIOoTaSM1kkBF6IWe2YII2TEqPywCynsvUDWUOLkKx4gjYfwe1SVTnnDfT+W4c2th9dDvzXhIF8jeVk4ijOZgyxWhiZXHQheyVibHczapVKAiDyD8Aw==
- secure: Fq/OEC43n9NM+fHYjgKmdytBxFckij4eh86ybG9h7TkkgfuI3OTAAnImLHQwBXsusbBlR3TyXM/Q5nE6anXDADNXS7Mx0hsuGd4Xk7hyt6xzajBzVjeIfEPjNbXh8R7xqpJszSmYREzrgjzkFUYfve7Bib7xrlyg2MDFrmPWh7iPwV4CfazWYl4a6PNAx80LOKLCQPmuOQGUGzKrvSbdhIPxFTigjMFvuOatzlJGRpSsdKBagz0Fvato9s0U7S0EX1Sx2TdFWsVYFer2DsLcUgLVyd8zEXDam/nCW7B0IxDVyVTh7BhVhtq0yPLb20bMcyWOa1DQ0kuSHf98EyRP7w==
# MONGOLAB_URI
- secure: ViN0QbUxxjWIzzM5iiB8pgEySMryOQITYjCjpzCsQP69AYguSCozxSI38+1bDlgDpNE3+5xIOindG9W3gI6h0gnNwFswfUdpDJ6Jun+vaS7+IgERiT9pvkOtKRkWHO2qmuTT5uXV57mJVuSuWCirGgTXuig0MATx90id7urapyPAVPkWAU49xFlAlj2mlFRbZkzptRAu6dgYyH6kQ2Y1SadftL9fapjKrk/+RlR86+UMcZxvyTG6ZyX7BJZv+o+rjeyard+zURxcW8dPcQg5uSqwDDmC+Ynqf4OpPtLuFDrUXLRhUkTkUugDp9YG8G+e6M+iy4KI1KBwaIMgeChC7g==
notifications:
email:
recipients:
- jpizarrom@gmail.com
on_success: change
on_failure: always
before_install:
- . $HOME/.rvm/scripts/rvm && rvm use ruby-2.0.0-p598 --default && ruby --version
- which heroku || wget -qO- https://toolbelt.heroku.com/install-ubuntu.sh | sh
- APP_NAME=`echo $APP_UUID-$BRANCH | cut -c 1-30`
- npm -g install npm@2.1.1
before_script:
- mkdir -p shippable/testresults
- mkdir -p shippable/codecoverage
- "export DISPLAY=:99.0"
- "/etc/init.d/xvfb start"
script:
- ./node_modules/loopback-testing/node_modules/.bin/mocha test/api/unit/ -R xunit
- ./node_modules/.bin/gulp test
# - ./node_modules/.bin/protractor --version
# - ./node_modules/.bin/protractor --baseUrl='http://app.foodpoints.cl' test/client/protractor.conf.js
- ./node_modules/.bin/lb-ng server/server.js ./app/js/modules/common/lb-services.js -u https://$APP_NAME.herokuapp.com/api/v1
- ./node_modules/.bin/gulp
# after_script:
# - "/etc/init.d/xvfb stop"
# - ./node_modules/.bin/istanbul cover test/client/protractor.conf.js -- -u tdd
# - ./node_modules/.bin/istanbul report cobertura --dir shippable/codecoverage/
after_failure:
- python slack_notifier.py --project $APP_NAME --org $SLACK_ORG --token $SLACK_TOKEN
after_success:
- python slack_notifier.py --project $APP_NAME.herokuapp.com --org $SLACK_ORG --token $SLACK_TOKEN -s
- test -f ~/.ssh/id_rsa.heroku || (ssh-keygen -y -f ~/.ssh/id_rsa > ~/.ssh/id_rsa.heroku && heroku keys:add ~/.ssh/id_rsa.heroku)
- heroku apps | grep ^$APP_NAME || ( (heroku create $APP_NAME -n && heroku addons:add mandrill --app $APP_NAME) || (heroku addons:add mandrill --app $APP_NAME))
- heroku addons --app $APP_NAME | grep deployhooks || heroku addons:add deployhooks:http --url=https://$SLACK_ORG.slack.com/services/hooks/heroku?token=$SLACK_HEROKU_TOKEN --app $APP_NAME
- heroku config:get MAIL_HOST --app $APP_NAME | grep . || heroku config:set MAIL_HOST=$APP_NAME.herokuapp.com --app $APP_NAME
# - >
# [[ $BRANCH != 'master' ]] && MONGOLAB_URI=`heroku config:get MONGOLAB_URI --app $APP_UUID-master`
- >
[[ $BRANCH != 'master' ]] && heroku config:set MONGOLAB_URI=$MONGOLAB_URI --app $APP_NAME || echo "ok"
# - git remote -v | grep ^heroku | grep ^$APP_NAME || heroku git:remote --ssh-git --app $APP_NAME
- git checkout -b deploy
- git add . -A
- git commit -m "-"
- git push git@heroku.com:$APP_NAME.git deploy:master -f
# - git push -f heroku deploy:master
- ./node_modules/.bin/protractor --baseUrl="https://$APP_NAME.herokuapp.com" test/client/protractor.conf.js
- "/etc/init.d/xvfb stop"
# - ./node_modules/.bin/istanbul cover test/client/protractor.conf.js -- -u tdd
# - ./node_modules/.bin/istanbul report cobertura --dir shippable/codecoverage/
Questions
References
- http://loopback.io/
- http://ionicframework.com/
- http://www.shippable.com/
- http://www.heroku.com/
- https://bitbucket.org/
Loopback-ionic
By Juan Pizarro
Loopback-ionic
- 2,710