Skip to content

Commit

Permalink
Merge branch 'simple-promises'
Browse files Browse the repository at this point in the history
  • Loading branch information
justsml committed Apr 9, 2018
2 parents d269b4a + 70a2d0d commit b48f9cb
Show file tree
Hide file tree
Showing 15 changed files with 113 additions and 191 deletions.
1 change: 0 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"camelcase": 1,
"comma-style": [1, "last"],
"consistent-this": [1, "self"],
"curly": [1, "multi"],
"eol-last": 1,
"eqeqeq": 1,
"func-names": 1,
Expand Down
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

[![Build Status](https://travis-ci.org/justsml/escape-from-callback-mountain.svg?branch=master)](https://travis-ci.org/justsml/escape-from-callback-mountain)

## Refactoring NodeJS/JavaScript - a 2017 Guide
## Refactoring JavaScript w/ Functional River Pattern

I am a big fan of Functional Programming and Modular JavaScript. This project's goal is to demonstrate the latest Functional Promise patterns, while taking you through a refactor of real world callback-based NodeJS/JavaScript.

#### Side-by-side Comparison: Callbacks & Promises
![image](https://user-images.githubusercontent.com/397632/28998900-9a7de362-79f3-11e7-83b2-09864d09f8fd.png)
#### Functional River Overview
![Functional River Highlights](https://user-images.githubusercontent.com/397632/38474143-e96bf632-3b57-11e8-8589-cbe3b3782d1a.gif)

The [technique I demonstrate](#after) is what I call the **_Functional River_ pattern**. Your input/parameters/data is the water, and the code forms the riverbed. More or less.
It is an async+sync version of the [Collection Pipeline](https://martinfowler.com/articles/collection-pipeline/) pattern.
Expand All @@ -27,9 +27,9 @@ _So forgive me if I skip the overblown theory & jargon._
* Substantially faster code readability - versus [methods which muddles the important parts, and further hides ad hoc error/glue code](https://github.com/justsml/escape-from-callback-mountain/wiki/Beating-a-dead-horse%3F).

> Note: Relies on ideas from Lisp to SmallTalk - adapted to a JavaScript world.
> Additionally, I happen to use [Bluebird Promises](http://bluebirdjs.com/docs/features.html). Apologies to `Promise Resistance Leader` [Brian Leroux](https://twitter.com/brianleroux). For alternative patterns please read my more detailed article demonstrating [4 Functional JavaScript Techniques (with Examples)](http://www.danlevy.net/2017/03/10/functional-javascript-composition/)
> Apologies to `Promise Resistance Leader` [Brian Leroux](https://twitter.com/brianleroux). For alternative patterns please read my more detailed article demonstrating [4 JavaScript Composition Techniques (with Examples)](http://www.danlevy.net/2017/03/10/functional-javascript-composition/)
#### Have feedback, fixes or questions? Please create issues or PRs. Or just complain at me on [twitter @justsml](https://twitter.com/justsml).
#### Have feedback, fixes or questions? Please create issues or PRs. Or DM me at [twitter @justsml](https://twitter.com/justsml).

If you feel this subject has been thoroughly explored, please see my post [Beating a dead horse?](https://github.com/justsml/escape-from-callback-mountain/wiki/Beating-a-dead-horse%3F)

Expand Down Expand Up @@ -94,12 +94,9 @@ Here's a rough visualization of our function:
This area of Functional JS patterns, and consenus around it's best practices has plenty room to go.


## Concerns
## Summary

#### Some really smart people out there have pointed out potential problems with over-modularization.
![image](https://cloud.githubusercontent.com/assets/397632/25776158/12d0be56-3274-11e7-87c9-7dee8a5e4b09.png)

While true of most coding patterns, an overly-done flat & modular JS Project can feel more disorganized over time.
An overly-done flat & modular JS Project can feel more disorganized over time.
Project and code discipline is just as important as it's always been. Also, the community is still developing consensus around Functional JS patterns, immutability and project organization.

When done right, one of _Functional River's_ **greatest strengths** is the ability to **relocate & rearrange** modules with **low risk**. If this still feels risky, your modules are probably still too entangled.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "escape-from-callback-mountain",
"version": "1.5.1",
"version": "2.0.0",
"description": "A Modern Guide to Functional Promises w/ many examples",
"main": "src/auth.js",
"directories": {
"example": "examples",
"src": "src"
},
"scripts": {
"test": "jscs src && NODE_ENV=production ava --verbose --serial",
"test": "jscs src && NODE_ENV=production ava --verbose --serial src/auth.test.js",
"start": "npm install && docker-compose -p task-queue up --build",
"postinstall": "cd examples/distributed-http-task-queue && npm install"
},
Expand Down
24 changes: 9 additions & 15 deletions src/auth.async.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
// 2/4: ASYNC/AWAIT
// Latest fad
const Promise = require('bluebird')
const {hashString} = require('./lib/crypto')
const {logEventAsync} = require('./lib/log')
const {getModel} = require('./lib/db')

const Users = getModel('users')
const {hashString} = require('./lib/crypto')
const users = require('./lib/users')

module.exports = {auth}

Expand All @@ -14,20 +10,18 @@ async function auth(username, password) {
if (!username || username.length < 1) throw new Error('Invalid username.')
if (!password || password.length < 6) throw new Error('Invalid password.')

logEventAsync({event: 'login', username})()

let hashedQuery = {
let query = {
username,
password: await hashString(password)
password: hashString(password)
}
let user = await Users
.findOneAsync(hashedQuery)
let user = await users
.getOne(query)

if (user && user._id)
if (user && user._id) {
return user
else
} else {
throw new Error('User Not found!')

}
} catch (ex) {
console.error(ex)
}
Expand Down
22 changes: 9 additions & 13 deletions src/auth.callbacks.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
// CALLBACKS - NodeJS, Original Recipe
const {hashString} = require('./lib/crypto')
const {logEvent} = require('./lib/log')
const {getModel} = require('./lib/db')
const {hashString} = require('./lib/crypto')
const users = require('./lib/users')

function auth(username, password, callback) {
if (!username || username.length < 1) return callback(new Error('Invalid username.'))
if (!password || password.length < 6) return callback(new Error('Invalid password.'))
if (!callback) throw new Error('Callback arg required!')
if (!callback) throw new Error('Callback arg required!')

getModel('users', function _models(err, users) {
hashString(password, function _hashPass(err, password) {
if (err) return callback(err)
hashString(password, function _hashed(err, hash) {
users.getOne({username, password}, function _handleUser(err, results) {
if (err) return callback(err)
users.findOne({username, password: hash}, function _find(err, results) {
if (err) return callback(err)
if (!results)
return callback(new Error('No users matched. Login failed'))

logEvent({event: 'login', username}, function _noOp() {/* do nothing */})
if (!results) {
callback(new Error('No users matched. Login failed'))
} else {
callback(null, results)
})
}
})
})
}
Expand Down
17 changes: 7 additions & 10 deletions src/auth.fp.await.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
// 3/4: EXPERIMENT w/ FUNCTIONAL PROMISES + ASYNC/AWAIT
// 3/4: VERSION USING ASYNC/AWAIT
// A little more modular fad
const Promise = require('bluebird')
const {hashString} = require('./lib/crypto')
const {logEventAsync} = require('./lib/log')
const {getModel} = require('./lib/db')
const {hashString} = require('./lib/crypto')
const users = require('./lib/users')

module.exports = {auth}

async function auth({username, password}) {
if (_isInputValid({username, password})) {
logEventAsync({event: 'login', username})()
const user = await _loginUser({username, password})
if (user && user._id)
if (user && user._id) {
return user

}
throw new Error('User Not found!')
}
}

async function _loginUser({username, password}) {
let query = {
username,
password: await hashString(password)
password: hashString(password)
}
return await getModel('users').findOneAsync(query)
return await users.getOne(query)
}

function _isInputValid({username, password}) {
Expand Down
26 changes: 12 additions & 14 deletions src/auth.fp.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,29 @@
// FUNCTIONAL PROMISES (aka Functional River)
const Promise = require('bluebird')
const {hashStringAsync} = require('./lib/crypto')
const {logEventAsync} = require('./lib/log')
const {getModelAsync} = require('./lib/db')

module.exports = {auth}
// Functional River Pattern
const {hashString} = require('./lib/crypto')
const users = require('./lib/users')

function auth({username = '', password = ''}) {
return Promise.resolve({username, password})
.then(_checkArgs)
.tap(logEventAsync({event: 'login', username}))
.then(_loginUser)
.then(_checkUser)
}

function _checkArgs({username = '', password = ''}) {
if (username.length < 1 || password.length < 6) throw new Error('Check args')
if (username.length < 1) throw new Error('Enter valid username')
if (password.length < 6) throw new Error('Enter valid Password')
return {username, password}
}

function _loginUser({username, password}) {
return Promise.props({
Users: getModelAsync('users'),
password: hashStringAsync(password)
}).then(({Users, password}) => Users.findOneAsync({username, password}))
return Promise.resolve(password)
.then(hashString)
.then(password => users.getOne({username, password}))
}

function _checkUser(user) {
return user && user._id ? user : Promise.reject(new Error('User Not found!'))
if (user && user._id) return user
throw new Error('No User found. Check credentials and try again.')
}

module.exports = {auth, _checkArgs, _loginUser, _checkUser}
53 changes: 28 additions & 25 deletions src/auth.test.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
const test = require('ava')
const {auth, _checkArgs, _loginUser, _checkUser} = require('./auth.fp')

test.before('create user records', t => {
const {addUserAsync} = require('./lib/db')
return addUserAsync({id: 1, username: 'alice', email: 'alice@nsa.gov', password: 'superSecret1'})
.tap(result => {
// console.log('\ntest user:', result, '\n')
const users = require('./lib/users')
const {hashString} = require('./lib/crypto')
const password = hashString('superSecret1')
return users.create({username: 'alice', email: 'alice@nsa.gov', password})
.then(result => {
t.is(typeof result, 'object')
t.pass()
})
})

test.cb('callback: login successful', t => {
test('fun. river: _checkArgs', t => {
t.plan(2)
const {auth} = require('./auth.callbacks')
return auth('alice', 'superSecret1', (err, result) => {
t.is(result.username, 'alice')
t.falsy(err)
t.end()
})
const args = _checkArgs({username: 'alice', password: 'superSecret1'})
t.truthy(args.username)
t.truthy(args.password)
})

test.cb('callback: login failed', t => {
t.plan(1)
const {auth} = require('./auth.callbacks')
auth('eve', 'fooBar', (err, result) => t.truthy(err) || t.end())
test('fun. river: _loginUser', t => {
t.plan(2)
const result = t.notThrows(() => _loginUser({username: 'alice', password: 'superSecret1'}))
t.truthy(result == undefined)
})

test('fp/river: login successful', t => {
test('fun. river: _checkUser', t => {
t.plan(2)
const result = t.throws(() => _checkUser(null))
t.truthy(/No User found/.test(result.message))
})

test('fun. river: login successful', t => {
t.plan(2)
const {auth} = require('./auth.fp')
return auth({username: 'alice', password: 'superSecret1'})
.tap(result => {
t.is(result.username, 'alice')
t.pass()
})
.then(result => {
t.is(result.username, 'alice')
t.pass()
})
})

test('fp/river: login failed', t => {
test('fun. river: login failed', t => {
t.plan(1)
const {auth} = require('./auth.fp')
return auth({username: 'eve', password: 'fooBar'})
.then(result => t.fail())
.catch(err => t.pass())
.then(result => t.fail())
.catch(err => t.pass())
})

18 changes: 6 additions & 12 deletions src/lib/crypto.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
const Promise = require('bluebird')
const crypto = require('crypto')
const crypto = require('crypto')

module.exports = {
hashString,
hashStringAsync: Promise.promisify(hashString)
}
module.exports = {hashString}

function hashString(s, callback = (x, y) => y || x) {
try {
return callback(null, crypto.createHash('sha256').update(s).digest('hex'))
} catch (e) {
return callback(e)
}
function hashString(s, callback) {
s = crypto.createHash('sha256').update(s).digest('hex')
if (callback) callback(null, s)
return s
}
34 changes: 0 additions & 34 deletions src/lib/db.js

This file was deleted.

22 changes: 0 additions & 22 deletions src/lib/db.test.js

This file was deleted.

13 changes: 0 additions & 13 deletions src/lib/log.js

This file was deleted.

Loading

0 comments on commit b48f9cb

Please sign in to comment.