Do More With Less

Full Stack TypeScript

Doguhan Uluca

Questions? @duluca

Bluetooth Speaker

Give Away

  1. Follow @duluca on Twitter
  2. Tweet including @duluca #TypeScript @ExcellaCo
  3. Stick to the end and win!

Do More With Less

Full Stack TypeScript

Doguhan Uluca

Questions? @duluca

What is in a name?

name

stack

ease of use

happiness

effectiveness

stack is key

Presentation

API

Business

Persistence

Best Practices

IDE

Patterns

Libraries

TESTING

Full Stack Nirvana

Minimal MEAN

Angular && Angular Material

Express

Node

Native Mongo Driver

VS Code

npm

GitHub

jasmine && protractor

gulp

TypeScript

CI

Angular

Web

Mobile Web

Native Mobile

Native

Desktop

Server-Side

Progressive

Angular

Angular Core

Web

Ionic

Universal

React

Electron

View

View Model

Controller / Router

Services ($http, redux, logic)

Best Practices

IDE

Patterns

Libraries

TESTING

Angular

app.ts

rootRouter

services

filters

directives

/a: default

/master

/detail

/b/...

/c

childRouter

/d

/e

/f

Angular Material

Angular Material

High quality components

CSS Flexbox

You won't have to write CSS

Dynamic forms

Express

Fast, unopinionated, minimalist web framework for Node.js

Node

Lightweight and efficient JavaScript runtime, using an event-driven, non-blocking I/O model

Mongo

Document-oriented database with dynamic JSON-like schemas

Native Mongo Driver

Official Node.js APIs allowing access to all native functionality

webpack

Build script or task automation system

If you don't ship

It never happened

CodeShip.io

Continuous Integration

Continuous Delivery

Cloud Scalability

If you need to collaborate

You must behave differently

spaghetti(teamWork)

GrossException Thrown

Bugs 

Issues

Discussions

Not just about code

New features 

Code Reviews

Status Updates

GitHub

Waffle.io

GitHub for Desktop

For more details see my blog

You're (Probably) Using GitHub Wrong

on

http://blog.excella.com

Code

Goals

Angular Setup

Server Setup

DocumentTS

Use of async/await

Docker Setup

Database Connectivity

Heroku

DocumentTS

import { DocumentException, serialize } from 'document-ts'
import * as _ from 'lodash'

export interface IAddress
{
   addressLine1: string,  city: string,  state: string,
   zip: string,  addressLine2?: string, country: string
}

export class Address implements IAddress {
  constructor(public addressLine1 = '', public city = '', public state = '',
              public zip: string, public addressLine2?: string, public country = 'USA') {
    let valid = validateAddress(this)

    if(!valid) {
      throw new DocumentException('Invalid address')
    }
  }

  static build(address?: IAddress) {
    if(address) {
      return new Address(address.addressLine1, address.city, address.state, address.zip, address.addressLine2,
        address.country)
    }
    return undefined
  }

  get fullAddress(){
    var fullAddress = `${this.addressLine1}, `
    if(this.addressLine2){
        fullAddress += `${this.addressLine2}, `
    }
    fullAddress += `${this.city}, ${this.state}, ${this.zip}`
    return fullAddress
  }

  get localAddress(){
    return `${this.addressLine1}, ${this.city}, ${this.state}`
  }

  toJSON() {
    let keys = Object.keys(this).concat(['fullAddress', 'localAddress'])
    return serialize(this, keys)
  }
}

function validateZip(zipCode){
  var zipCodeRegexp = /^\d{5}$/;
  var result = zipCodeRegexp.test(zipCode);
  return result;
}

function validateAddress(address){
  address.zip = address.zip.toString();
  return validateZip(address.zip);
}

DocumentTS

export interface IQueryParameters {
  filter?: string,
  skip?: number,
  limit?: number,
  sortKeyOrList?: string | Object[] | Object
}

export interface ICollectionProvider {
  (): mongodb.Collection
}

export interface IDocument {
  _id: mongodb.ObjectID,
  collectionName: string
}

export interface IPaginationResult<TDocument extends IDocument> {
  data: TDocument[],
  total: number
}

DocumentTS

'use strict'

import { Document, IDocument, DocumentException, CollectionFactory } from 'document-ts'
import { ObjectID, AggregationCursor } from 'mongodb'

var uuid = require('node-uuid')
var bcrypt = require('bcryptjs')
var owasp = require('owasp-password-strength-test')
var AccountStatus = require('../../shared/constants/accountStatus')
var BusinessStatus = require('../../shared/constants/businessStatus')
var roles = require('../../shared/constants/roles')
var emailVerify = require('../services/emailVerify')
import * as _ from 'lodash'

import { IAddress } from './address'
import { Business, BusinessCollection } from './business'
import { PurchaseTransactionCollection } from './purchaseTransaction'
import { CreditCard } from './creditCard'

export function checkPasswordRequirements(password: string, role: string){
    let result = {
        errors: [],
        strong: ''
    }

    owasp.config({
        allowPassphrases       : true,
        maxLength              : 128,
        minLength              : 8,
        minPhraseLength        : 20,
        minOptionalTestsToPass : 2
    })

    result = owasp.test(password)

    return {
        errors: result.errors,
        passes: result.strong
    }
}

export async function verifyUser(email: string, token:string): Promise<User> {
  var verifyError = new Error('Could not verify email.')
  var existingUser = await UserCollection.findOne({ email: email })
  if(!existingUser){
    throw verifyError
  }
  if (token === existingUser.verificationToken) {
    existingUser.isVerified = true
    existingUser.status = AccountStatus.RequiresRegistration 
    await existingUser.save()
    return existingUser
  } else {
    throw verifyError
  }
}

export interface IUser extends IDocument {
    email: string,
    postalCode: number,
    displayName: string,
    picture: string,
    facebook: string,
    role: string,
    readonly businesses: Business[],
    verificationToken: string,
    isVerified: Boolean,
    status: string,
    stripeCustomerId: string,
    paymentMethods: CreditCard[],
    mailing: IAddress
}

let _businesses = new WeakMap<User, Business[]>()

export class User extends Document<IUser> implements IUser {
    static collectionName = 'users'

    private businessIds: ObjectID[]

    private password: string
    public activeRegion: Region
    public picture: string
    public facebook: string

    public verificationToken: string
    public isVerified: boolean
    public status: string 
    public stripeCustomerId: string
    public paymentMethods: CreditCard[]

    public mailing

    public displayName: string
    public email: string
    public postalCode : number
    public role: string

    constructor(user?: IUser) {
      super(User.collectionName, user)

        if(user) {
            if(this.paymentMethods) {
                for(let i = 0; i < this.paymentMethods.length; i++) {
                    this.paymentMethods[i] = CreditCard.build(this.paymentMethods[i])
                }
            }
            if (user.salesRep === null || typeof user.salesRep === 'undefined') {
                this.salesRep = ''
            }

            this.rewardTier = user.rewardTier
        } else {
            this.rewardTier = Tier.Bronze
        }
    }

    getCalculatedPropertiesToInclude(): string[]{
        return ['businesses', 'hasBankingInfo','hasMailingInfo', 'hasBusinessAccountReady', 'noCashier',
            'allRejectedBusiness', 'accountStatus', 'doughBalance', 'tokenBalance']
    }

    getPropertiesToExclude(): string[]{
        return ['password', '_doughBalance', '_tokenBalance']
    }

    async create(displayName: string, email: string, role: string, postalCode: number, password?: string,
        isVerified?: boolean, salesRep?: string) {
        this.displayName = displayName
        this.email = email
        this.postalCode = postalCode
        this.role = role

        if(!password) {
            password = uuid.v4()
        }

        this.password = await this.setPassword(password)
        this.verificationToken = uuid.v4()
        if(typeof isVerified !== 'undefined') {
          this.isVerified = isVerified
          this.status = AccountStatus.RequiresRegistration // when put salesSignup
        } else {
          this.isVerified = false
          this.status = AccountStatus.PendingEmailVerification // when post /auth/signup
        }
        if (salesRep === null || typeof salesRep === 'undefined') {
            this.salesRep = ''
        } else {
            this.salesRep = salesRep
        }

        this.businessIds = []
        this.paymentMethods = []

        await this.save()
    }

    async loadBusinesses() {
        if(Array.isArray(this.businessIds) && this.businessIds.length > 0) {
            let businesses = await BusinessCollection.find({ '_id': { $in: this.businessIds } })
            _businesses.set(this, businesses)
        } else {
            _businesses.set(this, [])
        }
    }

    get businesses(): Business[] {
        return _businesses.get(this) || []
    }

    get hasBankingInfo() {
        if(!this.bankAccount || !this.bankRouting){
            return false
        }
        else{
            return true
        }
    }

    get hasMailingInfo() {
        if(this.mailing) {
            return true
        }
        return false
    }

    get hasBusinessAccountReady(){
        return _.some(this.businesses, function(business){
            return (business.status === BusinessStatus.Active && business.live === true)
        })
    }

    addBusiness(business: Business) {
        this.businessIds.push(business._id)

        if(!_businesses.get(this)) {
            _businesses.set(this, [])
        }
        _businesses.get(this).push(business)
    }

    hasBusiness(businessId: ObjectID) {
        return _.some(this.businessIds, function(id: ObjectID) {
            return id.toHexString() === businessId.toHexString()
        })
    }

    verifyPaymentMethodToken(token) {
      return _.some(this.paymentMethods, { token: token })
    }

    async resetPassword(newPassword: string) {
        this.password = await this.setPassword(newPassword)
        await this.save()
    }

    private setPassword(newPassword: string): Promise<string> {
        return new Promise<string>(function(resolve, reject){
            bcrypt.genSalt(10, function(err, salt) {
                if(err) { return reject(err) }
                bcrypt.hash(newPassword, salt, function(err, hash) {
                    if(err) { return reject(err) }
                    resolve(hash)
                })
            })
        })
    }

    comparePassword(password){
        let user = this
        return new Promise(function(resolve, reject){
            bcrypt.compare(password, user.password, function(err, isMatch) {
                if(err) { return reject(err) }
                resolve(isMatch)
            })
        })
    }

    hasSameId(id: ObjectID): boolean {
        return this._id.toHexString() === id.toHexString()
    }

    setRole(role) {
        this.role = role
    }

    updateVerification(){
        this.verificationToken = uuid.v4()
    }
}

class UserCollectionFactory extends CollectionFactory<User> {
    constructor(docType: typeof User) {
        super(User.collectionName, docType, [ 'displayName', 'email' ])
    }

    activeHotlistQuery(userId: ObjectID): AggregationCursor {
        let aggregateQuery =
        [
            // Stage 1
            {
                $match: {
                    _id: userId
                }
            },
            // Stage 2
            {
                $project: {
                    'hotlists': 1
                }
            },
            // Stage 3
            {
                $unwind: '$hotlists.set'
            },
            // Stage 4
            {
                $match: {
                    'hotlists.set.city': 'Ashland'
                }
            },
            // Stage 5
            {
                $project: {
                    _id: 0,
                'hotlists.set.transactions': 1
                }
            },
            // Stage 6
            {
                $unwind: '$hotlists.set.transactions'
            },
            // Stage 7
            {
                $lookup: {
                    "from" : "businesses",
                    "localField" : "hotlists.set.transactions.businessId",
                    "foreignField" : "_id",
                    "as" : "business"
                }
            },
            // Stage 8
            {
                $unwind: '$business'
            },
            // Stage 9
            {
                $match: {
                'business.live': true
                }
            },
            // Stage 10
            {
                $project: {
                "_id": "$business._id",
                "name": "$business.name",
                "categories": "$business.categories",
                "coordinates": "$business.geoLocation.coordinates",
                "address": "$business.address.addressLine1",
                "image": "$business.image",
                "transactionId": "$hotlists.set.transactions.transactionId",
                "excludeFromHotlists" : "$business.excludeFromHotlists",
                "excludeFromMaps" : "$business.excludeFromMaps",
                }
            }
        ]
        return this.collection().aggregate(aggregateQuery)
    }
}

export let UserCollection = new UserCollectionFactory(User)

DocumentTS

app.get('/users', auth.ensureAdmin, catcher(async function(req, res) {
    var users = await UserCollection.findWithPagination(req.query, undefined, req.query.filter, undefined, true)
    res.send(users);
}));

app.get('/user/:email', auth.ensureAdmin, catcher(async function (req, res) {
  var user = await UserCollection.findOne({ email: req.params.email })
  if(!user){
    return res.status(404).send({ message: 'User not found.' })
  }
  res.send(user)
}));

app.get('/activeHotlist', auth.ensureAuthenticated, catcher(async function (req, res, next) {
  let user: User = req.user

  if (typeof user.hotlists === 'undefined' || user.hotlists === null || user.hotlistCount() <= 0) {
    await hotlists.makeHotlists(user)
  }

  let businesses = await UserCollection.activeHotlistQuery(user._id).toArray()
  res.send(businesses)
}))

Tip: Use http://runkit.com to run Node code with/without npm depedencies

Summary

  • What's a Stack
  • Minimal MEAN
  • DocumentTS
  • Shipping Code
  • Collaboration
  • Code

      Slides & Links

TheJavaScriptPromise.com

    Follow

@duluca