Doguhan Uluca PRO
Author of the best-selling Angular for Enterprise-Ready Web Apps. Google Developers Expert in Angular. Agile, JavaScript and Cloud expert, Go player.
Full Stack TypeScript
Doguhan Uluca
Questions? @duluca
Slides? TheJavaScriptPromise.com
Full Stack TypeScript
Doguhan Uluca
Questions? @duluca
Slides? TheJavaScriptPromise.com
stack
Presentation
API
Business
Persistence
Best Practices
IDE
Patterns
Libraries
Angular && Angular Material
Express
Node
Native Mongo Driver
VS Code
npm
GitHub
gulp
TypeScript
CI
Web
Mobile Web
Native Mobile
Native
Desktop
Server-Side
Progressive
Angular Core
Web
Ionic
Universal
React
Electron
View
View Model
Controller / Router
Services ($http, redux, logic)
Best Practices
IDE
Patterns
Libraries
app.ts
rootRouter
services
filters
directives
/a: default
/master
/detail
/b/...
/c
childRouter
/d
/e
/f
High quality components
CSS Flexbox
You won't have to write CSS
Dynamic forms
Fast, unopinionated, minimalist web framework for Node.js
Lightweight and efficient JavaScript runtime, using an event-driven, non-blocking I/O model
Document-oriented database with dynamic JSON-like schemas
Official Node.js APIs allowing access to all native functionality
Build script or task automation system
GrossException Thrown
Bugs
Issues
Discussions
New features
Code Reviews
Status Updates
Angular Setup
Server Setup
DocumentTS
Use of async/await
Docker Setup
Database Connectivity
Heroku
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);
}
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
}
'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)
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
Slides & Links
Follow
http://slides.com/doguhanuluca/agile-done-right-using-javascript-and-node#/6/2
http://blog.edenmsg.com/angular2-typescript-gulp-and-expressjs/
https://github.com/Microsoft/TypeScriptSamples/tree/master/imageboard
http://www.jbrantly.com/es6-modules-with-typescript-and-webpack/
https://www.airpair.com/node.js/posts/nodejs-framework-comparison-express-koa-hapi
http://www.excella.com/blog/node-js-challenges-why-mean-doesnt-work-but-a-hapi-leb...
https://docs.google.com/presentation/d/1anh69vBK1OLtB1RjwpKzeYmxCGMLX_HdJ...
https://www.excella.com/insights/youre-probably-using-github-wrong
By Doguhan Uluca
It can be daunting to pick the right stack to deliver your idea to the cloud. Without realizing, you can introduce one too many "sandbag of complexity" between you and something you can release. For the first time ever it is possible to do full-stack development with a consistent set of tools and best practices using the same language, JavaScript. No more to context switching between front-end and back-end languages, libraries and tools. That is The JavaScript Promise. My easy to learn and use stack 'Minimal MEAN' will get you started and deployed on the cloud over a lazy weekend, without requiring a MongoDB install, while leveraging the latest tools like async/await with Typescript, ng2, Node v6.
Author of the best-selling Angular for Enterprise-Ready Web Apps. Google Developers Expert in Angular. Agile, JavaScript and Cloud expert, Go player.