Introduction

Welcome back to our final lesson on this introductory walkthrough on modern Javascript with ES8. Yesterday, we began writing our ES8 version of the api and now we're going to complete it by writing our controllers and services. Today, you should be able to really see the benefits of ES8 come to light and how they can enhance your overall happiness as a developer. So let's dive in.

Getting Started

Let's open user.js and write it out.

// import core modules
const Promise = require('bluebird')

// import models
const {User, Task} = Promise.promisifyAll(require('../model/Model'))

// get user by username
exports.getUser = async ctx => {
  // grab user from database by username
  const {username} = ctx.params
  const user = await User.findOneAsync({username})
  if (!user) {throw new Error('User not found.')}
  // grab task data for user.id
  const taskList = await Task.findAsync({user: user.id})
  if (!taskList) {throw new Error('Task List not found')}
  // send proper user info in response
  ctx.body = {username: user.username, tasks: taskList, id: user.id}
}

// create new user
exports.createUser = async ctx => {
  // format needed data
  const {username, password} = ctx.request.body
  // create user  
  const user = await User.createAsync({username, password})
  if (!user) {throw new Error('Error while creating user.')}
  // send response
  ctx.status = 200
  ctx.body = "success"
}

// delete user by username
exports.deleteUser = async ctx => {
  // get username
  const {username} = ctx.request.body
  // delete user by username
  const deletedUser = await User.findOneAndRemoveAsync({username})
  if (!deletedUser) {throw new Error('Failed to delete user.')}
  // send response if all goes well
  ctx.status = 200
  ctx.body = "success"
}

First, you may notice we're importing Bluebird. It's important to know that we do not need Bluebird to use Promises. We are only using it to convert other callback based libraries to use Promises. Next, let's take note of our use of destructuring all throughout the file. Notice we used it here where we are importing our modules but also here too for grabbing our request body. It just helps keep us from polluting our code with more lines and text than we need. Object literals are also used here and here to once again make our code appear just a little more readable and to the point. However, this would not be complete without addressing the elephant in the room... async/await. Notice how, despite all of our code being asynchronous, it looks like ordinary synchronous code. Everything is in order going straight down the line. No boomerang of doom, not even a Promise cascade. Thus making this code drastically easier to reason about compared to our previous example.

Now let's open user.js and write it out.

// import core modules
const Promise = require('bluebird')

// import models
const {User, Task} = Promise.promisifyAll(require('../model/Model'))

// get all tasks for specific user
exports.getUserTasks = async ctx => {
  // grab username
  const {username} = ctx.params
  // find user by username
  const user = await User.findOneAsync({username})
  if (!user) {throw new Error('User not found')}
  // setup empty array then fill it with all the tasks from the User
  let x = []
  for (let id of user.tasks) {
    const newTask = await Task.findByIdAsync({id})
    if (!newTask) {throw new Error('Task not found.')}
    x.push(newTask)
  }
  // send array of tasks out to the user.
  ctx.body = x
}

// create new task
exports.createTask = async ctx => {
  // get username and taskname info
  const {username, taskname} = ctx.request.body
  // find user by username to get the id
  const user = await User.findOneAsync({username})
  if (!user) {throw new Error('User not found')}
  // create task
  const newTask = await Task.createAsync({name: taskname, user: user.id})
  if (!newTask) {throw new Error('Task failed to create.')}
  // send success signal
  ctx.body = {taskname: newTask.name, id: newTask.id}
}

// edit task
exports.editTask = async ctx => {
  // grab the taskId, and new taskname
  const {taskId, newTaskName} = ctx.request.body
  // Find the task and update it
  const editTask = await Task.findByIdAndUpdateAsync(taskId, {name: newTaskName})
  if (!editTask) {throw new Error('Failed to update task.')}
  // send success signal
  ctx.status = 200
  ctx.body = 'success'
}

// delete task
exports.deleteTask = async ctx => {
  // grab the username and taskname
  const {username, taskId} = ctx.params
  // get the user
  const user = await User.findOneAsync({username})
  if (!user) {throw new Error('User not found')}
  // delete Task and edit User in parallel operation
  const deadTask = Task.findByIdAndRemoveAsync(taskId)
  const editUser = User.findOneAndUpdateAsync({username}, {$pull: {tasks: {$in: [taskId]}}})
  const [dt, eu] = await Promise.all([deadTask, editUser])
  if (!dt || !eu) {throw new Error('Agh! Failed to delete user.')}
  // // send success signal
  ctx.status = 200
  ctx.body = 'success'
}

Again, take note of the use of destructuring and object literals being used through out the code here. It's also worth noting the use of the new for..of loop here rather than the tradition ES5 for loop which is just far easier to read and comprehend in a real world environment. Lastly, let's take a look at deleteTask. Remember how we demonstrated in the examples that async/await, Promises, and destructuring can be used together to create concurrent operations? Well, notice that is precisely what we are are doing here. This is far better both aesthetically and functionally compared to our original callback based version of this operation.

Now let's switch gears and open utility.js.

const Promise = require('bluebird')
const config = require('../config')
const jwt = Promise.promisifyAll(require('jsonwebtoken'))
const User = require('../model/Model').User

const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
exports.getRandomInt = getRandomInt

exports.generateJwt = payloadInfo => {
    const payload = payloadInfo
  const opts = {expiresIn: '1h'}
    return jwt.signAsync(payload, config.secret, opts)
}

exports.uid = len => {
  let buf = []
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
  const charlen = chars.length

  for (var i = 0; i < len; ++i) {
    buf.push(chars[getRandomInt(0, charlen - 1)])
  }

  return buf.join('')
}

First, notice how we managed to turn getRandomInt into a one line function with an implicit return. This is drastically different from the original ES5 version of the function. Now if you turn your attention to the uid method, you'll see that... hardly anything has changed and we're still using the old for loop. Why? Because for this particular job, the old style for loop ended up being the best and cleanest method I could come up with for handling the job. It just goes to show, that as developers, even when we have lots of shiny new things, sometimes the old ways of handling a job can still be the best one. It's about using discretion.

Now let's open clientAuth.js.

/*
* Passport configuration happens here.
*/

const Promise = require('bluebird')
const passport = Promise.promisifyAll(require('koa-passport'))
const ClientPasswordStrategy = require('passport-oauth2-client-password').Strategy
const Client = require('../model/Model').Client

/*
* Authenticate a client using the Client/Password strategy and passing the
* login info in the request body.
*/
passport.use('clientPassword', new ClientPasswordStrategy((clientId, clientSecret, done) => {
    // find the client by their id
  Client.findOne({clientId: clientId}, (err, client) => {
        // handle errors
    if (err) return done(err)
    if (!client) return done(null, false)
        // make sure the client is TRUSTED
    if (!client.trustedClient) return done(null, false)
        // if the clientSecrets match, return done
    if (client.clientSecret == clientSecret) return done(null, client)
    else return done(null, false)
  })
}))

As you may have noticed here, nothing really changed beyond using the arrow function. Why? Because some aspects of Passport just can't be promisified and so we had to use callbacks here. At the very least, we were able to do some minor doctoring with our arrow functions and use of const.

Let's open oauth.js.

/*
* OAuth2orize token exchanges are defined here.
* In this case, you are using the Password Exchange Flow
* and the Refresh Token Exchange Flow
*/

// load modules
const Promise = require('bluebird')
const oauth2orize = require('oauth2orize-koa')
const {Client, User} = Promise.promisifyAll(require('../model/Model'))
const jwt = Promise.promisifyAll(require('jsonwebtoken'))
const crypto = require('crypto')
const fs = require('fs')
const passport = require('koa-passport')
const bcrypt = Promise.promisifyAll(require('bcrypt-nodejs'))
const utility = require('../services/utility')
const config = require('../config')

/*
*   Config OAuth2orize
*/
// create OAuth 2.0 server
const server = oauth2orize.createServer()

// Password exchange flow
server.exchange(oauth2orize.exchange.password(async (client, username, password, scope) => {
  // generate refresh token
  const refreshToken = utility.uid(256)
  // encrypt the refresh token
  const refreshTokenHash = crypto.createHash('sha1').update(refreshToken).digest('hex')
  // Find user by email
  const user = await User.findOneAsync({username})
  if (!user) {return false}
  // password check
  const passwordCompareResult = await bcrypt.compareAsync(password, user.password)
  if (!passwordCompareResult) {return false}
  // format the jwt data
  const payload = {
    name: user.username,
    userId: user.id,
    sub: user.username,
    aud: 'todo-list',
    issuer: 'todo-list',
    role: user.role,
    clientId: client.clientId
  }
  // create jwt
  const token = await jwt.signAsync(payload, config.secret, {expiresIn: '1h'})
  if (!token) {return false}

  // send jwt
  return token
}))

// token endpoint
exports.token = [
  passport.authenticate(['clientBasic', 'clientPassword'], { session: false }),
  server.token(),
  server.errorHandler()
]

exports.server = server

Now this is a drastic change from our previous OAuth2orize setup. Without being stuck with callbacks, we were able to write code that was drastically more orderly. Notice how the async operations fall right in line with the synchronous ones. Thus opening the path to an overall happier development experience. Especially when debugging.

Summary

Congratulations! We've finished writing out APIs in both ES5 and ES8. You've been able to see much of what ES8 has to offer in terms of offering a happier development experience. That being said, this still doesn't encompass everything that modern Javascript has to offer. We now have Map, WeakMap, Sets, WeakSets, Reflections, and Symbols. The goal of this tutorial though was to give you an introductory look at what ES8 is and what it has to offer in your real world production code. For the hard nosed details, it is highly advised to check out all of the helpful links including reference on how to use the ES8 additions that weren't covered in this tutorial. Thank you and I sincerely hope this has been of value to you in your growth as a developer.

Franzé Jr

Software Engineer with experience working in multi-cultural teams, Franze wants to help people when he can, and he is passionate about programming and Computer Science. Founder of RemoteMeetup.com where he can meet people all over the World. When Franze is not coding, he is studying something about programming.

  1. Comments for Modern Javascript with ES8

You must login to comment

You May Also Like